feat(auth): P0 登录与会话(迁移 v16、argon2id、HttpOnly Cookie)
- 迁移机制:schema::migrate 支持 15→16,新增 admin/session/setting 表 - 接口:session/setup/login/logout/password;SameSite=Strict + Origin 校验 + 失败限流 - 会话只存 token 的 sha256;改密清空全部会话 - 前端:/login 独立壳(首次=设置密码),页头登录/退出切换 - 测试:37 passed(迁移 + 登录/会话 4 例) - 文档:README 登录说明、docs/ADMIN-PLAN.md 方案与 P0 记录
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 登录/会话的接口封装。
|
||||
*
|
||||
* 会话是 HttpOnly Cookie,前端拿不到 token,只能问 `/api/auth/session`。
|
||||
*/
|
||||
export type SessionState = {
|
||||
authenticated: boolean
|
||||
has_admin: boolean
|
||||
username?: string | null
|
||||
}
|
||||
|
||||
type Result = { ok: true } | { ok: false; status: number; error: string }
|
||||
|
||||
async function request(
|
||||
method: 'POST' | 'PATCH',
|
||||
url: string,
|
||||
body: unknown,
|
||||
): Promise<Result> {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
credentials: 'same-origin',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (response.ok) return { ok: true }
|
||||
|
||||
const payload = (await response.json().catch(() => null)) as { error?: string } | null
|
||||
return {
|
||||
ok: false,
|
||||
status: response.status,
|
||||
error: payload?.error ?? `请求失败(${response.status})`,
|
||||
}
|
||||
} catch (error) {
|
||||
return { ok: false, status: 0, error: error instanceof Error ? error.message : '网络错误' }
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchSession(): Promise<SessionState> {
|
||||
try {
|
||||
const response = await fetch('/api/auth/session', { credentials: 'same-origin' })
|
||||
if (!response.ok) return { authenticated: false, has_admin: false }
|
||||
return (await response.json()) as SessionState
|
||||
} catch {
|
||||
return { authenticated: false, has_admin: false }
|
||||
}
|
||||
}
|
||||
|
||||
export function login(password: string): Promise<Result> {
|
||||
return request('POST', '/api/auth/login', { password })
|
||||
}
|
||||
|
||||
export function setup(password: string, username = 'admin'): Promise<Result> {
|
||||
return request('POST', '/api/auth/setup', { password, username })
|
||||
}
|
||||
|
||||
export function logout(): Promise<Result> {
|
||||
return request('POST', '/api/auth/logout', {})
|
||||
}
|
||||
|
||||
export function changePassword(current: string, next: string): Promise<Result> {
|
||||
return request('PATCH', '/api/auth/password', { current, next })
|
||||
}
|
||||
Reference in New Issue
Block a user