feat: add cookie sessions and backend role authorization
This commit is contained in:
+12
-11
@@ -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}`)}
|
||||
|
||||
@@ -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}`)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 }
|
||||
Reference in New Issue
Block a user