feat: add cookie sessions and backend role authorization

This commit is contained in:
2026-10-16 07:51:59 +08:00
parent 411eadb431
commit 3f25ffab83
28 changed files with 1186 additions and 31 deletions
+12 -11
View File
@@ -1,17 +1,18 @@
export async function getHealth(){const r=await fetch('/api/health');if(!r.ok)throw new Error('health failed');return r.json()}
export async function askQuestion(question:string){const r=await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({question})});if(!r.ok)throw new Error('chat failed');return r.json() as Promise<{answer:string;status:string;sources:{title:string;source_url?:string|null}[]}>}
export async function getKnowledge(){const r=await fetch('/api/knowledge');if(!r.ok)throw new Error('knowledge failed');return r.json() as Promise<any[]>}
export async function createKnowledge(payload:{title:string;category:string;content:string;source_url?:string}){const r=await fetch('/api/knowledge',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});if(!r.ok)throw new Error('保存失败,请检查网址格式及后端状态');return r.json()}
import { apiRequest } from './services/session'
export const getHealth = () => apiRequest<{status:string}>('/api/health')
export const askQuestion = (question:string) => apiRequest<{answer:string;status:string;sources:{title:string;source_url?:string|null}[]}>('/api/chat',{method:'POST',body:JSON.stringify({question})})
export const getKnowledge = () => apiRequest<{id:number;title:string;category:string;content:string;source_url:string|null;verification_status:string;version:number}[]>('/api/knowledge')
export const createKnowledge = (payload:{title:string;category:string;content:string;source_url?:string}) => apiRequest('/api/knowledge',{method:'POST',body:JSON.stringify(payload)},true)
export type VerificationStatus='pending'|'verified'|'rejected'
export type SourceItem={id:number;version:number;name:string;source_url:string|null;notes:string;created_at:string}
async function catalogRequest<T>(path:string,init?:RequestInit):Promise<T>{const r=await fetch(path,init);if(!r.ok){const detail=await r.json().catch(()=>null);throw new Error(detail?.detail||'请求失败,请检查后端服务和填写内容。')}return r.json() as Promise<T>}
export function getSources(q=''){return catalogRequest<{items:SourceItem[];total:number}>(`/api/sources?q=${encodeURIComponent(q)}`)}
export function createSource(payload:{name:string;source_url?:string;notes:string}){return catalogRequest<SourceItem>('/api/sources',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)})}
export function reviewKnowledge(id:number,payload:{expected_version:number;verification_status:VerificationStatus;reviewer:string;verification_notes:string}){return catalogRequest(`/api/knowledge/${id}/review`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)})}
export function getSources(q=''){return apiRequest<{items:SourceItem[];total:number}>(`/api/sources?q=${encodeURIComponent(q)}`)}
export function createSource(payload:{name:string;source_url?:string;notes:string}){return apiRequest<SourceItem>('/api/sources',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)},true)}
export function reviewKnowledge(id:number,payload:{expected_version:number;verification_status:VerificationStatus;reviewer:string;verification_notes:string}){return apiRequest(`/api/knowledge/${id}/review`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)},true)}
export type RetrievalHit={rank:number;knowledge_id:number;knowledge_version:number;document_id:number|null;document_version:number|null;source_id:number|null;source_version:number|null;source_name:string|null;source_url:string;title:string;category:string;snippet:string;snippet_start:number;snippet_end:number;score:number;matched_terms:string[]}
export type RetrievalResult={run_id:string;query:string;mode:'keyword';status:'ok'|'no_evidence';top_k:number;category:string|null;query_terms:string[];corpus_size:number;corpus_fingerprint:string;total_hits:number;results:RetrievalHit[];elapsed_ms:number;created_at:string;config:{tokenizer:string;scorer:string;k1:number;b:number;title_weight:number;stopwords_version:string}}
export function retrieveKnowledge(payload:{query:string;mode?:'keyword';top_k?:number;category?:string}){return catalogRequest<RetrievalResult>('/api/retrieval',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)})}
export function getRetrievalRun(runId:string){return catalogRequest<RetrievalResult>(`/api/retrieval/runs/${encodeURIComponent(runId)}`)}
export function getRetrievalRuns(page=1,pageSize=20){return catalogRequest<{items:RetrievalResult[];total:number;page:number;page_size:number}>(`/api/retrieval/runs?page=${page}&page_size=${pageSize}`)}
export function retrieveKnowledge(payload:{query:string;mode?:'keyword';top_k?:number;category?:string}){return apiRequest<RetrievalResult>('/api/retrieval',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)})}
export function getRetrievalRun(runId:string){return apiRequest<RetrievalResult>(`/api/retrieval/runs/${encodeURIComponent(runId)}`)}
export function getRetrievalRuns(page=1,pageSize=20){return apiRequest<{items:RetrievalResult[];total:number;page:number;page_size:number}>(`/api/retrieval/runs?page=${page}&page_size=${pageSize}`)}
+20
View File
@@ -0,0 +1,20 @@
import type { ChangePasswordPayload, CurrentUser, LoginPayload, RegisterPayload, SessionUser, UpdateProfilePayload } from '../types/auth'
import { apiRequest, ApiError, clearSessionToken } from './session'
export { ApiError as AuthApiUnavailableError }
export const register = (payload: RegisterPayload) => apiRequest<CurrentUser>('/api/auth/register', { method: 'POST', body: JSON.stringify(payload) })
export const login = async (payload: LoginPayload) => {
clearSessionToken()
return apiRequest<SessionUser>('/api/auth/login', { method: 'POST', body: JSON.stringify(payload) })
}
export const getCurrentUser = () => apiRequest<SessionUser>('/api/auth/me')
export const updateMyProfile = (payload: UpdateProfilePayload) => apiRequest<CurrentUser>('/api/users/me', { method: 'PATCH', body: JSON.stringify(payload) }, true)
export const logout = async () => {
await apiRequest<void>('/api/auth/logout', { method: 'POST' }, true)
clearSessionToken()
}
export const changePassword = async (payload: ChangePasswordPayload) => {
await apiRequest<void>('/api/auth/change-password', { method: 'POST', body: JSON.stringify(payload) }, true)
clearSessionToken()
}
export const getUsers = (limit = 100, offset = 0) => apiRequest<CurrentUser[]>(`/api/admin/users?limit=${limit}&offset=${offset}`)
+28
View File
@@ -0,0 +1,28 @@
let csrfToken: string | null = null
export class ApiError extends Error {
constructor(public readonly status: number, message: string) { super(message) }
}
export function clearSessionToken() { csrfToken = null }
export async function apiRequest<T>(path: string, init: RequestInit = {}, protect = false): Promise<T> {
if (protect && !csrfToken) await apiRequest('/api/auth/me')
const headers = new Headers(init.headers)
headers.set('Accept', 'application/json')
if (init.body) headers.set('Content-Type', 'application/json')
if (protect && csrfToken) headers.set('X-CSRF-Token', csrfToken)
const response = await fetch(path, { ...init, credentials: 'include', headers })
if (!response.ok) {
if (response.status === 401) clearSessionToken()
const body: unknown = await response.json().catch(() => null)
const detail = body && typeof body === 'object' && 'detail' in body ? body.detail : null
throw new ApiError(response.status, typeof detail === 'string' ? detail : `请求失败(${response.status}),请检查登录状态或填写内容。`)
}
if (response.status === 204) return undefined as T
const data = await response.json()
if (path === '/api/auth/login' || path === '/api/auth/me') {
csrfToken = typeof data.csrf_token === 'string' ? data.csrf_token : null
}
return data as T
}
+14
View File
@@ -0,0 +1,14 @@
export type UserRole = 'user' | 'admin'
export type CurrentUser = {
id: number
username: string
nickname: string | null
email: string | null
role: UserRole
created_at: string
}
export type SessionUser = CurrentUser & { csrf_token: string }
export type RegisterPayload = { username: string; password: string; email?: string; nickname?: string }
export type LoginPayload = { username: string; password: string }
export type UpdateProfilePayload = { nickname?: string | null; email?: string | null }
export type ChangePasswordPayload = { current_password: string; new_password: string }