sessions fix password change sign-in fix
This commit is contained in:
1 parent
63861449cf
commit
f0706432d4
20 files changed
+539
-117
No files matched your search
@@ -1,5 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref, useTemplateRef } from 'vue'
|
||||
import { authClient } from '@/lib/auth-client.ts'
|
||||
import { resizeImage } from '@/lib/image-resize.ts'
|
||||
import { fileUpload } from '@/api/file.api.ts'
|
||||
import { avatarRemove, avatarSet } from '@/api/user.api.ts'
|
||||
import { useSnackbarStore } from '@/stores/snackbar.ts'
|
||||
import EditableText from '@/components/EditableText.vue'
|
||||
|
||||
interface Props {
|
||||
user?: {
|
||||
@@ -13,20 +19,116 @@ interface Props {
|
||||
}
|
||||
}
|
||||
|
||||
/** The largest avatar is 128px in the profile, so 512px covers HiDPI screens */
|
||||
const AVATAR_MAX_SIZE = 512
|
||||
/** The server shows only these types inline */
|
||||
const AVATAR_TYPES = 'image/png,image/jpeg,image/gif,image/webp'
|
||||
|
||||
const { user } = defineProps<Props>()
|
||||
|
||||
const snackbarStore = useSnackbarStore()
|
||||
const input = useTemplateRef<HTMLInputElement>('input')
|
||||
const avatarSaving = ref(false)
|
||||
|
||||
const avatar = computed(() => {
|
||||
return user?.image ?? undefined
|
||||
})
|
||||
|
||||
function showError(text: string, e: unknown) {
|
||||
snackbarStore.add({ title: text, text: e instanceof Error ? e.message : '', color: 'error' })
|
||||
}
|
||||
|
||||
// The avatar is changed by our API, not better-auth, so its 30s session cookie cache
|
||||
// is stale: re-read the session from the DB, then let useSession() consumers refetch
|
||||
async function refreshSession() {
|
||||
await authClient.getSession({ query: { disableCookieCache: true } })
|
||||
authClient.$store.notify('$sessionSignal')
|
||||
}
|
||||
|
||||
async function saveName(name: string) {
|
||||
const trimmed = name.trim()
|
||||
if (!trimmed || trimmed === user?.name) return
|
||||
|
||||
const { error } = await authClient.updateUser({ name: trimmed })
|
||||
if (error) showError('Failed to change name', new Error(error.message))
|
||||
}
|
||||
|
||||
function openPicker() {
|
||||
input.value?.click()
|
||||
}
|
||||
|
||||
async function onInputChange(event: Event) {
|
||||
const target = event.target as HTMLInputElement
|
||||
const file = target.files?.[0]
|
||||
// Allow picking the same file again
|
||||
target.value = ''
|
||||
if (!file) return
|
||||
|
||||
avatarSaving.value = true
|
||||
try {
|
||||
const stored = await fileUpload(await resizeImage(file, AVATAR_MAX_SIZE))
|
||||
await avatarSet(stored.id)
|
||||
await refreshSession()
|
||||
} catch (e) {
|
||||
showError('Failed to change avatar', e)
|
||||
} finally {
|
||||
avatarSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function removeAvatar() {
|
||||
avatarSaving.value = true
|
||||
try {
|
||||
await avatarRemove()
|
||||
await refreshSession()
|
||||
} catch (e) {
|
||||
showError('Failed to remove avatar', e)
|
||||
} finally {
|
||||
avatarSaving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-sheet border class="pa-4 d-flex flex-column ga-2">
|
||||
<div v-if="user" class="d-flex ga-4">
|
||||
<v-avatar size="x-large" color="info" :image="avatar" />
|
||||
<v-menu>
|
||||
<template #activator="{ props: menuProps }">
|
||||
<v-hover #default="{ isHovering, props: hoverProps }">
|
||||
<v-avatar v-bind="{ ...menuProps, ...hoverProps }" size="128" color="background" class="cursor-pointer">
|
||||
<!-- With a default slot v-avatar ignores its image prop, so the image goes in the slot -->
|
||||
<v-img v-if="avatar" :src="avatar" alt="" cover />
|
||||
<v-icon v-else icon="mdi-account" size="64" />
|
||||
<v-overlay
|
||||
:model-value="isHovering || avatarSaving"
|
||||
contained
|
||||
persistent
|
||||
scrim="black"
|
||||
class="align-center justify-center"
|
||||
>
|
||||
<v-progress-circular v-if="avatarSaving" indeterminate size="40" width="3" color="white" />
|
||||
<v-icon v-else icon="mdi-camera" size="40" color="white" />
|
||||
</v-overlay>
|
||||
</v-avatar>
|
||||
</v-hover>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<div>Name: {{ user.name }}</div>
|
||||
<v-list density="compact">
|
||||
<v-list-item prepend-icon="mdi-upload" title="Upload photo" :disabled="avatarSaving" @click="openPicker" />
|
||||
<v-list-item
|
||||
v-if="avatar"
|
||||
prepend-icon="mdi-trash-can"
|
||||
title="Remove photo"
|
||||
base-color="error"
|
||||
:disabled="avatarSaving"
|
||||
@click="removeAvatar"
|
||||
/>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<input ref="input" type="file" :accept="AVATAR_TYPES" hidden @change="onInputChange" />
|
||||
|
||||
<div class="flex-grow-1">
|
||||
<editable-text class="mb-2" :text="user.name" placeholder="Enter your name" required @changed="saveName" />
|
||||
<div>E-mail: {{ user.email }}</div>
|
||||
<div>Created At: {{ user.createdAt }}</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user