1286 lines
62 KiB
TypeScript
1286 lines
62 KiB
TypeScript
import { clearSession, dispatchSessionEvent, getSessionTenantId, hasRecentUserActivity, readSession, requestReauthentication, type LoginSession } from './session';
|
||
|
||
type RequestOptions = RequestInit & {
|
||
tenantId?: string;
|
||
reauthenticationAttempted?: boolean;
|
||
};
|
||
|
||
type ApiErrorBody = { message?: string | string[]; error?: string; code?: string };
|
||
|
||
async function readErrorBody(response: Response): Promise<ApiErrorBody> {
|
||
const text = await response.text();
|
||
if (!text) return {};
|
||
try {
|
||
return JSON.parse(text) as ApiErrorBody;
|
||
} catch {
|
||
return { message: text };
|
||
}
|
||
}
|
||
|
||
export const DEFAULT_CLIENT_TENANT_ID = 'tenant-a';
|
||
|
||
async function readErrorMessage(response: Response) {
|
||
const fallback = `请求失败(${response.status})`;
|
||
const text = await response.text();
|
||
if (!text) return fallback;
|
||
|
||
try {
|
||
const parsed = JSON.parse(text) as { message?: string | string[]; error?: string };
|
||
if (Array.isArray(parsed.message)) return parsed.message.join(';');
|
||
if (parsed.message) return parsed.message;
|
||
if (parsed.error) return parsed.error;
|
||
} catch {
|
||
return text;
|
||
}
|
||
|
||
return text;
|
||
}
|
||
|
||
async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
||
const headers = new Headers(options.headers);
|
||
headers.set('Content-Type', 'application/json');
|
||
const session = readSession();
|
||
if (session && hasRecentUserActivity()) headers.set('x-session-activity', 'user');
|
||
const tenantId = options.tenantId ?? (path.startsWith('/client') ? getSessionTenantId() : undefined);
|
||
if (tenantId) {
|
||
headers.set('x-tenant-id', tenantId);
|
||
}
|
||
const response = await fetch(`/api${path}`, { ...options, headers, credentials: 'same-origin' });
|
||
if (response.status === 401 && session) {
|
||
const body = await readErrorBody(response.clone());
|
||
if (body.code === 'SESSION_LOCKED') {
|
||
dispatchSessionEvent('locked', { message: body.message });
|
||
throw new Error(typeof body.message === 'string' ? body.message : '由于长时间未操作,会话已安全锁定');
|
||
}
|
||
clearSession();
|
||
dispatchSessionEvent('logout', { code: body.code, message: body.message });
|
||
window.location.assign(session.portal === 'admin' ? '/admin/login' : '/client/login');
|
||
throw new Error('登录会话已失效,请重新登录');
|
||
}
|
||
if (response.status === 403 && session && !options.reauthenticationAttempted) {
|
||
const body = await readErrorBody(response.clone());
|
||
if (body.code === 'RECENT_AUTHENTICATION_REQUIRED') {
|
||
await requestReauthentication();
|
||
return request<T>(path, { ...options, reauthenticationAttempted: true });
|
||
}
|
||
}
|
||
if (!response.ok) {
|
||
throw new Error(await readErrorMessage(response));
|
||
}
|
||
return response.json() as Promise<T>;
|
||
}
|
||
|
||
async function requestBlob(path: string, options: RequestOptions = {}): Promise<Blob> {
|
||
const headers = new Headers(options.headers);
|
||
const session = readSession();
|
||
if (session && hasRecentUserActivity()) headers.set('x-session-activity', 'user');
|
||
const tenantId = options.tenantId ?? (path.startsWith('/client') ? getSessionTenantId() : undefined);
|
||
if (tenantId) {
|
||
headers.set('x-tenant-id', tenantId);
|
||
}
|
||
const response = await fetch(`/api${path}`, { ...options, headers, credentials: 'same-origin' });
|
||
if (response.status === 401 && session) {
|
||
const body = await readErrorBody(response.clone());
|
||
if (body.code === 'SESSION_LOCKED') {
|
||
dispatchSessionEvent('locked', { message: body.message });
|
||
throw new Error(typeof body.message === 'string' ? body.message : '由于长时间未操作,会话已安全锁定');
|
||
}
|
||
clearSession();
|
||
dispatchSessionEvent('logout', { code: body.code, message: body.message });
|
||
window.location.assign(session.portal === 'admin' ? '/admin/login' : '/client/login');
|
||
throw new Error('登录会话已失效,请重新登录');
|
||
}
|
||
if (response.status === 403 && session && !options.reauthenticationAttempted) {
|
||
const body = await readErrorBody(response.clone());
|
||
if (body.code === 'RECENT_AUTHENTICATION_REQUIRED') {
|
||
await requestReauthentication();
|
||
return requestBlob(path, { ...options, reauthenticationAttempted: true });
|
||
}
|
||
}
|
||
if (!response.ok) {
|
||
throw new Error(await readErrorMessage(response));
|
||
}
|
||
return response.blob();
|
||
}
|
||
|
||
export type AdminChannel = {
|
||
id: string;
|
||
code: string;
|
||
name: string;
|
||
carrier?: string | null;
|
||
sendRegion?: string | null;
|
||
gatewayHost: string;
|
||
gatewayPort: number;
|
||
enterpriseCode?: string | null;
|
||
account: string;
|
||
srcId: string;
|
||
cmppVersion?: '2.0' | '3.0' | string | null;
|
||
rateLimitPerSecond: number;
|
||
unitPrice: number;
|
||
status: string;
|
||
config?: { desiredConnections?: number; windowSize?: number; extensionDigits?: number; [key: string]: unknown } | null;
|
||
connectionStates?: CmppConnectionState[];
|
||
};
|
||
|
||
export type ChannelConnectionLogResponse = {
|
||
channelId: string;
|
||
connectionStates: CmppConnectionState[];
|
||
logs: Array<{
|
||
id: string;
|
||
time: string;
|
||
event: string;
|
||
action: string;
|
||
resourceId?: string;
|
||
detail?: unknown;
|
||
}>;
|
||
};
|
||
|
||
export type ChannelTestResponse = {
|
||
channelId: string;
|
||
status: string;
|
||
testNo: string;
|
||
submitted: number;
|
||
messages: Array<{
|
||
phoneNumber: string;
|
||
messageRecordId: string;
|
||
submitId: string;
|
||
streamMessageId?: string;
|
||
}>;
|
||
queuedAt: string;
|
||
};
|
||
|
||
export type EnterpriseCertification = {
|
||
id: string;
|
||
tenantId: string;
|
||
companyName: string;
|
||
licenseNo?: string | null;
|
||
contactName?: string | null;
|
||
contactPhone?: string | null;
|
||
materials?: Record<string, unknown> | null;
|
||
status: string;
|
||
rejectReason?: string | null;
|
||
submittedAt: string;
|
||
reviewedAt?: string | null;
|
||
tenant?: { id: string; name: string; code: string };
|
||
};
|
||
|
||
export type SmsTemplateAudit = {
|
||
id: string;
|
||
tenantId: string;
|
||
applicationId: string;
|
||
name: string;
|
||
content: string;
|
||
category?: string | null;
|
||
auditStatus: string;
|
||
rejectReason?: string | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
application?: { name: string };
|
||
tenant?: { name: string };
|
||
};
|
||
|
||
export type TenantOption = {
|
||
id: string;
|
||
name: string;
|
||
code: string;
|
||
status: string;
|
||
enterpriseProfile?: {
|
||
creditCode?: string;
|
||
province?: string;
|
||
city?: string;
|
||
address?: string;
|
||
contactName?: string;
|
||
contactIdCard?: string;
|
||
contactPhone?: string;
|
||
contactEmail?: string;
|
||
photoFileObjectId?: string;
|
||
} | null;
|
||
};
|
||
|
||
export type TenantManagementRow = TenantOption & {
|
||
account?: TenantAccount | null;
|
||
todaySpendCents: number;
|
||
todayRefundCents: number;
|
||
};
|
||
|
||
export type CaptchaResponse = {
|
||
captchaId: string;
|
||
challenge: string;
|
||
expiresInSeconds: number;
|
||
};
|
||
|
||
export type ManagedUser = {
|
||
id: string;
|
||
tenantId?: string | null;
|
||
username: string;
|
||
email?: string | null;
|
||
phone?: string | null;
|
||
displayName: string;
|
||
status: string;
|
||
failedLoginCount: number;
|
||
lockedUntil?: string | null;
|
||
lastLoginAt?: string | null;
|
||
createdAt: string;
|
||
tenant?: TenantOption | null;
|
||
roles: Array<{ role: { code: string; name: string; scope: string } }>;
|
||
};
|
||
|
||
export type UserPayload = {
|
||
tenantId?: string | null;
|
||
username?: string;
|
||
email?: string | null;
|
||
phone?: string | null;
|
||
displayName: string;
|
||
password?: string;
|
||
status?: string;
|
||
roleCode: 'platform_admin' | 'enterprise_admin';
|
||
operatorId?: string;
|
||
};
|
||
|
||
export type DashboardResponse = {
|
||
taskCount: number;
|
||
messageStatus: Array<{ status: string; _count: { _all: number }; _sum: { amountCents?: number | null; billingUnits?: number | null } }>;
|
||
today: { sent: number; delivered: number; failed: number; unknown: number; successRate: number; spendCents: number; billingUnits: number };
|
||
uplinkCount: number;
|
||
billing: { _count: { _all: number }; _sum: { amountCents?: number | null; billingUnits?: number | null } };
|
||
transactions: { _count: { _all: number }; _sum: { amountCents?: number | null } };
|
||
gatewayConnections: Array<{ status: string; _count: { _all: number }; _sum: { currentConnections?: number | null; desiredConnections?: number | null } }>;
|
||
pendingAuditCount: number;
|
||
pendingAudits: { enterpriseCertifications: number; smsAudits: number; templates: number; signatures: number; drainageInfos: number; total: number };
|
||
downstreamDeliverySummary?: {
|
||
pending: number;
|
||
failed: number;
|
||
delivered: number;
|
||
stalledPending: number;
|
||
recentFailed: number;
|
||
alertCount: number;
|
||
};
|
||
accounts: Array<{ id: string; tenantId: string; balanceCents: number; status: string; tenant?: TenantOption }>;
|
||
recentTasks: Array<Record<string, unknown>>;
|
||
recentRecharges: Array<RechargeOrder>;
|
||
};
|
||
|
||
export type RechargeOrder = {
|
||
id: string;
|
||
tenantId: string;
|
||
orderNo: string;
|
||
amountCents: number;
|
||
status: string;
|
||
payMethod?: string | null;
|
||
paidAt?: string | null;
|
||
operatorId?: string | null;
|
||
remark?: string | null;
|
||
balanceAfterCents?: number | null;
|
||
createdAt: string;
|
||
tenant?: TenantOption;
|
||
};
|
||
|
||
export type ClientSmsApplication = {
|
||
id: string;
|
||
tenantId: string;
|
||
name: string;
|
||
scene?: string | null;
|
||
customerUnitPrice?: number | null;
|
||
queuePriority?: 'normal' | 'priority' | string | null;
|
||
status: string;
|
||
dailyLimit?: number | null;
|
||
createdAt?: string;
|
||
updatedAt?: string;
|
||
sentToday?: number;
|
||
deliveryRate?: number;
|
||
cmppStatus?: 'connected' | 'degraded' | 'disconnected' | 'inactive';
|
||
cmppConnections?: CmppDownstreamConnection[];
|
||
};
|
||
|
||
export type ClientSmsSignature = {
|
||
id: string;
|
||
tenantId: string;
|
||
applicationId?: string | null;
|
||
name: string;
|
||
purpose?: string | null;
|
||
drainageInfo?: Record<string, unknown> | null;
|
||
auditStatus: string;
|
||
rejectReason?: string | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
materials?: Array<Record<string, unknown>>;
|
||
tenant?: TenantOption;
|
||
application?: ClientSmsApplication | null;
|
||
reportStatus?: string;
|
||
reportTasks?: Array<ReportTask & { channel?: AdminChannel }>;
|
||
reportTargets?: Array<{ channel: AdminChannel; channelId: string; status: string; taskId?: string }>;
|
||
carrierReportSummary?: Record<'mobile' | 'unicom' | 'telecom', { status: string; approved: number; total: number }>;
|
||
drainageReportTargets?: Record<string, Array<{ channel: AdminChannel; channelId: string; status: string; taskId?: string }>>;
|
||
drainageCarrierReportSummary?: Record<string, Record<'mobile' | 'unicom' | 'telecom', { status: string; approved: number; total: number }>>;
|
||
};
|
||
|
||
export type SmsDrainageInfo = {
|
||
id: string;
|
||
tenantId: string;
|
||
signatureId: string;
|
||
applicationId?: string | null;
|
||
siteName: string;
|
||
url: string;
|
||
remark?: string | null;
|
||
reportValues?: Record<string, unknown> | null;
|
||
auditStatus: string;
|
||
rejectReason?: string | null;
|
||
submittedAt: string;
|
||
reviewedAt?: string | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
tenant?: TenantOption;
|
||
signature?: ClientSmsSignature;
|
||
application?: ClientSmsApplication | null;
|
||
reportTasks?: ReportTask[];
|
||
};
|
||
|
||
export type ClientSmsTemplate = {
|
||
id: string;
|
||
tenantId: string;
|
||
applicationId: string;
|
||
signatureId?: string | null;
|
||
name: string;
|
||
content: string;
|
||
category?: string | null;
|
||
auditStatus: string;
|
||
rejectReason?: string | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
variables?: Array<{ name: string; example?: string | null; required?: boolean }>;
|
||
application?: { id: string; name: string };
|
||
signature?: { id: string; name: string; purpose?: string | null; drainageInfo?: Record<string, unknown> | null };
|
||
tenant?: TenantOption;
|
||
};
|
||
|
||
export type SmsBatchTask = {
|
||
id: string;
|
||
tenantId: string;
|
||
taskNo: string;
|
||
sourceType?: string;
|
||
contentHash?: string | null;
|
||
windowStartedAt?: string | null;
|
||
windowEndsAt?: string | null;
|
||
applicationId?: string | null;
|
||
templateId?: string | null;
|
||
content: string;
|
||
category?: string | null;
|
||
phoneTotal: number;
|
||
status: string;
|
||
auditStatus?: string | null;
|
||
reviewReason?: string | null;
|
||
rejectReason?: string | null;
|
||
progressTotal: number;
|
||
progressSent?: number;
|
||
progressDelivered?: number;
|
||
progressFailed?: number;
|
||
submittedTotal?: number;
|
||
successTotal?: number;
|
||
failedTotal?: number;
|
||
unknownTotal?: number;
|
||
timeoutTotal?: number;
|
||
scheduledAt?: string | null;
|
||
canceledAt?: string | null;
|
||
createdAt: string;
|
||
tenant?: TenantOption;
|
||
application?: { id: string; name: string };
|
||
template?: { id: string; name: string; content: string; billingUnits?: number };
|
||
messages?: SmsMessageRecord[];
|
||
messageStats?: Array<{
|
||
batchTaskId?: string | null;
|
||
carrier?: string | null;
|
||
province?: string | null;
|
||
status: string;
|
||
_count: { _all: number };
|
||
_sum: { billingUnits?: number | null };
|
||
}>;
|
||
};
|
||
|
||
export type ImportPreviewResponse = {
|
||
fileName?: string;
|
||
encoding: string;
|
||
totalRows: number;
|
||
validCount: number;
|
||
errorCount: number;
|
||
phones: string[];
|
||
errors: Array<{ rowNumber: number; phoneNumber?: string; reason: string }>;
|
||
};
|
||
|
||
export type SmsMessageRecord = {
|
||
id: string;
|
||
tenantId: string;
|
||
batchTaskId?: string | null;
|
||
applicationId?: string | null;
|
||
channelId?: string | null;
|
||
messageId: string;
|
||
phoneNumber: string;
|
||
carrier?: string | null;
|
||
province?: string | null;
|
||
content: string;
|
||
billingUnits: number;
|
||
amountCents: number;
|
||
status: string;
|
||
errorMessage?: string | null;
|
||
errorCode?: string | null;
|
||
queuedAt: string;
|
||
submittedAt?: string | null;
|
||
deliveredAt?: string | null;
|
||
receiptStatus?: string | null;
|
||
submitStatus?: string | null;
|
||
channel?: AdminChannel | null;
|
||
tenant?: TenantOption | null;
|
||
application?: { id: string; name: string };
|
||
submitRecords?: SmsSubmitRecord[];
|
||
receiptRecords?: SmsReceiptRecord[];
|
||
};
|
||
|
||
export type SmsSubmitRecord = {
|
||
id: string;
|
||
channelId: string;
|
||
submitId: string;
|
||
sequenceId?: number | null;
|
||
gatewayMessageId?: string | null;
|
||
submitStatus: string;
|
||
errorCode?: string | null;
|
||
errorMessage?: string | null;
|
||
submittedAt?: string | null;
|
||
createdAt: string;
|
||
channel?: AdminChannel | null;
|
||
};
|
||
|
||
export type SmsReceiptRecord = {
|
||
id: string;
|
||
channelId?: string | null;
|
||
messageId: string;
|
||
gatewayMessageId: string;
|
||
sequenceId?: number | null;
|
||
receiptStatus: string;
|
||
rawStatus: string;
|
||
errorCode?: string | null;
|
||
deliveredAt: string;
|
||
createdAt: string;
|
||
channel?: AdminChannel | null;
|
||
};
|
||
|
||
export type SmsMessageSegmentAudit = {
|
||
id: string;
|
||
tenantId: string;
|
||
batchTaskId?: string | null;
|
||
messageRecordId: string;
|
||
submitRecordId?: string | null;
|
||
channelId?: string | null;
|
||
submitId: string;
|
||
attempt: number;
|
||
segmentTotal: number;
|
||
segmentIndex: number;
|
||
sequenceId?: number | null;
|
||
gatewayMessageId?: string | null;
|
||
submitStatus: string;
|
||
receiptStatus?: string | null;
|
||
rawStatus?: string | null;
|
||
compensationType?: string | null;
|
||
errorCode?: string | null;
|
||
errorMessage?: string | null;
|
||
submittedAt?: string | null;
|
||
deliveredAt?: string | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
channel?: AdminChannel | null;
|
||
};
|
||
|
||
export type SmsUplinkMessage = {
|
||
id: string;
|
||
tenantId?: string | null;
|
||
channelId: string;
|
||
applicationId?: string | null;
|
||
messageRecordId?: string | null;
|
||
messageId?: string | null;
|
||
sequenceId?: number | null;
|
||
phoneNumber: string;
|
||
destId: string;
|
||
content: string;
|
||
matchStatus?: string;
|
||
matchReason?: string | null;
|
||
receivedAt: string;
|
||
createdAt: string;
|
||
tenant?: TenantOption | null;
|
||
application?: { id: string; name: string } | null;
|
||
messageRecord?: SmsMessageRecord | null;
|
||
channel?: AdminChannel | null;
|
||
matchCandidates?: SmsUplinkMatchCandidate[];
|
||
};
|
||
|
||
export type SmsUplinkMatchCandidate = {
|
||
id: string;
|
||
uplinkMessageId: string;
|
||
tenantId: string;
|
||
applicationId: string;
|
||
messageRecordId?: string | null;
|
||
matchSource: string;
|
||
confidence: number;
|
||
reason?: string | null;
|
||
status: string;
|
||
claimedAt?: string | null;
|
||
claimedById?: string | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
tenant?: TenantOption | null;
|
||
application?: { id: string; name: string } | null;
|
||
messageRecord?: SmsMessageRecord | null;
|
||
};
|
||
|
||
export type DictionaryItem = Record<string, unknown> & {
|
||
id: string;
|
||
status?: string;
|
||
createdAt?: string;
|
||
updatedAt?: string;
|
||
};
|
||
|
||
export type ChannelGroup = DictionaryItem & {
|
||
code: string;
|
||
name: string;
|
||
carrier: 'mobile' | 'unicom' | 'telecom';
|
||
description?: string | null;
|
||
retryEnabled?: boolean;
|
||
retryTimeLimitHours?: number;
|
||
retryTimeLimitMinutes?: number;
|
||
items?: ChannelGroupItem[];
|
||
};
|
||
|
||
export type ChannelGroupItem = DictionaryItem & {
|
||
groupId: string;
|
||
channelId: string;
|
||
carrier?: 'mobile' | 'unicom' | 'telecom' | null;
|
||
province?: string | null;
|
||
priority: number;
|
||
weight?: number;
|
||
isBackup?: boolean;
|
||
rateLimitPerSecond?: number | null;
|
||
channel?: AdminChannel;
|
||
};
|
||
|
||
export type ChannelReportField = DictionaryItem & {
|
||
channelId: string;
|
||
drainageFieldId?: string | null;
|
||
reportType?: 'signature' | 'drainage' | 'both';
|
||
code: string;
|
||
name: string;
|
||
fieldType: string;
|
||
required: boolean;
|
||
description?: string | null;
|
||
sortOrder?: number;
|
||
drainageField?: DictionaryItem | null;
|
||
};
|
||
|
||
export type ApplicationReportField = {
|
||
id: string;
|
||
code: string;
|
||
name: string;
|
||
fieldType: string;
|
||
required: boolean;
|
||
description?: string | null;
|
||
reportTypes: string[];
|
||
channels: Array<{ id: string; code: string; name: string; groupId: string; groupName: string; required: boolean; reportType: 'signature' | 'drainage' | 'both' }>;
|
||
};
|
||
|
||
export type ReportTask = DictionaryItem & {
|
||
tenantId: string;
|
||
signatureId: string;
|
||
channelId: string;
|
||
reportType?: 'signature' | 'drainage';
|
||
drainageItemId?: string | null;
|
||
status: string;
|
||
signature?: { id: string; name: string; purpose?: string | null; drainageInfo?: Record<string, unknown> | null };
|
||
drainageInfo?: SmsDrainageInfo | null;
|
||
channel?: { id: string; name: string; code: string };
|
||
};
|
||
|
||
export type ReportRecord = DictionaryItem & {
|
||
taskId: string;
|
||
channelId: string;
|
||
action: string;
|
||
statusBefore?: string | null;
|
||
statusAfter?: string | null;
|
||
reason?: string | null;
|
||
sourceEntry?: 'system' | 'legacy' | 'enterprise_signature' | 'report_task' | 'channel_report';
|
||
channel?: AdminChannel;
|
||
task?: ReportTask;
|
||
};
|
||
|
||
export type FileObject = {
|
||
id: string;
|
||
tenantId?: string | null;
|
||
bucket: string;
|
||
objectKey: string;
|
||
fileName: string;
|
||
contentType: string;
|
||
sizeBytes: string | number;
|
||
purpose: string;
|
||
createdAt: string;
|
||
};
|
||
|
||
export type FileRef = {
|
||
fileObjectId: string;
|
||
fileName: string;
|
||
contentType?: string;
|
||
};
|
||
|
||
export function fileDownloadUrl(fileObjectId: string, disposition: 'attachment' | 'inline' = 'attachment') {
|
||
return `/api/admin/files/${encodeURIComponent(fileObjectId)}/download?disposition=${disposition}`;
|
||
}
|
||
|
||
export type RiskReviewTask = {
|
||
id: string;
|
||
tenantId: string;
|
||
applicationId?: string | null;
|
||
templateId?: string | null;
|
||
taskNo: string;
|
||
sourceType?: string;
|
||
contentHash?: string | null;
|
||
windowStartedAt?: string | null;
|
||
windowEndsAt?: string | null;
|
||
content: string;
|
||
category?: string | null;
|
||
phoneTotal: number;
|
||
uniquePhoneTotal: number;
|
||
duplicateRatio: number;
|
||
illegalRatio: number;
|
||
blacklistHitRatio: number;
|
||
variableIssues?: unknown;
|
||
status: string;
|
||
riskDecision: string;
|
||
reviewReason?: string | null;
|
||
rejectReason?: string | null;
|
||
createdAt: string;
|
||
reviewedAt?: string | null;
|
||
riskHits?: Array<{ id: string; ruleName: string; reason: string }>;
|
||
_count?: { messageRecords: number };
|
||
};
|
||
|
||
export type TenantAccount = {
|
||
id: string;
|
||
tenantId: string;
|
||
balanceCents: number;
|
||
status: string;
|
||
tenant?: TenantOption;
|
||
};
|
||
|
||
export type OperationLogItem = {
|
||
id: string;
|
||
time: string;
|
||
level: 'info' | 'success' | 'warning' | 'error';
|
||
tenant: string;
|
||
module: string;
|
||
operator: string;
|
||
action: string;
|
||
resourceId: string;
|
||
detail: Record<string, unknown>;
|
||
ip: string;
|
||
userAgent: string;
|
||
};
|
||
|
||
export type OperationLogResponse = {
|
||
items: OperationLogItem[];
|
||
total: number;
|
||
page: number;
|
||
pageSize: number;
|
||
modules: string[];
|
||
};
|
||
|
||
export type PagedResponse<T> = {
|
||
items: T[];
|
||
total: number;
|
||
page: number;
|
||
pageSize: number;
|
||
};
|
||
|
||
export type CursorPage<T> = {
|
||
items: T[];
|
||
pageSize: number;
|
||
hasMore: boolean;
|
||
nextCursor: string | null;
|
||
};
|
||
|
||
export type EnterpriseApplication = {
|
||
id: string;
|
||
tenantId: string;
|
||
name: string;
|
||
scene?: string | null;
|
||
status: string;
|
||
dailyLimit?: number | null;
|
||
customerUnitPrice?: number | null;
|
||
queuePriority?: 'normal' | 'priority' | string | null;
|
||
maxPhonesPerTask?: number | null;
|
||
templateMismatchMode?: string | null;
|
||
downstreamReceiptRetryEnabled?: boolean | null;
|
||
downstreamUplinkRetryEnabled?: boolean | null;
|
||
cmppAccount?: string | null;
|
||
cmppEnterpriseCode?: string | null;
|
||
interfaceEnabled?: boolean | null;
|
||
interfaceType?: 'cmpp20' | string | null;
|
||
cmppMaxConnections?: number | null;
|
||
cmppWindowSize?: number | null;
|
||
ipAllowlist?: Array<{ id: string; ipCidr: string; remark?: string | null }>;
|
||
tenant?: TenantOption;
|
||
sentToday?: number;
|
||
deliveryRate?: number;
|
||
cmppStatus?: 'connected' | 'degraded' | 'disconnected' | 'inactive';
|
||
cmppConnections?: CmppDownstreamConnection[];
|
||
};
|
||
|
||
export type CmppDownstreamConnection = {
|
||
id: string;
|
||
tenantId: string;
|
||
applicationId: string;
|
||
account: string;
|
||
enterpriseCode: string;
|
||
connectionId: string;
|
||
remoteIp?: string | null;
|
||
protocol?: string | null;
|
||
status: string;
|
||
connectedAt: string;
|
||
lastHeartbeatAt?: string | null;
|
||
lastSubmitAt?: string | null;
|
||
lastDeliverAt?: string | null;
|
||
disconnectedAt?: string | null;
|
||
lastError?: string | null;
|
||
updatedAt: string;
|
||
};
|
||
|
||
export type CmppConnectionState = {
|
||
id: string;
|
||
tenantId?: string | null;
|
||
applicationId?: string | null;
|
||
channelId: string;
|
||
connectionId: string;
|
||
status: string;
|
||
desiredConnections: number;
|
||
currentConnections: number;
|
||
lastConnectedAt?: string | null;
|
||
lastDisconnectedAt?: string | null;
|
||
lastHeartbeatAt?: string | null;
|
||
reconnectCount: number;
|
||
lastError?: string | null;
|
||
updatedAt: string;
|
||
channel?: AdminChannel;
|
||
};
|
||
|
||
export type ApplicationConnectionsResponse = {
|
||
application: EnterpriseApplication;
|
||
connections: CmppDownstreamConnection[];
|
||
summary: { desiredConnections: number; currentConnections: number; status: string };
|
||
};
|
||
|
||
export type ApplicationCmppParams = {
|
||
applicationId: string;
|
||
applicationName: string;
|
||
tenantName: string;
|
||
appCode: string;
|
||
gatewayHost: string;
|
||
gatewayPort: number;
|
||
enterpriseCode: string;
|
||
account: string;
|
||
passwordCipher: string;
|
||
srcId: string;
|
||
interfaceEnabled?: boolean;
|
||
interfaceType?: string;
|
||
maxConnections: number;
|
||
heartbeatSeconds: number;
|
||
windowSize: number;
|
||
protocolVersion: string;
|
||
};
|
||
|
||
export type DownstreamDeliveryRecord = {
|
||
id: string;
|
||
tenantId: string;
|
||
applicationId: string;
|
||
messageRecordId?: string | null;
|
||
messageId?: string | null;
|
||
deliveryType: string;
|
||
status: string;
|
||
payload: Record<string, unknown>;
|
||
retryCount: number;
|
||
retryEnabled: boolean;
|
||
nextRetryAt?: string | null;
|
||
sentAt?: string | null;
|
||
acknowledgedAt?: string | null;
|
||
ackDeadlineAt?: string | null;
|
||
ackResult?: number | null;
|
||
ackSequenceId?: string | null;
|
||
ackMessageId?: string | null;
|
||
connectionId?: string | null;
|
||
deliveredAt?: string | null;
|
||
lastError?: string | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
tenant?: TenantOption | null;
|
||
application?: EnterpriseApplication | null;
|
||
messageRecord?: SmsMessageRecord | null;
|
||
};
|
||
|
||
export type BatchRequeueResponse = {
|
||
total: number;
|
||
successCount: number;
|
||
failedCount: number;
|
||
results: Array<{ id: string; status: 'success' | 'failed'; errorMessage?: string }>;
|
||
};
|
||
|
||
export type DownstreamDeliveryDashboard = {
|
||
summary: {
|
||
total: number;
|
||
pending: number;
|
||
awaitingAck: number;
|
||
delivered: number;
|
||
failed: number;
|
||
unconfirmed: number;
|
||
rejected: number;
|
||
stalledPending: number;
|
||
stalledAck: number;
|
||
recentFailed: number;
|
||
alertCount: number;
|
||
};
|
||
typeBreakdown: Array<{
|
||
deliveryType: string;
|
||
total: number;
|
||
pending: number;
|
||
awaitingAck: number;
|
||
delivered: number;
|
||
failed: number;
|
||
unconfirmed: number;
|
||
rejected: number;
|
||
}>;
|
||
retryBuckets: Array<{
|
||
label: string;
|
||
count: number;
|
||
}>;
|
||
topApplications: Array<{
|
||
applicationId: string;
|
||
name: string;
|
||
pending: number;
|
||
awaitingAck: number;
|
||
failed: number;
|
||
unconfirmed: number;
|
||
rejected: number;
|
||
delivered: number;
|
||
alertCount: number;
|
||
}>;
|
||
};
|
||
|
||
export type GatewayDownstreamRecoveryStatus = {
|
||
id: string;
|
||
account: string;
|
||
tenantId?: string | null;
|
||
applicationId?: string | null;
|
||
gatewayInstanceId?: string | null;
|
||
state: string;
|
||
lockOwner?: string | null;
|
||
lockExpiresAt?: string | null;
|
||
lastAttemptAt?: string | null;
|
||
lastSuccessAt?: string | null;
|
||
lastFailureAt?: string | null;
|
||
nextRetryAt?: string | null;
|
||
attemptCount: number;
|
||
failureCategory?: string | null;
|
||
lastError?: string | null;
|
||
lastSkipReason?: string | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
tenant?: TenantOption | null;
|
||
application?: EnterpriseApplication | null;
|
||
};
|
||
|
||
export type DownstreamRecoveryStatusResponse = PagedResponse<GatewayDownstreamRecoveryStatus> & {
|
||
summary: {
|
||
total: number;
|
||
running: number;
|
||
success: number;
|
||
failed: number;
|
||
waitingConnection: number;
|
||
backoff: number;
|
||
failureCategories: Array<{ category: string; count: number }>;
|
||
};
|
||
};
|
||
|
||
export type DownstreamRecoveryStatusExportQuery = {
|
||
tenantId?: string;
|
||
applicationId?: string;
|
||
state?: string;
|
||
failureCategory?: string;
|
||
keyword?: string;
|
||
};
|
||
|
||
function withQuery(path: string, query: Record<string, string | number | undefined>) {
|
||
const params = new URLSearchParams();
|
||
Object.entries(query).forEach(([key, value]) => {
|
||
if (value !== undefined && value !== '' && value !== 'all') {
|
||
params.set(key, String(value));
|
||
}
|
||
});
|
||
const suffix = params.toString() ? `?${params}` : '';
|
||
return `${path}${suffix}`;
|
||
}
|
||
|
||
export const adminApi = {
|
||
getCaptcha: () => request<CaptchaResponse>('/admin/auth/captcha'),
|
||
login: (body: { login: string; password: string; captchaId: string; captchaText: string }) =>
|
||
request<LoginSession>('/admin/auth/login', { method: 'POST', body: JSON.stringify(body) }),
|
||
touchSession: () => request<Pick<LoginSession, 'idleTimeoutSeconds' | 'lockRecoverySeconds' | 'absoluteExpiresAt' | 'lastActivityAt' | 'recentAuthenticationExpiresAt'>>('/auth/session/touch', { method: 'POST', body: '{}' }),
|
||
lockSession: () => request<{ locked: boolean }>('/auth/session/lock', { method: 'POST', body: '{}' }),
|
||
unlockSession: (password: string) => request<Pick<LoginSession, 'idleTimeoutSeconds' | 'lockRecoverySeconds' | 'absoluteExpiresAt' | 'lastActivityAt' | 'recentAuthenticationExpiresAt'>>('/auth/session/unlock', { method: 'POST', body: JSON.stringify({ password }) }),
|
||
reauthenticate: (password: string) => request<Pick<LoginSession, 'recentAuthenticationExpiresAt'>>('/auth/reauthenticate', { method: 'POST', body: JSON.stringify({ password }), reauthenticationAttempted: true }),
|
||
logout: () => request<{ success: boolean }>('/auth/logout', { method: 'POST', body: '{}' }),
|
||
changeOwnPassword: (body: { currentPassword: string; password: string }) =>
|
||
request<ManagedUser>('/auth/password', { method: 'POST', body: JSON.stringify(body) }),
|
||
listTenants: () => request<TenantOption[]>('/admin/tenants'),
|
||
listTenantManagementRows: () => request<TenantManagementRow[]>('/admin/tenants/management-list'),
|
||
getTenant: (id: string) => request<TenantOption>(`/admin/tenants/${id}`),
|
||
createTenant: (body: { name: string; code?: string; status?: string; creditCode?: string; province?: string; city?: string; address?: string; contactName?: string; contactIdCard?: string; contactPhone?: string; contactEmail?: string; photoFileObjectId?: string }) =>
|
||
request<TenantOption>('/admin/tenants', { method: 'POST', body: JSON.stringify(body) }),
|
||
updateTenant: (id: string, body: { name?: string; code?: string; status?: string; creditCode?: string; province?: string; city?: string; address?: string; contactName?: string; contactIdCard?: string; contactPhone?: string; contactEmail?: string; photoFileObjectId?: string }) =>
|
||
request<TenantOption>(`/admin/tenants/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
|
||
changeTenantStatus: (id: string, status: string) =>
|
||
request<TenantOption>(`/admin/tenants/${id}/status`, { method: 'POST', body: JSON.stringify({ status }) }),
|
||
deleteTenant: (id: string) => request<TenantOption>(`/admin/tenants/${id}`, { method: 'DELETE' }),
|
||
listUsers: (query: { tenantId?: string; roleCode?: string } = {}) => request<ManagedUser[]>(withQuery('/admin/users', query)),
|
||
createUser: (body: UserPayload) => request<ManagedUser>('/admin/users', { method: 'POST', body: JSON.stringify(body) }),
|
||
updateUser: (id: string, body: Omit<UserPayload, 'password'>) => request<ManagedUser>(`/admin/users/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
|
||
changeUserStatus: (id: string, status: string, operatorId?: string) =>
|
||
request<ManagedUser>(`/admin/users/${id}/status`, { method: 'POST', body: JSON.stringify({ status, operatorId }) }),
|
||
deleteUser: (id: string, operatorId?: string) => request<ManagedUser>(`/admin/users/${id}`, { method: 'DELETE', body: JSON.stringify({ operatorId }) }),
|
||
changeUserPassword: (id: string, password: string, operatorId?: string) =>
|
||
request<ManagedUser>(`/admin/users/${id}/password`, { method: 'POST', body: JSON.stringify({ password, operatorId }) }),
|
||
getDashboard: (tenantId?: string) => request<DashboardResponse>(withQuery('/admin/operations/dashboard/statistics', { tenantId })),
|
||
listSystemLogs: (query: { tenantId?: string; keyword?: string; level?: string; module?: string; range?: string; page?: number; pageSize?: number }) =>
|
||
request<OperationLogResponse>(withQuery('/admin/system-logs', query)),
|
||
listAccounts: () => request<TenantAccount[]>('/admin/billing/accounts'),
|
||
listManualRecharges: (tenantId?: string) => request<RechargeOrder[]>(withQuery('/admin/billing/manual-recharges', { tenantId })),
|
||
createManualRecharge: (body: { tenantId: string; amountCents: number; operatorId?: string; remark?: string }) =>
|
||
request<RechargeOrder>('/admin/billing/manual-recharges', { method: 'POST', body: JSON.stringify(body) }),
|
||
listEnterpriseApplications: (query: { tenantId?: string; keyword?: string; enterpriseKeyword?: string; applicationKeyword?: string; status?: string } = {}) =>
|
||
request<EnterpriseApplication[]>(withQuery('/admin/enterprise-applications', query)),
|
||
getEnterpriseApplication: (id: string) =>
|
||
request<EnterpriseApplication>(`/admin/enterprise-applications/${id}`),
|
||
createEnterpriseApplication: (body: { tenantId: string; name: string; scene?: string; dailyLimit?: number; customerUnitPrice?: number; queuePriority?: 'normal' | 'priority'; maxPhonesPerTask?: number; templateMismatchMode?: string; downstreamReceiptRetryEnabled?: boolean; downstreamUplinkRetryEnabled?: boolean; cmppAccount?: string; cmppEnterpriseCode?: string; passwordCipher?: string; interfaceEnabled?: boolean; interfaceType?: 'cmpp20'; cmppMaxConnections?: number; cmppWindowSize?: number; ipAllowlist?: string[] }) =>
|
||
request<EnterpriseApplication>('/admin/enterprise-applications', { method: 'POST', body: JSON.stringify(body) }),
|
||
updateEnterpriseApplication: (id: string, body: { name?: string; scene?: string; dailyLimit?: number; customerUnitPrice?: number; queuePriority?: 'normal' | 'priority'; maxPhonesPerTask?: number; templateMismatchMode?: string; downstreamReceiptRetryEnabled?: boolean; downstreamUplinkRetryEnabled?: boolean; cmppAccount?: string; cmppEnterpriseCode?: string; passwordCipher?: string; interfaceEnabled?: boolean; interfaceType?: 'cmpp20'; cmppMaxConnections?: number; cmppWindowSize?: number; ipAllowlist?: string[] }) =>
|
||
request<EnterpriseApplication>(`/admin/enterprise-applications/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
|
||
changeApplicationStatus: (id: string, status: string, reason?: string) =>
|
||
request<EnterpriseApplication>(`/admin/enterprise-applications/${id}/status`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ status, reason }),
|
||
}),
|
||
listApplicationConnections: (applicationId: string) =>
|
||
request<ApplicationConnectionsResponse>(`/admin/enterprise-applications/${applicationId}/connections`),
|
||
listApplicationReportFields: (applicationId: string, reportType?: 'signature' | 'drainage') =>
|
||
request<ApplicationReportField[]>(withQuery(`/admin/enterprise-applications/${applicationId}/report-fields`, { reportType })),
|
||
getApplicationCmppParams: (applicationId: string) =>
|
||
request<ApplicationCmppParams>(`/admin/enterprise-applications/${applicationId}/cmpp-params`),
|
||
listChannels: () => request<AdminChannel[]>('/admin/channels'),
|
||
createChannel: (body: Partial<AdminChannel> & { passwordCipher?: string; desiredConnections?: number; windowSize?: number }) =>
|
||
request<AdminChannel>('/admin/channels', { method: 'POST', body: JSON.stringify(body) }),
|
||
updateChannel: (id: string, body: Partial<AdminChannel> & { passwordCipher?: string; desiredConnections?: number; windowSize?: number }) =>
|
||
request<AdminChannel>(`/admin/channels/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
|
||
copyChannel: (id: string, body: { operatorId?: string } = {}) => request<AdminChannel>(`/admin/channels/${id}/copy`, {
|
||
method: 'POST',
|
||
body: JSON.stringify(body),
|
||
}),
|
||
testChannel: (id: string, body: { phoneNumber?: string; phones?: string[] | string; content: string; accessNo?: string }) =>
|
||
request<ChannelTestResponse>(`/admin/channels/${id}/test`, { method: 'POST', body: JSON.stringify(body) }),
|
||
changeChannelStatus: (id: string, status: string, reason?: string) => request<AdminChannel>(`/admin/channels/${id}/status`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ status, reason }),
|
||
}),
|
||
deleteChannel: (id: string, reason?: string) => request<AdminChannel>(`/admin/channels/${id}`, {
|
||
method: 'DELETE',
|
||
body: JSON.stringify({ reason }),
|
||
}),
|
||
listChannelConnectionLogs: (id: string) => request<ChannelConnectionLogResponse>(`/admin/channels/${id}/connection-logs`),
|
||
listTemplateAudits: (query: { keyword?: string; status?: string }) => {
|
||
const params = new URLSearchParams();
|
||
if (query.keyword) params.set('keyword', query.keyword);
|
||
if (query.status && query.status !== 'all') params.set('status', query.status);
|
||
const suffix = params.toString() ? `?${params}` : '';
|
||
return request<SmsTemplateAudit[]>(`/admin/enterprise-templates${suffix}`);
|
||
},
|
||
approveTemplate: (id: string) => request<SmsTemplateAudit>(`/admin/templates/${id}/approve`, { method: 'POST', body: JSON.stringify({}) }),
|
||
rejectTemplate: (id: string, reason = '运营审核驳回') => request<SmsTemplateAudit>(`/admin/templates/${id}/reject`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ reason }),
|
||
}),
|
||
approveSignature: (id: string) => request<ClientSmsSignature>(`/admin/signatures/${id}/approve`, { method: 'POST', body: JSON.stringify({}) }),
|
||
rejectSignature: (id: string, reason = '运营审核驳回') => request<ClientSmsSignature>(`/admin/signatures/${id}/reject`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ reason }),
|
||
}),
|
||
listEnterpriseSignatures: (query: { tenantId?: string; keyword?: string; status?: string; enterpriseKeyword?: string; applicationKeyword?: string; signatureKeyword?: string; drainageKeyword?: string } = {}) =>
|
||
request<ClientSmsSignature[]>(withQuery('/admin/enterprise-signatures', query)),
|
||
createEnterpriseSignature: (body: { tenantId: string; applicationId?: string; name: string; purpose?: string; drainageInfo?: Record<string, unknown> }) =>
|
||
request<ClientSmsSignature>('/admin/enterprise-signatures', { method: 'POST', body: JSON.stringify(body) }),
|
||
updateEnterpriseSignature: (id: string, body: { applicationId?: string | null; name?: string; purpose?: string; auditStatus?: string; drainageInfo?: Record<string, unknown> }) =>
|
||
request<ClientSmsSignature>(`/admin/enterprise-signatures/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
|
||
changeEnterpriseSignatureStatus: (id: string, status: string, reason?: string) =>
|
||
request<ClientSmsSignature>(`/admin/enterprise-signatures/${id}/status`, { method: 'POST', body: JSON.stringify({ status, reason }) }),
|
||
listDrainageInfos: (query: { tenantId?: string; signatureId?: string; keyword?: string; status?: string } = {}) =>
|
||
request<SmsDrainageInfo[]>(withQuery('/admin/drainage-infos', query)),
|
||
createDrainageInfo: (signatureId: string, body: { siteName: string; url: string; remark?: string; reportValues?: Record<string, unknown> }) =>
|
||
request<SmsDrainageInfo>(`/admin/enterprise-signatures/${signatureId}/drainage-infos`, { method: 'POST', body: JSON.stringify(body) }),
|
||
updateDrainageInfo: (id: string, body: { siteName?: string; url?: string; remark?: string; reportValues?: Record<string, unknown> }) =>
|
||
request<SmsDrainageInfo>(`/admin/drainage-infos/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
|
||
approveDrainageInfo: (id: string) =>
|
||
request<SmsDrainageInfo>(`/admin/drainage-infos/${id}/approve`, { method: 'POST', body: JSON.stringify({}) }),
|
||
rejectDrainageInfo: (id: string, reason: string) =>
|
||
request<SmsDrainageInfo>(`/admin/drainage-infos/${id}/reject`, { method: 'POST', body: JSON.stringify({ reason }) }),
|
||
changeDrainageInfoStatus: (id: string, status: string, reason?: string) =>
|
||
request<SmsDrainageInfo>(`/admin/drainage-infos/${id}/status`, { method: 'POST', body: JSON.stringify({ status, reason }) }),
|
||
listEnterpriseTemplates: (query: { tenantId?: string; keyword?: string; status?: string; enterpriseKeyword?: string; applicationKeyword?: string; nameKeyword?: string; contentKeyword?: string } = {}) =>
|
||
request<ClientSmsTemplate[]>(withQuery('/admin/enterprise-templates', query)),
|
||
createEnterpriseTemplate: (body: { tenantId: string; applicationId: string; signatureId?: string; name: string; content: string; category?: string; variables?: Array<{ name: string; example?: string; required?: boolean }> }) =>
|
||
request<ClientSmsTemplate>('/admin/enterprise-templates', { method: 'POST', body: JSON.stringify(body) }),
|
||
updateEnterpriseTemplate: (id: string, body: { applicationId?: string; signatureId?: string | null; name?: string; content?: string; category?: string; auditStatus?: string; variables?: Array<{ name: string; example?: string; required?: boolean }> }) =>
|
||
request<ClientSmsTemplate>(`/admin/enterprise-templates/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
|
||
changeEnterpriseTemplateStatus: (id: string, status: string, reason?: string) =>
|
||
request<ClientSmsTemplate>(`/admin/enterprise-templates/${id}/status`, { method: 'POST', body: JSON.stringify({ status, reason }) }),
|
||
listEnterpriseCertifications: (query: { keyword?: string; status?: string }) => {
|
||
const params = new URLSearchParams();
|
||
if (query.keyword) params.set('keyword', query.keyword);
|
||
if (query.status && query.status !== 'all') params.set('status', query.status);
|
||
const suffix = params.toString() ? `?${params}` : '';
|
||
return request<EnterpriseCertification[]>(`/admin/enterprise-certifications${suffix}`);
|
||
},
|
||
getEnterpriseCertification: (id: string) => request<EnterpriseCertification>(`/admin/enterprise-certifications/${id}`),
|
||
approveEnterpriseCertification: (id: string) => request<EnterpriseCertification>(`/admin/enterprise-certifications/${id}/approve`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({}),
|
||
}),
|
||
rejectEnterpriseCertification: (id: string, reason = '运营审核驳回') => request<EnterpriseCertification>(`/admin/enterprise-certifications/${id}/reject`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ reason }),
|
||
}),
|
||
listChannelGroups: () => request<ChannelGroup[]>('/admin/channel-groups'),
|
||
createChannelGroup: (body: { code: string; name: string; carrier: 'mobile' | 'unicom' | 'telecom'; description?: string; status?: string; retryEnabled?: boolean; retryTimeLimitHours?: number; retryTimeLimitMinutes?: number }) =>
|
||
request<ChannelGroup>('/admin/channel-groups', { method: 'POST', body: JSON.stringify(body) }),
|
||
updateChannelGroup: (id: string, body: { code?: string; name?: string; carrier?: 'mobile' | 'unicom' | 'telecom'; description?: string; status?: string; retryEnabled?: boolean; retryTimeLimitHours?: number; retryTimeLimitMinutes?: number; items?: Array<Record<string, unknown>> }) =>
|
||
request<ChannelGroup>(`/admin/channel-groups/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
|
||
deleteChannelGroup: (id: string) =>
|
||
request<ChannelGroup>(`/admin/channel-groups/${id}`, { method: 'DELETE' }),
|
||
addChannelGroupItem: (body: Record<string, unknown>) =>
|
||
request<DictionaryItem>('/admin/channel-groups/items', { method: 'POST', body: JSON.stringify(body) }),
|
||
listChannelRouteRules: () => request<DictionaryItem[]>('/admin/channel-route-rules'),
|
||
createChannelRouteRule: (body: { tenantId?: string; applicationId: string; groupId: string; carrier: string; priority?: number; status?: string }) =>
|
||
request<DictionaryItem>('/admin/channel-route-rules', { method: 'POST', body: JSON.stringify(body) }),
|
||
listChannelConnections: (id: string) => request<CmppConnectionState[]>(`/admin/channels/${id}/connections`),
|
||
replaceApplicationRouteRules: (applicationId: string, body: { routes: Array<{ carrier: 'mobile' | 'unicom' | 'telecom'; groupId: string; priority?: number; status?: string }> }) =>
|
||
request<DictionaryItem[]>(`/admin/enterprise-applications/${applicationId}/route-rules`, { method: 'PUT', body: JSON.stringify(body) }),
|
||
listChannelReportFields: (channelId?: string) => request<ChannelReportField[]>(withQuery('/admin/channel-report-fields', { channelId })),
|
||
createChannelReportField: (body: Record<string, unknown>) =>
|
||
request<ChannelReportField>('/admin/channel-report-fields', { method: 'POST', body: JSON.stringify(body) }),
|
||
listReportTasks: (query: { tenantId?: string; status?: string; channelId?: string; reportType?: 'signature' | 'drainage' } = {}) => request<ReportTask[]>(withQuery('/admin/report-tasks', query)),
|
||
createReportTask: (body: { tenantId: string; signatureId: string; channelId: string; reportType?: 'signature' | 'drainage'; drainageItemId?: string; createdById?: string }) =>
|
||
request<ReportTask>('/admin/report-tasks/generate', { method: 'POST', body: JSON.stringify(body) }),
|
||
changeReportTaskStatuses: (body: { items: Array<{ signatureId: string; channelId: string; status: string; reportType?: 'signature' | 'drainage'; drainageItemId?: string }>; reason?: string; operatorId?: string; sourceEntry?: 'enterprise_signature' | 'report_task' | 'channel_report' }) =>
|
||
request<Array<{ signatureId: string; reportStatus: string; carrierReportSummary: Record<string, { status: string; approved: number; total: number }> }>>('/admin/report-tasks/status-change', { method: 'POST', body: JSON.stringify(body) }),
|
||
createReportExport: (id: string, body: { fileObjectId?: string; fileName: string; rowCount?: number }) =>
|
||
request<Record<string, unknown>>(`/admin/report-tasks/${id}/export`, { method: 'POST', body: JSON.stringify(body) }),
|
||
importReportReceipt: (id: string, body: { fileObjectId?: string; fileName: string; fileContent?: string; delimiter?: ',' | '\t'; rowCount?: number; successCount?: number; failedCount?: number; statusAfter?: string; reason?: string; result?: Record<string, unknown> }) =>
|
||
request<Record<string, unknown>>(`/admin/report-tasks/${id}/receipt-import`, { method: 'POST', body: JSON.stringify(body) }),
|
||
listReportRecords: (query: { taskId?: string; channelId?: string } = {}) => request<ReportRecord[]>(withQuery('/admin/report-records', query)),
|
||
listAdminBatchTasks: (query: { tenantId?: string; status?: string } = {}) =>
|
||
request<SmsBatchTask[]>(withQuery('/admin/send/batch-tasks', query)),
|
||
terminateAdminBatchTask: (id: string) =>
|
||
request<SmsBatchTask>(`/admin/send/batch-tasks/${id}/terminate`, { method: 'POST', body: JSON.stringify({}) }),
|
||
listAdminMessages: (query: { tenantId?: string; applicationId?: string; channelId?: string; taskId?: string; phoneNumber?: string; status?: string } = {}) =>
|
||
request<SmsMessageRecord[]>(withQuery('/admin/send/messages', query)),
|
||
listMessageSegmentAudits: (query: { messageId?: string; messageRecordId?: string }) =>
|
||
request<SmsMessageSegmentAudit[]>(withQuery('/admin/operations/message-segment-audits', query)),
|
||
listOperationMessages: (query: { tenantId?: string; applicationId?: string; channelId?: string; channelKeyword?: string; taskId?: string; messageId?: string; phoneNumber?: string; contentKeyword?: string; status?: string; queuedAtFrom?: string; queuedAtTo?: string } = {}) =>
|
||
request<SmsMessageRecord[]>(withQuery('/admin/operations/messages', query)),
|
||
listAdminUplinkMessages: (query: { tenantId?: string; channelId?: string } = {}) =>
|
||
request<SmsUplinkMessage[]>(withQuery('/admin/operations/uplink-messages', query)),
|
||
claimUplinkMatchCandidate: (uplinkMessageId: string, body: { candidateId: string; operatorId?: string }) =>
|
||
request<SmsUplinkMessage>(`/admin/operations/uplink-messages/${uplinkMessageId}/claim`, { method: 'POST', body: JSON.stringify(body) }),
|
||
listMonitor: (query: { tenantId?: string; channelId?: string } = {}) => request<Record<string, unknown>>(withQuery('/admin/operations/monitor', query)),
|
||
listStatistics: (query: { tenantId?: string; groupBy?: string } = {}) => request<Array<Record<string, unknown>>>(withQuery('/admin/operations/statistics', query)),
|
||
getDownstreamDeliveryDashboard: (query: { tenantId?: string; applicationId?: string; deliveryType?: string } = {}) =>
|
||
request<DownstreamDeliveryDashboard>(withQuery('/admin/operations/downstream-deliveries/dashboard', query)),
|
||
listDownstreamRecoveryStatuses: (query: { tenantId?: string; applicationId?: string; state?: string; failureCategory?: string; keyword?: string; page?: number; pageSize?: number } = {}) =>
|
||
request<DownstreamRecoveryStatusResponse>(withQuery('/admin/operations/downstream-recovery-statuses', query)),
|
||
getDownstreamRecoveryStatus: (id: string) =>
|
||
request<GatewayDownstreamRecoveryStatus>(`/admin/operations/downstream-recovery-statuses/${id}`),
|
||
exportDownstreamRecoveryStatuses: (query: DownstreamRecoveryStatusExportQuery = {}) =>
|
||
requestBlob(withQuery('/admin/operations/downstream-recovery-statuses/export', query)),
|
||
listDownstreamDeliveries: (query: { tenantId?: string; applicationId?: string; deliveryType?: string; status?: string; keyword?: string; page?: number; pageSize?: number } = {}) =>
|
||
request<PagedResponse<DownstreamDeliveryRecord>>(withQuery('/admin/operations/downstream-deliveries', query)),
|
||
requeueDownstreamDelivery: (id: string) =>
|
||
request<DownstreamDeliveryRecord>(`/admin/operations/downstream-deliveries/${id}/requeue`, { method: 'POST', body: JSON.stringify({}) }),
|
||
batchRequeueDownstreamDeliveries: (ids: string[]) =>
|
||
request<BatchRequeueResponse>('/admin/operations/downstream-deliveries/requeue', { method: 'POST', body: JSON.stringify({ ids }) }),
|
||
listRiskReviewTasks: (query: { tenantId?: string; status?: string } = {}) => request<RiskReviewTask[]>(withQuery('/admin/risk-review/tasks', query)),
|
||
approveRiskReviewTask: (id: string, reason?: string) =>
|
||
request<RiskReviewTask>(`/admin/risk-review/tasks/${id}/approve`, { method: 'POST', body: JSON.stringify({ reason }) }),
|
||
rejectRiskReviewTask: (id: string, reason?: string) =>
|
||
request<RiskReviewTask>(`/admin/risk-review/tasks/${id}/reject`, { method: 'POST', body: JSON.stringify({ reason }) }),
|
||
listSensitiveWords: (query: { keyword?: string; status?: string } = {}) => request<DictionaryItem[]>(withQuery('/admin/dictionaries/sensitive-words', query)),
|
||
createSensitiveWord: (body: { word: string; level?: string; status?: string }) =>
|
||
request<DictionaryItem>('/admin/dictionaries/sensitive-words', { method: 'POST', body: JSON.stringify(body) }),
|
||
deleteSensitiveWord: (id: string) => request<DictionaryItem>(`/admin/dictionaries/sensitive-words/${id}`, { method: 'DELETE' }),
|
||
listGlobalBlacklist: (query: { keyword?: string; status?: string } = {}) => request<DictionaryItem[]>(withQuery('/admin/dictionaries/blacklists/global', query)),
|
||
createGlobalBlacklist: (body: { phoneNumber: string; reason?: string; status?: string; operatorId?: string }) =>
|
||
request<DictionaryItem>('/admin/dictionaries/blacklists/global', { method: 'POST', body: JSON.stringify(body) }),
|
||
deleteGlobalBlacklist: (id: string) => request<DictionaryItem>(`/admin/dictionaries/blacklists/global/${id}`, { method: 'DELETE' }),
|
||
listEnterpriseBlacklist: (query: { tenantId?: string; applicationId?: string; keyword?: string; status?: string; enterpriseKeyword?: string; applicationKeyword?: string; phoneNumber?: string; reasonKeyword?: string } = {}) => request<DictionaryItem[]>(withQuery('/admin/dictionaries/blacklists/enterprise', query)),
|
||
createEnterpriseBlacklist: (body: { tenantId: string; applicationId: string; phoneNumber: string; reason?: string; status?: string; operatorId?: string }) =>
|
||
request<DictionaryItem>('/admin/dictionaries/blacklists/enterprise', { method: 'POST', body: JSON.stringify(body) }),
|
||
deleteEnterpriseBlacklist: (id: string) => request<DictionaryItem>(`/admin/dictionaries/blacklists/enterprise/${id}`, { method: 'DELETE' }),
|
||
listPhoneSegments: (query: { keyword?: string; page?: number; pageSize?: number } = {}) =>
|
||
request<{ items: DictionaryItem[]; total: number; page: number; pageSize: number }>(withQuery('/admin/dictionaries/phone-segments', query)),
|
||
createPhoneSegment: (body: { prefix: string; carrier: string; province?: string; city?: string }) =>
|
||
request<DictionaryItem>('/admin/dictionaries/phone-segments', { method: 'POST', body: JSON.stringify(body) }),
|
||
listPhoneCarrierRules: (query: { keyword?: string; page?: number; pageSize?: number } = {}) =>
|
||
request<{ items: DictionaryItem[]; total: number; page: number; pageSize: number }>(withQuery('/admin/dictionaries/phone-carrier-rules', query)),
|
||
createPhoneCarrierRule: (body: { carrier: string; pattern: string; priority?: number; status?: string; remark?: string }) =>
|
||
request<DictionaryItem>('/admin/dictionaries/phone-carrier-rules', { method: 'POST', body: JSON.stringify(body) }),
|
||
listDrainageFields: () => request<DictionaryItem[]>('/admin/dictionaries/drainage-fields'),
|
||
createDrainageField: (body: { code: string; name: string; fieldType: 'string' | 'image' | 'file'; required?: boolean; status?: string; description?: string }) =>
|
||
request<DictionaryItem>('/admin/dictionaries/drainage-fields', { method: 'POST', body: JSON.stringify(body) }),
|
||
uploadFileObject: async (file: File, body: { purpose: string; prefix?: string }, tenantId?: string) => {
|
||
const form = new FormData();
|
||
form.set('file', file);
|
||
form.set('purpose', body.purpose);
|
||
if (body.prefix) {
|
||
form.set('prefix', body.prefix);
|
||
}
|
||
const headers = new Headers();
|
||
const session = readSession();
|
||
if (session && hasRecentUserActivity()) headers.set('x-session-activity', 'user');
|
||
if (tenantId) {
|
||
headers.set('x-tenant-id', tenantId);
|
||
}
|
||
const response = await fetch('/api/admin/files/upload', { method: 'POST', headers, body: form, credentials: 'same-origin' });
|
||
if (response.status === 401 && session) {
|
||
const error = await readErrorBody(response.clone());
|
||
if (error.code === 'SESSION_LOCKED') {
|
||
dispatchSessionEvent('locked', { message: error.message });
|
||
} else {
|
||
clearSession();
|
||
dispatchSessionEvent('logout', { code: error.code, message: error.message });
|
||
window.location.assign(session.portal === 'admin' ? '/admin/login' : '/client/login');
|
||
}
|
||
}
|
||
if (!response.ok) {
|
||
throw new Error(await response.text());
|
||
}
|
||
return response.json() as Promise<FileObject>;
|
||
},
|
||
};
|
||
|
||
export const clientApi = {
|
||
getCaptcha: () => request<CaptchaResponse>('/client/auth/captcha'),
|
||
login: (body: { login: string; password: string; captchaId: string; captchaText: string }) =>
|
||
request<LoginSession>('/client/auth/login', { method: 'POST', body: JSON.stringify(body) }),
|
||
listUsers: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ManagedUser[]>('/client/users', { tenantId }),
|
||
createUser: (body: UserPayload, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ManagedUser>('/client/users', { method: 'POST', tenantId, body: JSON.stringify(body) }),
|
||
updateUser: (id: string, body: Omit<UserPayload, 'password'>, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ManagedUser>(`/client/users/${id}`, { method: 'PUT', tenantId, body: JSON.stringify(body) }),
|
||
changeUserStatus: (id: string, status: string, operatorId?: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ManagedUser>(`/client/users/${id}/status`, { method: 'POST', tenantId, body: JSON.stringify({ status, operatorId }) }),
|
||
deleteUser: (id: string, operatorId?: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ManagedUser>(`/client/users/${id}`, { method: 'DELETE', tenantId, body: JSON.stringify({ operatorId }) }),
|
||
changeUserPassword: (id: string, password: string, operatorId?: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ManagedUser>(`/client/users/${id}/password`, { method: 'POST', tenantId, body: JSON.stringify({ password, operatorId }) }),
|
||
getDashboard: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<DashboardResponse>('/client/operations/dashboard', { tenantId }),
|
||
listEnterpriseCertifications: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<EnterpriseCertification[]>('/client/enterprise-certification', { tenantId }),
|
||
submitEnterpriseCertification: (body: { companyName: string; licenseNo?: string; contactName?: string; contactPhone?: string; materials?: Record<string, unknown> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<EnterpriseCertification>('/client/enterprise-certification', {
|
||
method: 'POST',
|
||
tenantId,
|
||
body: JSON.stringify({ ...body, tenantId }),
|
||
}),
|
||
listSystemLogs: (query: { keyword?: string; level?: string; module?: string; range?: string; page?: number; pageSize?: number }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<OperationLogResponse>(withQuery('/client/operations/system-logs', query), { tenantId }),
|
||
listOrders: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<RechargeOrder[]>('/client/billing/orders', { tenantId }),
|
||
listApplications: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ClientSmsApplication[]>('/client/applications', { tenantId }),
|
||
getApplicationCmppParams: (applicationId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ApplicationCmppParams>(`/client/applications/${applicationId}/cmpp-params`, { tenantId }),
|
||
listApplicationReportFields: (applicationId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ApplicationReportField[]>(`/client/applications/${applicationId}/report-fields`, { tenantId }),
|
||
listSignatures: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ClientSmsSignature[]>('/client/signatures', { tenantId }),
|
||
createSignature: (body: { tenantId?: string; applicationId?: string; name: string; purpose?: string; drainageInfo?: Record<string, unknown> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ClientSmsSignature>('/client/signatures', { method: 'POST', tenantId, body: JSON.stringify({ ...body, tenantId }) }),
|
||
submitSignature: (id: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ClientSmsSignature>(`/client/signatures/${id}/submit`, { method: 'POST', tenantId, body: JSON.stringify({}) }),
|
||
changeSignatureStatus: (id: string, status: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ClientSmsSignature>(`/client/signatures/${id}/status`, { method: 'POST', tenantId, body: JSON.stringify({ status }) }),
|
||
createSignatureMaterial: (id: string, body: { fileObjectId?: string; materialType: string; title: string; description?: string }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<Record<string, unknown>>(`/client/signatures/${id}/materials`, { method: 'POST', tenantId, body: JSON.stringify(body) }),
|
||
createDrainageInfo: (signatureId: string, body: { siteName: string; url: string; remark?: string; reportValues?: Record<string, unknown> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<SmsDrainageInfo>(`/client/signatures/${signatureId}/drainage-infos`, { method: 'POST', tenantId, body: JSON.stringify(body) }),
|
||
updateDrainageInfo: (id: string, body: { siteName?: string; url?: string; remark?: string; reportValues?: Record<string, unknown> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<SmsDrainageInfo>(`/client/drainage-infos/${id}`, { method: 'PUT', tenantId, body: JSON.stringify(body) }),
|
||
changeDrainageInfoStatus: (id: string, status: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<SmsDrainageInfo>(`/client/drainage-infos/${id}/status`, { method: 'POST', tenantId, body: JSON.stringify({ status }) }),
|
||
listTemplates: (query: { status?: string; keyword?: string } = {}, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ClientSmsTemplate[]>(withQuery('/client/templates', query), { tenantId }),
|
||
createTemplate: (body: { tenantId?: string; applicationId: string; signatureId?: string; name: string; content: string; category?: string; variables?: Array<{ name: string; example?: string; required?: boolean }> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ClientSmsTemplate>('/client/templates', { method: 'POST', tenantId, body: JSON.stringify({ ...body, tenantId }) }),
|
||
updateTemplate: (id: string, body: { applicationId?: string; signatureId?: string | null; name?: string; content?: string; category?: string; auditStatus?: string; variables?: Array<{ name: string; example?: string; required?: boolean }> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ClientSmsTemplate>(`/client/templates/${id}`, { method: 'PUT', tenantId, body: JSON.stringify(body) }),
|
||
submitTemplate: (id: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ClientSmsTemplate>(`/client/templates/${id}/submit`, { method: 'POST', tenantId, body: JSON.stringify({}) }),
|
||
changeTemplateStatus: (id: string, status: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ClientSmsTemplate>(`/client/templates/${id}/status`, { method: 'POST', tenantId, body: JSON.stringify({ status }) }),
|
||
listBatchTasks: (query: { status?: string } = {}, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<SmsBatchTask[]>(withQuery('/client/send/batch-tasks', query), { tenantId }),
|
||
cancelBatchTask: (id: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<SmsBatchTask>(`/client/send/batch-tasks/${id}/cancel`, { method: 'POST', tenantId, body: JSON.stringify({}) }),
|
||
createBatchTask: (body: { applicationId?: string; templateId?: string; content: string; category?: string; phones: string[]; sendMode?: 'immediate' | 'scheduled'; scheduledAt?: string; variables?: Record<string, unknown> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<SmsBatchTask>('/client/send/batch-tasks', { method: 'POST', tenantId, body: JSON.stringify({ ...body, tenantId }) }),
|
||
previewImport: (body: { applicationId?: string; content: string; fileName?: string; delimiter?: ',' | '\t'; requiredVariables?: string[] }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ImportPreviewResponse>('/client/send/imports/preview', { method: 'POST', tenantId, body: JSON.stringify({ ...body, tenantId }) }),
|
||
confirmImport: (body: { applicationId?: string; templateId?: string; content: string; category?: string; importContent: string; sendMode?: 'immediate' | 'scheduled'; scheduledAt?: string; requiredVariables?: string[]; variables?: Record<string, unknown> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<SmsBatchTask>('/client/send/imports/confirm', { method: 'POST', tenantId, body: JSON.stringify({ ...body, tenantId }) }),
|
||
listBatchTaskMessages: (id: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<SmsMessageRecord[]>(`/client/send/batch-tasks/${id}/messages`, { tenantId }),
|
||
listMessages: (query: { applicationId?: string; taskId?: string; messageId?: string; phoneNumber?: string; status?: string } = {}, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<SmsMessageRecord[]>(withQuery('/client/operations/messages', query), { tenantId }),
|
||
listUplinkMessages: (query: { channelId?: string } = {}, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<SmsUplinkMessage[]>(withQuery('/client/operations/uplink-messages', query), { tenantId }),
|
||
createFileObject: (body: { bucket?: string; objectKey: string; fileName: string; contentType: string; sizeBytes: number; purpose: string }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<FileObject>('/admin/files', { method: 'POST', tenantId, body: JSON.stringify({ ...body, tenantId, bucket: body.bucket ?? 'cmpp-platform' }) }),
|
||
uploadFileObject: async (file: File, body: { purpose: string; prefix?: string }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => {
|
||
const form = new FormData();
|
||
form.set('file', file);
|
||
form.set('purpose', body.purpose);
|
||
if (body.prefix) {
|
||
form.set('prefix', body.prefix);
|
||
}
|
||
const headers = new Headers();
|
||
const session = readSession();
|
||
if (session && hasRecentUserActivity()) headers.set('x-session-activity', 'user');
|
||
if (tenantId) {
|
||
headers.set('x-tenant-id', tenantId);
|
||
}
|
||
const response = await fetch('/api/admin/files/upload', { method: 'POST', headers, body: form, credentials: 'same-origin' });
|
||
if (response.status === 401 && session) {
|
||
const error = await readErrorBody(response.clone());
|
||
if (error.code === 'SESSION_LOCKED') {
|
||
dispatchSessionEvent('locked', { message: error.message });
|
||
} else {
|
||
clearSession();
|
||
dispatchSessionEvent('logout', { code: error.code, message: error.message });
|
||
window.location.assign(session.portal === 'admin' ? '/admin/login' : '/client/login');
|
||
}
|
||
}
|
||
if (!response.ok) {
|
||
throw new Error(await response.text());
|
||
}
|
||
return response.json() as Promise<FileObject>;
|
||
},
|
||
};
|