358 lines
8.9 KiB
TypeScript
358 lines
8.9 KiB
TypeScript
import type {
|
|
AdminDebugHttpRequest,
|
|
AdminDebugHttpResponse,
|
|
AdminDisableProfileRedeemCodeRequest,
|
|
AdminDisableProfileTaskConfigRequest,
|
|
AdminDatabaseTableListResponse,
|
|
AdminDatabaseTableRowsQuery,
|
|
AdminDatabaseTableRowsResponse,
|
|
AdminLoginResponse,
|
|
AdminMeResponse,
|
|
AdminOverviewResponse,
|
|
AdminTrackingEventListQuery,
|
|
AdminTrackingEventListResponse,
|
|
AdminUpsertProfileInviteCodeRequest,
|
|
AdminUpsertProfileRedeemCodeRequest,
|
|
AdminUpsertProfileTaskConfigRequest,
|
|
ApiErrorEnvelope,
|
|
ApiMeta,
|
|
ApiSuccessEnvelope,
|
|
ProfileInviteCodeAdminListResponse,
|
|
ProfileInviteCodeAdminResponse,
|
|
ProfileRedeemCodeAdminListResponse,
|
|
ProfileRedeemCodeAdminResponse,
|
|
ProfileTaskConfigAdminListResponse,
|
|
ProfileTaskConfigAdminResponse,
|
|
} 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 getAdminDatabaseTables(token: string) {
|
|
return request<AdminDatabaseTableListResponse>('/admin/api/database/tables', {
|
|
token,
|
|
});
|
|
}
|
|
|
|
export function getAdminDatabaseTableRows(
|
|
token: string,
|
|
tableName: string,
|
|
query: AdminDatabaseTableRowsQuery = {},
|
|
) {
|
|
return request<AdminDatabaseTableRowsResponse>(
|
|
`/admin/api/database/tables/${encodeURIComponent(tableName)}/rows${buildDatabaseTableRowsQuery(query)}`,
|
|
{token},
|
|
);
|
|
}
|
|
|
|
export function debugAdminHttp(token: string, payload: AdminDebugHttpRequest) {
|
|
return request<AdminDebugHttpResponse>('/admin/api/debug/http', {
|
|
method: 'POST',
|
|
token,
|
|
body: payload,
|
|
});
|
|
}
|
|
|
|
export function listAdminTrackingEvents(
|
|
token: string,
|
|
query: AdminTrackingEventListQuery = {},
|
|
) {
|
|
return request<AdminTrackingEventListResponse>(
|
|
`/admin/api/tracking/events${buildQueryString(query)}`,
|
|
{token},
|
|
);
|
|
}
|
|
|
|
export function listProfileRedeemCodes(token: string) {
|
|
return request<ProfileRedeemCodeAdminListResponse>(
|
|
'/admin/api/profile/redeem-codes',
|
|
{token},
|
|
);
|
|
}
|
|
|
|
export function upsertProfileRedeemCode(
|
|
token: string,
|
|
payload: AdminUpsertProfileRedeemCodeRequest,
|
|
) {
|
|
return request<ProfileRedeemCodeAdminResponse>(
|
|
'/admin/api/profile/redeem-codes',
|
|
{
|
|
method: 'POST',
|
|
token,
|
|
body: payload,
|
|
},
|
|
);
|
|
}
|
|
|
|
export function listProfileInviteCodes(token: string) {
|
|
return request<ProfileInviteCodeAdminListResponse>(
|
|
'/admin/api/profile/invite-codes',
|
|
{token},
|
|
);
|
|
}
|
|
|
|
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,
|
|
},
|
|
);
|
|
}
|
|
|
|
export function listProfileTaskConfigs(token: string) {
|
|
return request<ProfileTaskConfigAdminListResponse>(
|
|
'/admin/api/profile/tasks',
|
|
{token},
|
|
);
|
|
}
|
|
|
|
export function upsertProfileTaskConfig(
|
|
token: string,
|
|
payload: AdminUpsertProfileTaskConfigRequest,
|
|
) {
|
|
return request<ProfileTaskConfigAdminResponse>('/admin/api/profile/tasks', {
|
|
method: 'POST',
|
|
token,
|
|
body: payload,
|
|
});
|
|
}
|
|
|
|
export function disableProfileTaskConfig(
|
|
token: string,
|
|
payload: AdminDisableProfileTaskConfigRequest,
|
|
) {
|
|
return request<ProfileTaskConfigAdminResponse>(
|
|
'/admin/api/profile/tasks/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 buildQueryString(query: AdminTrackingEventListQuery) {
|
|
const params = new URLSearchParams();
|
|
appendQueryParam(params, 'eventKey', query.eventKey);
|
|
appendQueryParam(params, 'userId', query.userId);
|
|
appendQueryParam(params, 'scopeKind', query.scopeKind);
|
|
appendQueryParam(params, 'scopeId', query.scopeId);
|
|
if (typeof query.limit === 'number' && Number.isFinite(query.limit)) {
|
|
params.set('limit', String(query.limit));
|
|
}
|
|
const queryString = params.toString();
|
|
return queryString ? `?${queryString}` : '';
|
|
}
|
|
|
|
function buildDatabaseTableRowsQuery(query: AdminDatabaseTableRowsQuery) {
|
|
const params = new URLSearchParams();
|
|
appendQueryParam(params, 'search', query.search);
|
|
appendQueryParam(params, 'filters', query.filters);
|
|
if (typeof query.limit === 'number' && Number.isFinite(query.limit)) {
|
|
params.set('limit', String(query.limit));
|
|
}
|
|
const queryString = params.toString();
|
|
return queryString ? `?${queryString}` : '';
|
|
}
|
|
|
|
function appendQueryParam(
|
|
params: URLSearchParams,
|
|
key: string,
|
|
value: string | null | undefined,
|
|
) {
|
|
const trimmed = value?.trim();
|
|
if (trimmed) {
|
|
params.set(key, trimmed);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|