/** * useAuthStore — Pinia auth store for DocuVault. * * Security invariants (CLAUDE.md): * - accessToken lives ONLY in ref() memory — never written to browser storage * - Refresh token is an httpOnly cookie managed by the backend * - On 401: api/client.js calls authStore.refresh() (uses httpOnly cookie) * * State: * accessToken ref(null) — JWT access token, memory only * user ref(null) — { id, handle, email, role, totp_enabled } * loading ref(false) * error ref(null) */ import { defineStore } from 'pinia' import { ref } from 'vue' import * as api from '../api/client.js' export const useAuthStore = defineStore('auth', () => { // State — accessToken in memory only (CLAUDE.md rule: no browser storage writes) const accessToken = ref(null) const user = ref(null) const loading = ref(false) const error = ref(null) /** * Register a new account. * Does NOT auto-login — caller should redirect to /login after success. */ async function register(handle, email, password) { loading.value = true error.value = null try { const data = await api.register({ handle, email, password }) return data } catch (e) { error.value = e.message throw e } finally { loading.value = false } } /** * Login with email + password, optionally with TOTP or backup code. * * options: * { totpCode?: string, backupCode?: string } * * Return values: * { requires_totp: true } — TOTP challenge (no tokens issued) * { requires_password_change: true, user_id: string } — must change pw first * undefined — full success (accessToken + user set) */ async function login(email, password, options = {}) { loading.value = true error.value = null try { const data = await api.login({ email, password, totp_code: options.totpCode ?? null, backup_code: options.backupCode ?? null, }) if (data.requires_totp) { return { requires_totp: true } } if (data.requires_password_change) { return { requires_password_change: true, user_id: data.user_id } } // Full login success accessToken.value = data.access_token user.value = data.user } catch (e) { error.value = e.message throw e } finally { loading.value = false } } /** * Refresh the access token using the httpOnly refresh cookie. * Called automatically by api/client.js on 401. * Throws on failure (session expired — caller should redirect to /login). */ async function refresh() { const data = await api.refreshToken() accessToken.value = data.access_token user.value = data.user } /** * Logout: revoke the current refresh token and clear local state. * Always clears accessToken + user, even if the API call fails. */ async function logout() { try { await api.logout() } catch { // Ignore errors — clear state regardless } finally { accessToken.value = null user.value = null } } /** * Sign out of all devices: revoke ALL refresh tokens for the current user. */ async function logoutAll() { try { await api.logoutAll() } catch { // Ignore errors } finally { accessToken.value = null user.value = null } } return { accessToken, user, loading, error, register, login, logout, logoutAll, refresh, } })