C-2: JWT moved from localStorage to httpOnly cookie (eliminates XSS token theft) C-3: WebSocket auth via short-lived single-use tickets (JWT no longer in URLs) H-1: JWT expiry reduced from 7 days to 4 hours H-3: TOTP secrets encrypted at rest with vault EncryptionService (auto-migrates plaintext) H-6: Rate limiting via @nestjs/throttler (60 req/min global, tighten on auth) H-8: Constant-time login — Argon2id verify runs against dummy hash for non-existent users H-9: Password hashing upgraded from bcrypt(10) to Argon2id (auto-upgrades on login) H-10: Credential list API no longer returns encrypted blobs H-16: Admin pages use Nuxt route middleware instead of client-side guard Plus: auth bootstrap plugin, cookie-parser middleware, all frontend Authorization headers removed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
68 lines
1.8 KiB
TypeScript
68 lines
1.8 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
|
|
interface User {
|
|
id: number
|
|
email: string
|
|
displayName: string | null
|
|
role: string
|
|
totpEnabled?: boolean
|
|
}
|
|
|
|
interface LoginResponse {
|
|
user?: User
|
|
requires_totp?: boolean
|
|
}
|
|
|
|
export const useAuthStore = defineStore('auth', {
|
|
state: () => ({
|
|
// C-2: No more token in state or localStorage — JWT lives in httpOnly cookie
|
|
user: null as User | null,
|
|
}),
|
|
getters: {
|
|
isAuthenticated: (state) => !!state.user,
|
|
isAdmin: (state) => state.user?.role === 'admin',
|
|
},
|
|
actions: {
|
|
async login(email: string, password: string, totpCode?: string): Promise<LoginResponse> {
|
|
const body: Record<string, string> = { email, password }
|
|
if (totpCode) body.totpCode = totpCode
|
|
|
|
const res = await $fetch<LoginResponse>('/api/auth/login', {
|
|
method: 'POST',
|
|
body,
|
|
})
|
|
|
|
if (res.requires_totp) {
|
|
return { requires_totp: true }
|
|
}
|
|
|
|
// Server set httpOnly cookie — we just store the user info
|
|
this.user = res.user!
|
|
return res
|
|
},
|
|
async logout() {
|
|
try {
|
|
await $fetch('/api/auth/logout', { method: 'POST' })
|
|
} catch { /* server may be unreachable */ }
|
|
this.user = null
|
|
navigateTo('/login')
|
|
},
|
|
async fetchProfile() {
|
|
try {
|
|
// Cookie sent automatically on same-origin request
|
|
this.user = await $fetch('/api/auth/profile')
|
|
} catch {
|
|
this.user = null
|
|
}
|
|
},
|
|
/**
|
|
* Get a short-lived WS ticket for WebSocket connections (C-3).
|
|
* The ticket replaces putting the JWT in the URL query string.
|
|
*/
|
|
async getWsTicket(): Promise<string> {
|
|
const res = await $fetch<{ ticket: string }>('/api/auth/ws-ticket', { method: 'POST' })
|
|
return res.ticket
|
|
},
|
|
},
|
|
})
|