refactor(auth): split IAM module and add access/refresh session flow

This commit is contained in:
2026-03-01 21:26:37 +08:00
parent 6a2d2c9724
commit 57c27e9102
13 changed files with 1377 additions and 345 deletions

View File

@@ -1,10 +1,10 @@
import type { Credentials, Task } from '../types'
import { session } from '../stores/session'
import { clearSession, session, setSessionToken } from '../stores/session'
const API_BASE = 'http://localhost:8080/api/v1'
function headers() {
const h: Record<string, string> = { 'Content-Type': 'application/json' }
function headers(extra?: Record<string, string>) {
const h: Record<string, string> = { 'Content-Type': 'application/json', ...(extra ?? {}) }
if (session.token) h.Authorization = `Bearer ${session.token}`
return h
}
@@ -18,59 +18,100 @@ async function handle<T>(response: Response): Promise<T> {
return (await response.json()) as T
}
export async function register(credentials: Credentials) {
const response = await fetch(`${API_BASE}/auth/register`, {
async function refreshAccessToken(): Promise<boolean> {
try {
const response = await fetch(`${API_BASE}/auth/refresh`, {
method: 'POST',
credentials: 'include',
headers: headers(),
})
if (!response.ok) return false
const data = (await response.json()) as { access_token: string }
if (!data?.access_token) return false
setSessionToken(data.access_token)
return true
} catch {
return false
}
}
async function request<T>(path: string, init: RequestInit = {}, allowRefresh = true): Promise<T> {
const merged: RequestInit = {
credentials: 'include',
...init,
headers: headers((init.headers as Record<string, string> | undefined) ?? {}),
}
const response = await fetch(`${API_BASE}${path}`, merged)
if (response.status === 401 && allowRefresh && !path.startsWith('/auth/')) {
const refreshed = await refreshAccessToken()
if (refreshed) {
return request<T>(path, init, false)
}
clearSession()
}
return handle<T>(response)
}
export async function register(credentials: Credentials, autoLogin = true) {
return request<{ email: string; access_token?: string; expires_in?: number; session_id?: string }>('/auth/register', {
method: 'POST',
headers: headers(),
body: JSON.stringify(credentials),
body: JSON.stringify({ ...credentials, auto_login: autoLogin }),
})
return handle<{ id: number; email: string }>(response)
}
export async function login(credentials: Credentials) {
const response = await fetch(`${API_BASE}/auth/login`, {
return request<{ access_token: string; expires_in: number; session_id: string }>('/auth/login', {
method: 'POST',
headers: headers(),
body: JSON.stringify(credentials),
})
return handle<{ token: string }>(response)
}
export async function refresh() {
return request<{ access_token: string; expires_in: number; session_id: string }>('/auth/refresh', {
method: 'POST',
}, false)
}
export async function logout() {
const response = await fetch(`${API_BASE}/auth/logout`, {
return request<void>('/auth/logout', {
method: 'POST',
headers: headers(),
})
return handle<void>(response)
}, false)
}
export async function logoutAll() {
return request<void>('/auth/logout-all', {
method: 'POST',
}, false)
}
export async function listSessions() {
return request<Array<{ id: string; device_info: string; ip: string; user_agent: string; created_at: string; expires_at: string; revoked_at?: string }>>('/auth/sessions')
}
export async function revokeSession(id: string) {
return request<void>(`/auth/sessions/${id}`, { method: 'DELETE' }, false)
}
export async function listTasks() {
const response = await fetch(`${API_BASE}/tasks`, { headers: headers() })
return handle<Task[]>(response)
return request<Task[]>('/tasks')
}
export async function createTask(payload: Partial<Task>) {
const response = await fetch(`${API_BASE}/tasks`, {
return request<Task>('/tasks', {
method: 'POST',
headers: headers(),
body: JSON.stringify(payload),
})
return handle<Task>(response)
}
export async function updateTask(id: number, payload: Partial<Task>) {
const response = await fetch(`${API_BASE}/tasks/${id}`, {
return request<Task>(`/tasks/${id}`, {
method: 'PUT',
headers: headers(),
body: JSON.stringify(payload),
})
return handle<Task>(response)
}
export async function deleteTask(id: number) {
const response = await fetch(`${API_BASE}/tasks/${id}`, {
return request<void>(`/tasks/${id}`, {
method: 'DELETE',
headers: headers(),
})
return handle<void>(response)
}

View File

@@ -1,6 +1,6 @@
import { reactive } from 'vue'
const tokenKey = 'supertodo_token'
const tokenKey = 'supertodo_access_token'
export const session = reactive({
token: localStorage.getItem(tokenKey) ?? '',
@@ -14,6 +14,11 @@ export function setSession(token: string, email: string) {
localStorage.setItem('supertodo_email', email)
}
export function setSessionToken(token: string) {
session.token = token
localStorage.setItem(tokenKey, token)
}
export function clearSession() {
session.token = ''
session.email = ''

View File

@@ -19,7 +19,7 @@ async function submit() {
await api.register(form)
}
const data = await api.login(form)
setSession(data.token, form.email)
setSession(data.access_token, form.email)
router.push('/todos')
} catch (e) {
error.value = t('auth_failed')