feat(02-02): frontend auth store, router guard, Login/Register views
- frontend/src/stores/auth.js: useAuthStore with accessToken in memory only (never browser storage); login() accepts options.backupCode - frontend/src/api/client.js: extended with Bearer token injection, 401 auto-refresh retry, all auth/admin API functions, changePassword - frontend/src/router/index.js: auth routes added (/login, /register, /password-reset, /account, /admin); beforeEach guard redirects unauthenticated users to /login with redirect param - frontend/src/layouts/AuthLayout.vue: centered bare layout for auth pages - frontend/src/views/auth/LoginView.vue: three-step flow (password, TOTP, backup code); "Use a backup code instead" link; UI-SPEC copywriting - frontend/src/views/auth/RegisterView.vue: registration with PasswordStrengthBar; HIBP error display; UI-SPEC copywriting - frontend/src/components/auth/PasswordStrengthBar.vue: 4-segment bar - frontend/src/components/ui/AppSpinner.vue: animate-spin SVG spinner - Stub views: PasswordResetView, NewPasswordView, AccountView, AdminView - .gitignore: exclude frontend/node_modules, dist, package-lock.json npm run build exits 0. All acceptance criteria verified. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+149
-1
@@ -1,10 +1,37 @@
|
||||
/**
|
||||
* API client using native Fetch API.
|
||||
* All requests go to /api (proxied to backend by Vite in dev, or nginx in prod).
|
||||
*
|
||||
* Phase 2 additions (D-11):
|
||||
* - Injects Authorization: Bearer header from useAuthStore().accessToken
|
||||
* - On 401: calls authStore.refresh() and retries once (_retry guard)
|
||||
* - On refresh failure: clears accessToken, throws 'Session expired'
|
||||
*/
|
||||
|
||||
async function request(path, options = {}) {
|
||||
const res = await fetch(path, options)
|
||||
// Lazy import to avoid circular dependency (stores/auth.js → api/client.js → stores/auth.js)
|
||||
const { useAuthStore } = await import('../stores/auth.js')
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const headers = { ...(options.headers || {}) }
|
||||
if (authStore.accessToken) {
|
||||
headers['Authorization'] = `Bearer ${authStore.accessToken}`
|
||||
}
|
||||
|
||||
const res = await fetch(path, { ...options, headers, credentials: 'include' })
|
||||
|
||||
// 401 → attempt refresh → retry once
|
||||
if (res.status === 401 && !options._retry) {
|
||||
try {
|
||||
await authStore.refresh()
|
||||
return request(path, { ...options, _retry: true })
|
||||
} catch {
|
||||
authStore.accessToken = null
|
||||
authStore.user = null
|
||||
throw new Error('Session expired')
|
||||
}
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
let msg = `HTTP ${res.status}`
|
||||
try { msg = (await res.json()).detail || msg } catch {}
|
||||
@@ -103,3 +130,124 @@ export function testProvider(provider) {
|
||||
export function getDefaultPrompt() {
|
||||
return request('/api/settings/default-prompt')
|
||||
}
|
||||
|
||||
// ── Auth ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function login(body) {
|
||||
return request('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
}
|
||||
|
||||
export function register(body) {
|
||||
return request('/api/auth/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
}
|
||||
|
||||
export function refreshToken() {
|
||||
// No body — httpOnly cookie sent automatically via credentials: 'include'
|
||||
return request('/api/auth/refresh', { method: 'POST' })
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return request('/api/auth/logout', { method: 'POST' })
|
||||
}
|
||||
|
||||
export function logoutAll() {
|
||||
return request('/api/auth/logout-all', { method: 'POST' })
|
||||
}
|
||||
|
||||
export function getMe() {
|
||||
return request('/api/auth/me')
|
||||
}
|
||||
|
||||
export function changePassword(body) {
|
||||
return request('/api/auth/change-password', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
}
|
||||
|
||||
// ── TOTP ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function totpSetup() {
|
||||
return request('/api/auth/totp/setup')
|
||||
}
|
||||
|
||||
export function totpEnable(code) {
|
||||
return request('/api/auth/totp/enable', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code }),
|
||||
})
|
||||
}
|
||||
|
||||
export function totpDisable() {
|
||||
return request('/api/auth/totp', { method: 'DELETE' })
|
||||
}
|
||||
|
||||
// ── Password reset ────────────────────────────────────────────────────────────
|
||||
|
||||
export function passwordResetRequest(email) {
|
||||
return request('/api/auth/password-reset', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email }),
|
||||
})
|
||||
}
|
||||
|
||||
export function passwordResetConfirm(token, newPassword) {
|
||||
return request('/api/auth/password-reset/confirm', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token, new_password: newPassword }),
|
||||
})
|
||||
}
|
||||
|
||||
// ── Admin ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function adminListUsers() {
|
||||
return request('/api/admin/users')
|
||||
}
|
||||
|
||||
export function adminCreateUser(body) {
|
||||
return request('/api/admin/users', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
}
|
||||
|
||||
export function adminDeactivateUser(id) {
|
||||
return request(`/api/admin/users/${id}/deactivate`, { method: 'POST' })
|
||||
}
|
||||
|
||||
export function adminReactivateUser(id) {
|
||||
return request(`/api/admin/users/${id}/reactivate`, { method: 'POST' })
|
||||
}
|
||||
|
||||
export function adminResetUserPassword(id) {
|
||||
return request(`/api/admin/users/${id}/reset-password`, { method: 'POST' })
|
||||
}
|
||||
|
||||
export function adminUpdateQuota(id, limitBytes) {
|
||||
return request(`/api/admin/users/${id}/quota`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ limit_bytes: limitBytes }),
|
||||
})
|
||||
}
|
||||
|
||||
export function adminUpdateAiConfig(id, provider, model) {
|
||||
return request(`/api/admin/users/${id}/ai-config`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ provider, model }),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user