feat: add HTTP API and complete client workflows
This commit is contained in:
+83
-8
@@ -46,7 +46,8 @@ async function request<T>(path: string, options: RequestOptions = {}): Promise<T
|
||||
headers.set('x-tenant-id', tenantId);
|
||||
}
|
||||
const response = await fetch(`/api${path}`, { ...options, headers, credentials: 'same-origin' });
|
||||
if (response.status === 401 && session) {
|
||||
const isLoginAttempt = path === '/admin/auth/login' || path === '/client/auth/login';
|
||||
if (response.status === 401 && session && !isLoginAttempt) {
|
||||
const body = await readErrorBody(response.clone());
|
||||
if (body.code === 'SESSION_LOCKED') {
|
||||
dispatchSessionEvent('locked', { message: body.message });
|
||||
@@ -318,6 +319,7 @@ export type ClientSmsApplication = {
|
||||
deliveryRate?: number;
|
||||
cmppStatus?: 'connected' | 'degraded' | 'disconnected' | 'inactive';
|
||||
cmppConnections?: CmppDownstreamConnection[];
|
||||
httpConfig?: HttpApiConfig | null;
|
||||
};
|
||||
|
||||
export type ClientSmsSignature = {
|
||||
@@ -342,6 +344,32 @@ export type ClientSmsSignature = {
|
||||
drainageCarrierReportSummary?: Record<string, Record<'mobile' | 'unicom' | 'telecom', { status: string; approved: number; total: number }>>;
|
||||
};
|
||||
|
||||
export type ClientSmsSignatureView = Pick<ClientSmsSignature,
|
||||
'id' | 'tenantId' | 'applicationId' | 'name' | 'purpose' | 'auditStatus' | 'rejectReason' | 'createdAt' | 'updatedAt' | 'materials'
|
||||
> & {
|
||||
application?: Pick<ClientSmsApplication, 'id' | 'name' | 'status'> | null;
|
||||
submittedMaterialCount: number;
|
||||
reportValues: Record<string, unknown>;
|
||||
drainageInfo: { links: Array<{
|
||||
id: string;
|
||||
siteName: string;
|
||||
url: string;
|
||||
remark?: string | null;
|
||||
reportValues: Record<string, unknown>;
|
||||
auditStatus: string;
|
||||
rejectReason?: string | null;
|
||||
submittedAt: string;
|
||||
reviewedAt?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}> };
|
||||
};
|
||||
|
||||
export type ClientSignatureWorkspace = {
|
||||
items: ClientSmsSignatureView[];
|
||||
summary: { total: number; pending: number; approved: number; rejected: number; draft: number };
|
||||
};
|
||||
|
||||
export type SmsDrainageInfo = {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
@@ -557,6 +585,36 @@ export type SmsUplinkMatchCandidate = {
|
||||
messageRecord?: SmsMessageRecord | null;
|
||||
};
|
||||
|
||||
export type HttpApiConfig = {
|
||||
enabled: boolean;
|
||||
sendEnabled: boolean;
|
||||
messageQueryEnabled: boolean;
|
||||
receiptWebhookEnabled: boolean;
|
||||
uplinkWebhookEnabled: boolean;
|
||||
uplinkQueryEnabled: boolean;
|
||||
credentialSelfServiceEnabled: boolean;
|
||||
qpsLimit: number;
|
||||
timestampToleranceSeconds: number;
|
||||
maxCredentialCount: number;
|
||||
uplinkRetentionDays: number;
|
||||
maxQueryRangeDays: number;
|
||||
maxPageSize: number;
|
||||
receiptDeliveryMode: 'cmpp' | 'http' | 'both' | 'none';
|
||||
uplinkDeliveryMode: 'cmpp' | 'http' | 'both' | 'none';
|
||||
webhookRetryEnabled: boolean;
|
||||
webhookMaxAttempts: number;
|
||||
webhookTimeoutSeconds: number;
|
||||
requireHttps: boolean;
|
||||
allowClientManualRetry: boolean;
|
||||
allowClientTest: boolean;
|
||||
};
|
||||
|
||||
export type HttpApiConfigResponse = { applicationId: string; applicationName?: string; config: HttpApiConfig | null; ipAllowlist: string[] };
|
||||
export type HttpApiCredential = { id: string; name: string; accessKey: string; secretLast4: string; secret?: string; secretShownOnce?: boolean; status: string; expiresAt?: string | null; lastUsedAt?: string | null; lastUsedIp?: string | null; createdAt: string };
|
||||
export type HttpWebhookEndpoint = { id: string; eventType: 'receipt' | 'uplink'; url: string; secretLast4: string; secret?: string; secretShownOnce?: boolean; status: string; lastTestAt?: string | null; lastTestStatus?: string | null; updatedAt: string };
|
||||
export type HttpApiRequestLog = { id: string; requestId: string; clientMessageId?: string | null; sourceIp?: string | null; httpStatus?: number | null; businessCode?: string | null; status: string; durationMs?: number | null; createdAt: string; completedAt?: string | null };
|
||||
export type HttpWebhookDelivery = { id: string; status: string; attemptCount: number; lastHttpStatus?: number | null; lastError?: string | null; createdAt: string; event: { eventId: string; eventType: string; messageId?: string | null }; endpoint: { eventType: string; url: string } };
|
||||
|
||||
export type DictionaryItem = Record<string, unknown> & {
|
||||
id: string;
|
||||
status?: string;
|
||||
@@ -655,6 +713,8 @@ export type ApplicationReportField = {
|
||||
channels: Array<{ id: string; code: string; name: string; groupId: string; groupName: string; required: boolean; reportType: 'signature' | 'drainage' | 'both'; source?: 'common' | 'channel' | 'both' }>;
|
||||
};
|
||||
|
||||
export type ClientApplicationReportField = Omit<ApplicationReportField, 'channels' | 'commonReportTypes'>;
|
||||
|
||||
export type CommonReportField = DictionaryItem & {
|
||||
drainageFieldId: string;
|
||||
reportType: 'signature' | 'drainage';
|
||||
@@ -1167,6 +1227,8 @@ export const adminApi = {
|
||||
request<ApplicationReportField[]>(withQuery('/admin/report-fields/common', { reportType })),
|
||||
getApplicationCmppParams: (applicationId: string) =>
|
||||
request<ApplicationCmppParams>(`/admin/enterprise-applications/${applicationId}/cmpp-params`),
|
||||
getApplicationHttpApiConfig: (applicationId: string) => request<HttpApiConfigResponse>(`/admin/enterprise-applications/${applicationId}/http-api`),
|
||||
updateApplicationHttpApiConfig: (applicationId: string, body: Partial<HttpApiConfig> & { ipAllowlist?: string[] }) => request<HttpApiConfigResponse>(`/admin/enterprise-applications/${applicationId}/http-api`, { method: 'PUT', body: JSON.stringify(body) }),
|
||||
listChannels: () => request<AdminChannel[]>('/admin/channels'),
|
||||
listReconciliationReports: (query: { dateFrom?: string; dateTo?: string; tenantId?: string; applicationId?: string; page?: number; pageSize?: number } = {}) =>
|
||||
request<PagedResponse<DailyReconciliationReport>>(withQuery('/admin/reports/reconciliation', query)),
|
||||
@@ -1442,18 +1504,31 @@ export const clientApi = {
|
||||
request<ClientSmsApplication[]>('/client/applications', { tenantId }),
|
||||
getApplicationCmppParams: (applicationId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||||
request<ApplicationCmppParams>(`/client/applications/${applicationId}/cmpp-params`, { tenantId }),
|
||||
getApplicationHttpApiConfig: (applicationId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<HttpApiConfigResponse>(`/client/applications/${applicationId}/http-api`, { tenantId }),
|
||||
listHttpApiCredentials: (applicationId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<HttpApiCredential[]>(`/client/applications/${applicationId}/http-api/credentials`, { tenantId }),
|
||||
createHttpApiCredential: (applicationId: string, body: { name?: string; expiresAt?: string }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<HttpApiCredential>(`/client/applications/${applicationId}/http-api/credentials`, { method: 'POST', tenantId, body: JSON.stringify(body) }),
|
||||
revokeHttpApiCredential: (applicationId: string, credentialId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<{ id: string; status: string }>(`/client/applications/${applicationId}/http-api/credentials/${credentialId}/revoke`, { method: 'POST', tenantId, body: JSON.stringify({}) }),
|
||||
listHttpWebhooks: (applicationId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<HttpWebhookEndpoint[]>(`/client/applications/${applicationId}/http-api/webhooks`, { tenantId }),
|
||||
saveHttpWebhook: (applicationId: string, eventType: 'receipt' | 'uplink', body: { url: string; rotateSecret?: boolean; status?: string }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<HttpWebhookEndpoint>(`/client/applications/${applicationId}/http-api/webhooks/${eventType}`, { method: 'PUT', tenantId, body: JSON.stringify(body) }),
|
||||
listHttpApiRequests: (applicationId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<HttpApiRequestLog[]>(`/client/applications/${applicationId}/http-api/requests`, { tenantId }),
|
||||
listHttpWebhookDeliveries: (applicationId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<HttpWebhookDelivery[]>(`/client/applications/${applicationId}/http-api/webhook-deliveries`, { tenantId }),
|
||||
retryHttpWebhookDelivery: (applicationId: string, deliveryId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<{ id: string; status: string }>(`/client/applications/${applicationId}/http-api/webhook-deliveries/${deliveryId}/retry`, { method: 'POST', tenantId, body: JSON.stringify({}) }),
|
||||
listApplicationReportFields: (applicationId: string, reportType: 'signature' | 'drainage' = 'drainage', tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||||
request<ApplicationReportField[]>(withQuery(`/client/applications/${applicationId}/report-fields`, { reportType }), { tenantId }),
|
||||
request<ClientApplicationReportField[]>(withQuery(`/client/applications/${applicationId}/report-fields`, { reportType }), { tenantId }),
|
||||
listCommonApplicationReportFields: (reportType: 'signature' | 'drainage', tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||||
request<ApplicationReportField[]>(withQuery('/client/report-fields/common', { reportType }), { tenantId }),
|
||||
request<ClientApplicationReportField[]>(withQuery('/client/report-fields/common', { reportType }), { tenantId }),
|
||||
listSignatures: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||||
request<ClientSmsSignature[]>('/client/signatures', { tenantId }),
|
||||
request<ClientSmsSignatureView[]>('/client/signatures', { tenantId }),
|
||||
getSignatureWorkspace: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||||
request<ClientSignatureWorkspace>('/client/signatures-workspace', { 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 }) }),
|
||||
request<ClientSmsSignatureView>('/client/signatures', { method: 'POST', tenantId, body: JSON.stringify({ ...body, tenantId }) }),
|
||||
updateSignature: (id: string, body: { applicationId?: string; name?: string; purpose?: string; drainageInfo?: Record<string, unknown> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||||
request<ClientSmsSignatureView>(`/client/signatures/${id}`, { method: 'PUT', tenantId, body: JSON.stringify(body) }),
|
||||
submitSignature: (id: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||||
request<ClientSmsSignature>(`/client/signatures/${id}/submit`, { method: 'POST', tenantId, body: JSON.stringify({}) }),
|
||||
request<ClientSmsSignatureView>(`/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 }) }),
|
||||
request<ClientSmsSignatureView>(`/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) =>
|
||||
@@ -1486,7 +1561,7 @@ export const clientApi = {
|
||||
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) =>
|
||||
listUplinkMessages: (query: { channelId?: string; applicationId?: string; phoneNumber?: string; keyword?: string; startTime?: string; endTime?: 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' }) }),
|
||||
|
||||
Reference in New Issue
Block a user