/* Who is using the wiki right now. A session is a bearer token the API handed us, kept in a cookie rather than localStorage so the server can see it too: the sidebar then knows who is signed in on the very first draw, before any script runs. The token itself is never decoded here — the API is asked who it belongs to (see restoreSession). */ export interface AuthUser { id: string username: string /** Profile picture as a data URL; "" when the person has not chosen one. */ avatar: string /** An admin opens the account dashboard; a member reads and writes. */ role: AuthRole /** The interface language saved on the account; "" follows the browser. */ locale: string } export type AuthRole = 'admin' | 'member' interface Session { token: string user: AuthUser } const TOKEN_COOKIE = 'mathew-session' const WEEK_IN_SECONDS = 60 * 60 * 24 * 7 export function useAuth() { const api = `${useRuntimeConfig().public.apiBase}/api` const token = useCookie(TOKEN_COOKIE, { default: () => null, maxAge: WEEK_IN_SECONDS, sameSite: 'lax', }) const user = useState('auth:user', () => null) const loggedIn = computed(() => user.value !== null) /* Whether this session may open the admin dashboard. The API checks the account again on every admin request — this only decides what to draw. */ const isAdmin = computed(() => user.value?.role === 'admin') /* Header for the write endpoints. The read endpoints are public and ignore it. */ function authHeaders(): Record { return token.value ? { Authorization: `Bearer ${token.value}` } : {} } function openSession(session: Session) { token.value = session.token user.value = session.user } /* Signing out is local: the token simply stops being sent, and it lapses on its own — the API issues nothing that needs revoking. */ function signOut() { token.value = null user.value = null } async function signIn(username: string, password: string) { const session = await $fetch(`${api}/auth/login`, { method: 'POST', body: { username: username.trim().toLowerCase(), password }, }) openSession(session) return session.user } /* Opening an account signs you straight into it: the API answers with the same token pair as a login would. */ async function signUp(username: string, password: string) { const session = await $fetch(`${api}/auth/register`, { method: 'POST', body: { username: username.trim().toLowerCase(), password }, }) openSession(session) return session.user } /* Runs once at startup, on the server and in the browser: asks the API who a stored token belongs to. A token the API refuses — expired, or the account is gone — is dropped here rather than at the first save. A request that never got through (the backend is restarting) leaves it in place. */ async function restoreSession() { if (!token.value || user.value) return try { const { user: known } = await $fetch<{ user: AuthUser }>(`${api}/auth/me`, { headers: authHeaders(), }) user.value = known } catch (err: any) { if (err?.statusCode === 401 || err?.response?.status === 401) token.value = null } } /* The account updates from the profile popup. Each comes back with the account as the API now has it, so the sidebar and the popup are showing the truth the moment the request resolves. */ async function updateUsername(username: string) { const { user: updated } = await $fetch<{ user: AuthUser }>(`${api}/auth/me`, { method: 'PUT', headers: authHeaders(), body: { username: username.trim().toLowerCase() }, }) user.value = updated return updated } async function changePassword(currentPassword: string, newPassword: string) { await $fetch(`${api}/auth/password`, { method: 'PUT', headers: authHeaders(), body: { currentPassword, newPassword }, }) } /* The picture arrives already shrunk to a small data URL (see ProfileModal); "" clears it and leaves the initial in its place. */ async function setAvatar(dataUrl: string) { const { user: updated } = await $fetch<{ user: AuthUser }>(`${api}/auth/avatar`, { method: 'PUT', headers: authHeaders(), body: { avatar: dataUrl }, }) user.value = updated return updated } /* The interface language saved on the account, so the choice follows the person across devices. Switching this device's language is the caller's business (useAppLocale) — this only stores it. */ async function updateLocale(locale: string) { const { user: updated } = await $fetch<{ user: AuthUser }>(`${api}/auth/locale`, { method: 'PUT', headers: authHeaders(), body: { locale }, }) user.value = updated return updated } return { user, loggedIn, isAdmin, authHeaders, signIn, signUp, signOut, restoreSession, updateUsername, updateLocale, changePassword, setAvatar, } }