avatar fix
Gitea Actions / Deploy (push) Successful in 29s

sessions fix
password change
sign-in fix
This commit is contained in:
vadim committed 2026-10-23 19:25:11 +03:00
1 parent 63861449cf
commit f0706432d4
20 files changed
+539 -117

No files matched your search

+106 -4
View File
@@ -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>