Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions client/public/img/icons/avatar.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 14 additions & 7 deletions client/src/components/accounts/tokens.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
{{ item.id }}
</template>
<template v-slot:[`item.user.username`]="{ item }">
<span>{{ item.user.username }}</span>
<span>{{ item.user?.username }}</span>
</template>
<template v-slot:[`item.expiresAt`]="{ item }">
<span v-if="item.expiresAt">{{ new Date(item.expiresAt).toLocaleString() }}</span>
Expand All @@ -47,7 +47,7 @@
</template>
</v-data-table>

<!-- Button to add a token -->
<!-- Button to add a token
<div style="display: flex; justify-content: flex-end; margin-top: 16px;">
<v-btn
fab
Expand All @@ -60,12 +60,12 @@
</v-btn>
</div>

<!-- Dialog for a new Token -->
<!-- Dialog for a new Token
<v-dialog v-model="createDialog" max-width="500px">
<v-card>
<v-card-title>Create Token</v-card-title>
<v-card-text>
<v-text-field v-model="newToken.token" label="Token Value"></v-text-field>
<v-text-field v-model="newToken.name" label="Name"></v-text-field>
<v-text-field
v-model="newToken.expiresAt"
label="Expires At (ISO)"
Expand All @@ -79,6 +79,7 @@
</v-card-actions>
</v-card>
</v-dialog>
-->
</v-container>
</template>

Expand All @@ -91,18 +92,24 @@ export default defineComponent({
setup() {
interface Token {
id?: string;
token: string;
token?: string;
name: string;
expiresAt?: string;
userId?: string;
user?: {
id: string;
username: string;
};
}
const tokens = ref<Token[]>([])
const loading = ref(false)
const search = ref('')
const createDialog = ref(false)
const newToken = ref<Token>({ token: '', expiresAt: '', userId: '' })
const newToken = ref<Token>({ token: '', name: '', expiresAt: '', userId: '' })

const headers = [
{ title: 'Token ID', value: 'token' },
{ title: 'Name', value: 'name' },
{ title: 'Owner', value: 'user.username' },
{ title: 'Expires At', value: 'expiresAt' },
{ title: 'Actions', value: 'actions', sortable: false, align: 'end' as const },
Expand All @@ -129,7 +136,7 @@ export default defineComponent({
}

const openCreateDialog = () => {
newToken.value = { token: '', expiresAt: '', userId: '' }
newToken.value = { token: '', name: '', expiresAt: '', userId: '' }
createDialog.value = true
}

Expand Down
138 changes: 131 additions & 7 deletions client/src/components/profile/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<v-container class="py-8">
<v-row>
<v-col cols="12" md="6" lg="4">
<v-card class="pa-4">
<v-container class="pa-4">
<div style="position: relative; display: inline-block;">
<v-avatar size="150" class="mb-4">
<v-img :src="user.image || defaultAvatar" alt="User avatar" />
Expand Down Expand Up @@ -42,12 +42,12 @@
</v-card-actions>
</v-card>
</v-dialog>
</v-card>
</v-container>
</v-col>
<v-col cols="12" md="6" lg="8">
<v-card class="pa-4">
<v-card color="cardBackground" class="pa-4">
<h3 class="mb-4">Profile Details</h3>
<v-table density="compact">
<v-table density="compact" class="profile-table">
<tbody>
<tr>
<td><strong>First Name</strong></td>
Expand Down Expand Up @@ -93,9 +93,20 @@
</v-row>
<v-row class="mt-6">
<v-col cols="12">
<v-card class="pa-4">
<v-card color="cardBackground" class="pa-4">
<h3 class="mb-4">API Tokens</h3>
<v-table density="compact">
<div style="display: flex; justify-content: flex-end; margin-bottom: 8px;">
<v-btn
fab
color="primary"
style="margin-right: 6px;"
@click="openCreateDialog"
>
<v-icon>mdi-plus</v-icon>
<span class="sr-only">Create Token</span>
</v-btn>
</div>
<v-table density="compact" class="profile-table">
<thead>
<tr>
<th>Name</th>
Expand Down Expand Up @@ -126,6 +137,63 @@
</tr>
</tbody>
</v-table>
<v-dialog v-model="createDialog" max-width="500px">
<v-card>
<v-card-title>Create Token</v-card-title>
<v-card-text>
<v-text-field v-model="newToken.name" label="Name"></v-text-field>
<v-text-field
v-model="newToken.expiresAt"
label="Expires At (ISO)"
type="datetime-local"
></v-text-field>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn text @click="createDialog = false">Abort</v-btn>
<v-btn color="primary" @click="saveCreate">Create</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="tokenDialog" max-width="500px">
<v-card>
<v-card-title>Token Details</v-card-title>
<v-card-text>
<v-alert type="warning" density="compact" class="mb-2">
This token will <strong>not be shown again</strong>. Please copy and store it securely now.
</v-alert>
<v-textarea
v-model="generatedToken.token"
label="Token"
auto-grow
readonly
rows="3"
class="mb-2"
:class="{ 'flash': textareaFlash }"
></v-textarea>
<v-btn
color="primary"
@click="copyToken"
class="mb-2"
>
<v-icon left>mdi-content-copy</v-icon>
Copy Token
</v-btn>
<v-snackbar v-model="textareaFlash" timeout="3000">
Token copied to clipboard!
<template #actions>
<v-btn text @click="textareaFlash = false">
Close
</v-btn>
</template>
</v-snackbar>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn text @click="tokenDialog = false">Close</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-card>
</v-col>
</v-row>
Expand All @@ -150,10 +218,15 @@ export default defineComponent({
provider: '',
lastLogin: null,
})
const defaultAvatar = '/avatar.svg'
const defaultAvatar = '/img/icons/avatar.svg'
const tokens = ref<any[]>([])
const editAvatarDialog = ref(false)
const avatarFile = ref<File | null>(null)
const createDialog = ref(false)
const tokenDialog = ref(false)
const generatedToken = ref<any>({ name: '', expiresAt: '', token: '' })
const newToken = ref<any>({ name: '', expiresAt: '' })
const textareaFlash = ref(false)

const loadProfile = async () => {
try {
Expand Down Expand Up @@ -197,6 +270,34 @@ export default defineComponent({
}
}

const openCreateDialog = () => {
newToken.value = { name: '', expiresAt: '', token: '' }
createDialog.value = true
}

const saveCreate = async () => {
try {
const response = await axios.post('/api/tokens', newToken.value)
generatedToken.value = response.data
await loadTokens()
createDialog.value = false
tokenDialog.value = true
console.log('Token created:', newToken)
} catch (e) {
// error handling
}
}

const copyToken = () => {
if (generatedToken.value.token) {
navigator.clipboard.writeText(generatedToken.value.token)
textareaFlash.value = true
setTimeout(() => {
textareaFlash.value = false
}, 300)
}
}

onMounted(() => {
loadProfile()
loadTokens()
Expand All @@ -210,7 +311,30 @@ export default defineComponent({
editAvatarDialog,
avatarFile,
saveAvatar,
createDialog,
tokenDialog,
generatedToken,
newToken,
openCreateDialog,
saveCreate,
copyToken,
textareaFlash,
}
},
})
</script>

<style scoped>
.flash {
animation: flash-animation 3s ease-in-out;
}

@keyframes flash-animation {
0% { background-color: rgba(255, 255, 0, 0.3); }
100% { background-color: transparent; }
}

.profile-table {
background-color: inherit;
}
</style>
Loading
Loading