feat(M08-D): 补后台登录与可撤销会话
This commit is contained in:
+93
-5
@@ -1,6 +1,9 @@
|
||||
import type {
|
||||
Advertisement,
|
||||
AdvertisementInput,
|
||||
AdminIdentity,
|
||||
AdminSessionPayload,
|
||||
AdminAccess,
|
||||
AuditLog,
|
||||
CleaningSettlement,
|
||||
CleaningSettlementDetail,
|
||||
@@ -66,6 +69,34 @@ export interface ApiSession {
|
||||
token: string;
|
||||
}
|
||||
|
||||
const ACCESS_TOKEN_KEY = 'qipai.admin.token';
|
||||
const REFRESH_TOKEN_KEY = 'qipai.admin.refreshToken';
|
||||
let refreshInFlight: Promise<AdminSessionPayload> | null = null;
|
||||
|
||||
export async function adminPasswordLogin(input: {
|
||||
tenantCode: string; loginName: string; password: string;
|
||||
}) {
|
||||
return publicRequest<AdminSessionPayload>('/auth/login', {
|
||||
method: 'POST', body: JSON.stringify(input)
|
||||
});
|
||||
}
|
||||
|
||||
export function getAdminMe(session: ApiSession) {
|
||||
return request<{ user: AdminIdentity; access: AdminAccess }>(session, '/auth/me');
|
||||
}
|
||||
|
||||
export function logoutAdmin(session: ApiSession) {
|
||||
return request<{ revoked: boolean }>(session, '/auth/logout', { method: 'POST' });
|
||||
}
|
||||
|
||||
export function setAdminCredential(session: ApiSession, userId: string, input: {
|
||||
loginName: string; password: string;
|
||||
}) {
|
||||
return request<{ userId: string; configured: boolean }>(session,
|
||||
`/auth/credentials/${encodeURIComponent(userId)}`,
|
||||
{ method: 'PUT', body: JSON.stringify(input) });
|
||||
}
|
||||
|
||||
export function listManagedStores(session: ApiSession) {
|
||||
return request<ManagedStore[]>(session, '/stores');
|
||||
}
|
||||
@@ -317,14 +348,22 @@ async function request<T>(
|
||||
if (!session.token.trim()) {
|
||||
throw new ApiError('AUTH_TOKEN_REQUIRED', '需要先填入后台访问令牌');
|
||||
}
|
||||
const headers = new Headers(options.headers);
|
||||
headers.set('authorization', `Bearer ${session.token.trim()}`);
|
||||
if (options.body && !headers.has('content-type')) {
|
||||
headers.set('content-type', 'application/json');
|
||||
let response = await authorizedFetch(path, options, session.token.trim());
|
||||
if (response.status === 401 && localStorage.getItem(REFRESH_TOKEN_KEY)) {
|
||||
try {
|
||||
const refreshed = await refreshAccessToken();
|
||||
response = await authorizedFetch(path, options, refreshed.accessToken);
|
||||
} catch {
|
||||
clearStoredSession();
|
||||
window.dispatchEvent(new Event('qipai:admin-unauthorized'));
|
||||
}
|
||||
}
|
||||
const response = await fetch(`${API_BASE}${path}`, { ...options, headers });
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok || payload.code !== 0) {
|
||||
if (response.status === 401) {
|
||||
clearStoredSession();
|
||||
window.dispatchEvent(new Event('qipai:admin-unauthorized'));
|
||||
}
|
||||
throw new ApiError(
|
||||
String(payload.code || `HTTP_${response.status}`),
|
||||
String(payload.message || '请求失败'),
|
||||
@@ -334,6 +373,55 @@ async function request<T>(
|
||||
return payload.data as T;
|
||||
}
|
||||
|
||||
async function publicRequest<T>(path: string, options: RequestInit): Promise<T> {
|
||||
const headers = new Headers(options.headers);
|
||||
headers.set('content-type', 'application/json');
|
||||
headers.set('x-request-id', requestId());
|
||||
const response = await fetch(`${API_BASE}${path}`, { ...options, headers });
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok || payload.code !== 0) throw new ApiError(
|
||||
String(payload.code || `HTTP_${response.status}`),
|
||||
String(payload.message || '请求失败'), String(payload.traceId || '')
|
||||
);
|
||||
return payload.data as T;
|
||||
}
|
||||
|
||||
async function authorizedFetch(path: string, options: RequestInit, token: string) {
|
||||
const headers = new Headers(options.headers);
|
||||
headers.set('authorization', `Bearer ${token}`);
|
||||
headers.set('x-request-id', requestId());
|
||||
if (options.body && !headers.has('content-type')) headers.set('content-type', 'application/json');
|
||||
return fetch(`${API_BASE}${path}`, { ...options, headers });
|
||||
}
|
||||
|
||||
async function refreshAccessToken() {
|
||||
if (!refreshInFlight) {
|
||||
const refreshToken = localStorage.getItem(REFRESH_TOKEN_KEY) || '';
|
||||
refreshInFlight = publicRequest<AdminSessionPayload>('/auth/refresh', {
|
||||
method: 'POST', body: JSON.stringify({ refreshToken })
|
||||
}).then((payload) => {
|
||||
storeAdminSession(payload);
|
||||
window.dispatchEvent(new CustomEvent('qipai:admin-refreshed', { detail: payload }));
|
||||
return payload;
|
||||
}).finally(() => { refreshInFlight = null; });
|
||||
}
|
||||
return refreshInFlight;
|
||||
}
|
||||
|
||||
export function storeAdminSession(payload: AdminSessionPayload) {
|
||||
localStorage.setItem(ACCESS_TOKEN_KEY, payload.accessToken);
|
||||
localStorage.setItem(REFRESH_TOKEN_KEY, payload.refreshToken);
|
||||
}
|
||||
|
||||
export function clearStoredSession() {
|
||||
localStorage.removeItem(ACCESS_TOKEN_KEY);
|
||||
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
||||
}
|
||||
|
||||
function requestId() {
|
||||
return globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
}
|
||||
|
||||
export function listCleaningTasks(
|
||||
session: ApiSession,
|
||||
input: { page: number; pageSize: number; status?: TaskStatus; storeId?: string; cleanerUserId?: string }
|
||||
|
||||
Reference in New Issue
Block a user