First commit

This commit is contained in:
2026-09-30 01:49:35 +02:00
commit 6edd03aace
109 changed files with 27020 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
/* 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<string | null>(TOKEN_COOKIE, {
default: () => null,
maxAge: WEEK_IN_SECONDS,
sameSite: 'lax',
})
const user = useState<AuthUser | null>('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<string, string> {
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<Session>(`${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<Session>(`${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,
}
}