Merge branch 'codex/web-admin'
# Conflicts: # server-rs/crates/api-server/src/admin.rs
This commit is contained in:
238
apps/admin-web/src/api/adminApiClient.ts
Normal file
238
apps/admin-web/src/api/adminApiClient.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import type {
|
||||
AdminDebugHttpRequest,
|
||||
AdminDebugHttpResponse,
|
||||
AdminDisableProfileRedeemCodeRequest,
|
||||
AdminLoginResponse,
|
||||
AdminMeResponse,
|
||||
AdminOverviewResponse,
|
||||
AdminUpsertProfileInviteCodeRequest,
|
||||
AdminUpsertProfileRedeemCodeRequest,
|
||||
ApiErrorEnvelope,
|
||||
ApiMeta,
|
||||
ApiSuccessEnvelope,
|
||||
ProfileInviteCodeAdminResponse,
|
||||
ProfileRedeemCodeAdminResponse,
|
||||
} from './adminApiTypes';
|
||||
|
||||
const API_RESPONSE_ENVELOPE_HEADER = 'x-genarrative-response-envelope';
|
||||
const ADMIN_API_BASE_URL = normalizeBaseUrl(
|
||||
import.meta.env.VITE_ADMIN_API_BASE_URL ?? '',
|
||||
);
|
||||
|
||||
interface AdminRequestOptions {
|
||||
method?: string;
|
||||
token?: string;
|
||||
body?: unknown;
|
||||
headers?: Record<string, string>;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export class AdminApiError extends Error {
|
||||
status: number;
|
||||
code: string;
|
||||
details: Record<string, unknown> | null;
|
||||
meta: ApiMeta | null;
|
||||
responseText: string;
|
||||
|
||||
constructor(params: {
|
||||
message: string;
|
||||
status: number;
|
||||
code?: string;
|
||||
details?: Record<string, unknown> | null;
|
||||
meta?: ApiMeta | null;
|
||||
responseText?: string;
|
||||
}) {
|
||||
super(params.message);
|
||||
this.name = 'AdminApiError';
|
||||
this.status = params.status;
|
||||
this.code = params.code ?? 'ADMIN_API_ERROR';
|
||||
this.details = params.details ?? null;
|
||||
this.meta = params.meta ?? null;
|
||||
this.responseText = params.responseText ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
export function isAdminApiError(error: unknown): error is AdminApiError {
|
||||
return error instanceof AdminApiError;
|
||||
}
|
||||
|
||||
export function formatAdminApiError(error: unknown) {
|
||||
if (isAdminApiError(error)) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
if (error instanceof Error && error.message.trim()) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return '请求失败';
|
||||
}
|
||||
|
||||
export async function request<T>(
|
||||
path: string,
|
||||
options: AdminRequestOptions = {},
|
||||
): Promise<T> {
|
||||
const method = options.method ?? 'GET';
|
||||
const headers: Record<string, string> = {
|
||||
Accept: 'application/json',
|
||||
[API_RESPONSE_ENVELOPE_HEADER]: 'v1',
|
||||
...(options.headers ?? {}),
|
||||
};
|
||||
|
||||
const token = options.token?.trim();
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const init: RequestInit = {
|
||||
method,
|
||||
headers,
|
||||
signal: options.signal,
|
||||
};
|
||||
|
||||
if (typeof options.body !== 'undefined') {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
init.body = JSON.stringify(options.body);
|
||||
}
|
||||
|
||||
const response = await fetch(buildRequestUrl(path), init);
|
||||
const responseText = await response.text();
|
||||
const payload = parseJsonResponse(responseText);
|
||||
|
||||
if (!response.ok) {
|
||||
throw buildAdminApiError(response, payload, responseText);
|
||||
}
|
||||
|
||||
return unwrapSuccessPayload<T>(payload);
|
||||
}
|
||||
|
||||
export function loginAdmin(username: string, password: string) {
|
||||
return request<AdminLoginResponse>('/admin/api/login', {
|
||||
method: 'POST',
|
||||
body: {username, password},
|
||||
});
|
||||
}
|
||||
|
||||
export function getAdminMe(token: string) {
|
||||
return request<AdminMeResponse>('/admin/api/me', {token});
|
||||
}
|
||||
|
||||
export function getAdminOverview(token: string) {
|
||||
return request<AdminOverviewResponse>('/admin/api/overview', {token});
|
||||
}
|
||||
|
||||
export function debugAdminHttp(token: string, payload: AdminDebugHttpRequest) {
|
||||
return request<AdminDebugHttpResponse>('/admin/api/debug/http', {
|
||||
method: 'POST',
|
||||
token,
|
||||
body: payload,
|
||||
});
|
||||
}
|
||||
|
||||
export function upsertProfileRedeemCode(
|
||||
token: string,
|
||||
payload: AdminUpsertProfileRedeemCodeRequest,
|
||||
) {
|
||||
return request<ProfileRedeemCodeAdminResponse>(
|
||||
'/admin/api/profile/redeem-codes',
|
||||
{
|
||||
method: 'POST',
|
||||
token,
|
||||
body: payload,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function upsertProfileInviteCode(
|
||||
token: string,
|
||||
payload: AdminUpsertProfileInviteCodeRequest,
|
||||
) {
|
||||
return request<ProfileInviteCodeAdminResponse>(
|
||||
'/admin/api/profile/invite-codes',
|
||||
{
|
||||
method: 'POST',
|
||||
token,
|
||||
body: payload,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function disableProfileRedeemCode(
|
||||
token: string,
|
||||
payload: AdminDisableProfileRedeemCodeRequest,
|
||||
) {
|
||||
return request<ProfileRedeemCodeAdminResponse>(
|
||||
'/admin/api/profile/redeem-codes/disable',
|
||||
{
|
||||
method: 'POST',
|
||||
token,
|
||||
body: payload,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(value: string) {
|
||||
return value.trim().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function buildRequestUrl(path: string) {
|
||||
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
||||
return `${ADMIN_API_BASE_URL}${normalizedPath}`;
|
||||
}
|
||||
|
||||
function parseJsonResponse(responseText: string): unknown {
|
||||
if (!responseText.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(responseText) as unknown;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function unwrapSuccessPayload<T>(payload: unknown): T {
|
||||
if (isRecord(payload) && 'data' in payload) {
|
||||
return (payload as ApiSuccessEnvelope<T>).data as T;
|
||||
}
|
||||
|
||||
return payload as T;
|
||||
}
|
||||
|
||||
function buildAdminApiError(
|
||||
response: Response,
|
||||
payload: unknown,
|
||||
responseText: string,
|
||||
) {
|
||||
const envelope = isRecord(payload) ? (payload as ApiErrorEnvelope) : null;
|
||||
const errorPayload = envelope?.error;
|
||||
const details = isRecord(errorPayload?.details)
|
||||
? errorPayload.details
|
||||
: null;
|
||||
const detailsMessage =
|
||||
typeof details?.message === 'string' ? details.message.trim() : '';
|
||||
const payloadMessage =
|
||||
typeof errorPayload?.message === 'string' ? errorPayload.message.trim() : '';
|
||||
const topLevelMessage =
|
||||
typeof envelope?.message === 'string' ? envelope.message.trim() : '';
|
||||
const message =
|
||||
detailsMessage ||
|
||||
payloadMessage ||
|
||||
topLevelMessage ||
|
||||
response.statusText ||
|
||||
`HTTP ${response.status}`;
|
||||
|
||||
return new AdminApiError({
|
||||
message,
|
||||
status: response.status,
|
||||
code: errorPayload?.code,
|
||||
details,
|
||||
meta: envelope?.meta ?? null,
|
||||
responseText,
|
||||
});
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
Reference in New Issue
Block a user