fix: harden tenant auth and quality gates

This commit is contained in:
hectorzhao
2026-08-28 11:44:16 +08:00
parent c3bf8af3e6
commit 2744690f9f
51 changed files with 1750 additions and 466 deletions
@@ -3,164 +3,7 @@ import { AlertTriangle, BarChart3, CheckCircle2, Eye, RefreshCw, Search, TimerRe
import { adminApi, type DownstreamDeliveryDashboard, type DownstreamDeliveryRecord, type DownstreamRequeuePreview, type DownstreamRequeueTask, type DownstreamRequeueTaskItem, type EnterpriseApplication, type TenantOption } from '@/api/adminApi';
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Pagination, Select, Tag, Textarea, type DateRangeValue } from '@/components/ui';
import { formatDateTime } from '@/utils/dateTime';
const statusTone: Record<string, 'neutral' | 'info' | 'success' | 'warning' | 'danger'> = {
pending: 'warning',
manual_requeueing: 'info',
awaiting_ack: 'info',
delivered: 'success',
failed: 'danger',
unconfirmed: 'warning',
rejected: 'danger',
};
const statusLabel: Record<string, string> = {
manual_requeueing: '人工重投处理中',
awaiting_ack: '等待客户端确认',
delivered: '客户端已确认',
failed: '投递失败',
unconfirmed: '客户端未确认',
rejected: '客户端拒绝',
};
function deliveryStatusLabel(record: DownstreamDeliveryRecord) {
if (record.status !== 'pending') return statusLabel[record.status] ?? record.status;
if (record.manualRetryCount > 0) return '人工重投排队中';
if (record.retryCount > 0) return '等待自动重试';
return '待首次投递';
}
const deliveryTypeLabel: Record<string, string> = {
receipt: '状态回执',
uplink: '上行短信',
};
const attemptStatusLabel: Record<string, string> = {
awaiting_ack: '等待客户端确认',
acknowledged: '客户端已确认',
rejected: '客户端拒绝',
failed: '投递失败',
};
const requeueTaskStatusLabel: Record<string, string> = {
queued: '排队中', running: '执行中', paused: '已暂停', completed: '已完成',
partial_completed: '部分完成', terminated: '已终止',
};
const requeueItemStatusLabel: Record<string, string> = {
queued: '排队中', processing: '处理中', waiting_connection: '等待连接',
waiting_external_ack: '等待其他链路确认', waiting_ack: '等待客户确认', success: '成功',
failed: '失败', skipped: '跳过', unprocessed: '未处理',
};
function requeueTone(status: string) {
if (status === 'completed' || status === 'success') return 'success' as const;
if (status === 'partial_completed' || status === 'failed') return 'danger' as const;
if (status === 'paused' || status === 'skipped' || status === 'unprocessed') return 'warning' as const;
return 'info' as const;
}
type RequeueTarget =
| { kind: 'single'; record: DownstreamDeliveryRecord }
| { kind: 'batch'; ids: string[] };
type RequeueResult = {
status: 'success' | 'partial' | 'failed';
title: string;
message: string;
failures?: string[];
};
function formatLocalDate(value: Date) {
return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`;
}
function recentSevenDays(): DateRangeValue {
const end = new Date();
const start = new Date(end);
start.setDate(end.getDate() - 6);
return { start: formatLocalDate(start), end: formatLocalDate(end) };
}
function attemptStatusTone(status: string) {
if (status === 'acknowledged') return 'success' as const;
if (status === 'rejected' || status === 'failed') return 'danger' as const;
if (status === 'awaiting_ack') return 'info' as const;
return 'neutral' as const;
}
function DeliveryDetailModal({ record, onClose }: { record: DownstreamDeliveryRecord; onClose: () => void }) {
const payloadText = useMemo(() => JSON.stringify(record.payload ?? {}, null, 2), [record.payload]);
return (
<Modal
open
onClose={onClose}
size="xl"
title={<div className="template-modal-title"><h2></h2><p>{record.id}</p></div>}
footer={<Button onClick={onClose}></Button>}
>
<div className="report-record-detail">
<div className="detail-grid">
<div><span></span><strong>{record.tenant?.name ?? record.tenantId}</strong></div>
<div><span></span><strong>{record.application?.name ?? record.applicationId}</strong></div>
<div><span></span><strong>{deliveryTypeLabel[record.deliveryType] ?? record.deliveryType}</strong></div>
<div><span></span><strong>{deliveryStatusLabel(record)}</strong></div>
<div><span> ID</span><strong>{record.messageId ?? '-'}</strong></div>
<div><span></span><strong>{record.retryCount}</strong></div>
<div><span></span><strong>{record.manualRetryCount}</strong></div>
<div><span></span><strong>{record.lastRetriedAt ? formatDateTime(record.lastRetriedAt) : '-'}</strong></div>
<div><span></span><strong>{record.nextRetryAt ?? '-'}</strong></div>
<div><span></span><strong>{record.retryEnabled ? '开启' : '关闭'}</strong></div>
<div><span></span><strong>{record.sentAt ?? '-'}</strong></div>
<div><span></span><strong>{record.acknowledgedAt ?? '-'}</strong></div>
<div><span>ACK Result</span><strong>{record.ackResult ?? '-'}</strong></div>
<div><span>ACK Sequence_Id</span><strong>{record.ackSequenceId ?? '-'}</strong></div>
<div><span>ACK Msg_Id</span><strong>{record.ackMessageId ?? '-'}</strong></div>
<div><span> ID</span><strong>{record.connectionId ?? '-'}</strong></div>
<div className="detail-grid__wide"><span></span><strong>{record.lastError ?? '-'}</strong></div>
</div>
<section className="report-history">
<h3></h3>
<div className="downstream-attempt-timeline" aria-label="逐次投递记录">
{(record.attempts ?? []).map((attempt) => (
<article className="downstream-attempt-card" key={attempt.id}>
<div className={`downstream-attempt-marker downstream-attempt-marker--${attemptStatusTone(attempt.status)}`}>
{attempt.attemptNo}
</div>
<div className="downstream-attempt-card__body">
<header>
<strong> {attempt.attemptNo} </strong>
<Tag tone={attemptStatusTone(attempt.status)}>{attemptStatusLabel[attempt.status] ?? attempt.status}</Tag>
</header>
<dl className="downstream-attempt-card__times">
<div><dt></dt><dd>{attempt.sentAt ? formatDateTime(attempt.sentAt) : '-'}</dd></div>
<div><dt>ACK </dt><dd>{attempt.acknowledgedAt ? formatDateTime(attempt.acknowledgedAt) : '-'}</dd></div>
<div><dt>ACK </dt><dd>{attempt.ackDeadlineAt ? formatDateTime(attempt.ackDeadlineAt) : '-'}</dd></div>
</dl>
<dl className="downstream-attempt-card__identifiers">
<div><dt> ID</dt><dd>{attempt.connectionId ?? '-'}</dd></div>
<div><dt>Sequence_Id</dt><dd>{attempt.sequenceId ?? '-'}</dd></div>
<div><dt>Msg_Id</dt><dd>{attempt.messageId ?? '-'}</dd></div>
</dl>
<div className={`downstream-attempt-result${attempt.errorMessage || attempt.failureType ? ' downstream-attempt-result--error' : ''}`}>
<span>ACK Result{attempt.ackResult ?? '-'}</span>
<strong>{attempt.errorMessage ?? attempt.failureType ?? '本次投递未记录异常'}</strong>
</div>
</div>
</article>
))}
{(record.attempts ?? []).length === 0 ? <p></p> : null}
</div>
</section>
<section className="report-history">
<h3>Payload</h3>
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', margin: 0 }}>{payloadText}</pre>
</section>
</div>
</Modal>
);
}
import { DeliveryDetailModal, deliveryStatusLabel, deliveryTypeLabel, recentSevenDays, requeueItemStatusLabel, requeueTaskStatusLabel, requeueTone, statusLabel, statusTone, type RequeueResult, type RequeueTarget } from './downstreamDeliveryPresentation';
export function AdminDownstreamDeliveriesPage() {
const [records, setRecords] = useState<DownstreamDeliveryRecord[]>([]);
+1 -1
View File
@@ -9,13 +9,13 @@ import { useNavigate } from 'react-router-dom';
import {
Breadcrumb,
Button,
Chart,
Modal,
MoneyText,
Table,
Tag,
type TableColumn,
} from '@/components/ui';
import { Chart } from '@/components/ui/Chart';
import { adminApi, type DashboardResponse, type SendQualityResponse } from '@/api/adminApi';
import { createDualAxisBarLineOption, createLineOption } from '@/theme/chartOptions';
import { formatAmount, moneyUnitsToYuan } from '@/utils/currency';
+1 -1
View File
@@ -1,6 +1,6 @@
import { Check, X } from 'lucide-react';
import { Button, Tag, type TableColumn } from '@/components/ui';
import type { AuditItem, AuditStatus } from '@/mock';
import type { AuditItem, AuditStatus } from '@/api/types/audit';
const riskToneMap = {
low: 'success',
@@ -0,0 +1,162 @@
import { useMemo } from 'react';
import type { DownstreamDeliveryRecord } from '@/api/adminApi';
import { Button, Modal, Tag, type DateRangeValue } from '@/components/ui';
import { formatDateTime } from '@/utils/dateTime';
export const statusTone: Record<string, 'neutral' | 'info' | 'success' | 'warning' | 'danger'> = {
pending: 'warning',
manual_requeueing: 'info',
awaiting_ack: 'info',
delivered: 'success',
failed: 'danger',
unconfirmed: 'warning',
rejected: 'danger',
};
export const statusLabel: Record<string, string> = {
manual_requeueing: '人工重投处理中',
awaiting_ack: '等待客户端确认',
delivered: '客户端已确认',
failed: '投递失败',
unconfirmed: '客户端未确认',
rejected: '客户端拒绝',
};
export function deliveryStatusLabel(record: DownstreamDeliveryRecord) {
if (record.status !== 'pending') return statusLabel[record.status] ?? record.status;
if (record.manualRetryCount > 0) return '人工重投排队中';
if (record.retryCount > 0) return '等待自动重试';
return '待首次投递';
}
export const deliveryTypeLabel: Record<string, string> = {
receipt: '状态回执',
uplink: '上行短信',
};
const attemptStatusLabel: Record<string, string> = {
awaiting_ack: '等待客户端确认',
acknowledged: '客户端已确认',
rejected: '客户端拒绝',
failed: '投递失败',
};
export const requeueTaskStatusLabel: Record<string, string> = {
queued: '排队中', running: '执行中', paused: '已暂停', completed: '已完成',
partial_completed: '部分完成', terminated: '已终止',
};
export const requeueItemStatusLabel: Record<string, string> = {
queued: '排队中', processing: '处理中', waiting_connection: '等待连接',
waiting_external_ack: '等待其他链路确认', waiting_ack: '等待客户确认', success: '成功',
failed: '失败', skipped: '跳过', unprocessed: '未处理',
};
export function requeueTone(status: string) {
if (status === 'completed' || status === 'success') return 'success' as const;
if (status === 'partial_completed' || status === 'failed') return 'danger' as const;
if (status === 'paused' || status === 'skipped' || status === 'unprocessed') return 'warning' as const;
return 'info' as const;
}
export type RequeueTarget =
| { kind: 'single'; record: DownstreamDeliveryRecord }
| { kind: 'batch'; ids: string[] };
export type RequeueResult = {
status: 'success' | 'partial' | 'failed';
title: string;
message: string;
failures?: string[];
};
function formatLocalDate(value: Date) {
return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`;
}
export function recentSevenDays(): DateRangeValue {
const end = new Date();
const start = new Date(end);
start.setDate(end.getDate() - 6);
return { start: formatLocalDate(start), end: formatLocalDate(end) };
}
function attemptStatusTone(status: string) {
if (status === 'acknowledged') return 'success' as const;
if (status === 'rejected' || status === 'failed') return 'danger' as const;
if (status === 'awaiting_ack') return 'info' as const;
return 'neutral' as const;
}
export function DeliveryDetailModal({ record, onClose }: { record: DownstreamDeliveryRecord; onClose: () => void }) {
const payloadText = useMemo(() => JSON.stringify(record.payload ?? {}, null, 2), [record.payload]);
return (
<Modal
open
onClose={onClose}
size="xl"
title={<div className="template-modal-title"><h2></h2><p>{record.id}</p></div>}
footer={<Button onClick={onClose}></Button>}
>
<div className="report-record-detail">
<div className="detail-grid">
<div><span></span><strong>{record.tenant?.name ?? record.tenantId}</strong></div>
<div><span></span><strong>{record.application?.name ?? record.applicationId}</strong></div>
<div><span></span><strong>{deliveryTypeLabel[record.deliveryType] ?? record.deliveryType}</strong></div>
<div><span></span><strong>{deliveryStatusLabel(record)}</strong></div>
<div><span> ID</span><strong>{record.messageId ?? '-'}</strong></div>
<div><span></span><strong>{record.retryCount}</strong></div>
<div><span></span><strong>{record.manualRetryCount}</strong></div>
<div><span></span><strong>{record.lastRetriedAt ? formatDateTime(record.lastRetriedAt) : '-'}</strong></div>
<div><span></span><strong>{record.nextRetryAt ?? '-'}</strong></div>
<div><span></span><strong>{record.retryEnabled ? '开启' : '关闭'}</strong></div>
<div><span></span><strong>{record.sentAt ?? '-'}</strong></div>
<div><span></span><strong>{record.acknowledgedAt ?? '-'}</strong></div>
<div><span>ACK Result</span><strong>{record.ackResult ?? '-'}</strong></div>
<div><span>ACK Sequence_Id</span><strong>{record.ackSequenceId ?? '-'}</strong></div>
<div><span>ACK Msg_Id</span><strong>{record.ackMessageId ?? '-'}</strong></div>
<div><span> ID</span><strong>{record.connectionId ?? '-'}</strong></div>
<div className="detail-grid__wide"><span></span><strong>{record.lastError ?? '-'}</strong></div>
</div>
<section className="report-history">
<h3></h3>
<div className="downstream-attempt-timeline" aria-label="逐次投递记录">
{(record.attempts ?? []).map((attempt) => (
<article className="downstream-attempt-card" key={attempt.id}>
<div className={`downstream-attempt-marker downstream-attempt-marker--${attemptStatusTone(attempt.status)}`}>
{attempt.attemptNo}
</div>
<div className="downstream-attempt-card__body">
<header>
<strong> {attempt.attemptNo} </strong>
<Tag tone={attemptStatusTone(attempt.status)}>{attemptStatusLabel[attempt.status] ?? attempt.status}</Tag>
</header>
<dl className="downstream-attempt-card__times">
<div><dt></dt><dd>{attempt.sentAt ? formatDateTime(attempt.sentAt) : '-'}</dd></div>
<div><dt>ACK </dt><dd>{attempt.acknowledgedAt ? formatDateTime(attempt.acknowledgedAt) : '-'}</dd></div>
<div><dt>ACK </dt><dd>{attempt.ackDeadlineAt ? formatDateTime(attempt.ackDeadlineAt) : '-'}</dd></div>
</dl>
<dl className="downstream-attempt-card__identifiers">
<div><dt> ID</dt><dd>{attempt.connectionId ?? '-'}</dd></div>
<div><dt>Sequence_Id</dt><dd>{attempt.sequenceId ?? '-'}</dd></div>
<div><dt>Msg_Id</dt><dd>{attempt.messageId ?? '-'}</dd></div>
</dl>
<div className={`downstream-attempt-result${attempt.errorMessage || attempt.failureType ? ' downstream-attempt-result--error' : ''}`}>
<span>ACK Result{attempt.ackResult ?? '-'}</span>
<strong>{attempt.errorMessage ?? attempt.failureType ?? '本次投递未记录异常'}</strong>
</div>
</div>
</article>
))}
{(record.attempts ?? []).length === 0 ? <p></p> : null}
</div>
</section>
<section className="report-history">
<h3>Payload</h3>
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', margin: 0 }}>{payloadText}</pre>
</section>
</div>
</Modal>
);
}
@@ -2,7 +2,8 @@ import { useCallback, useEffect, useMemo, useState, type ComponentProps } from '
import type { EChartsOption } from 'echarts';
import { AlertTriangle, Ban, BellRing, RefreshCw, Settings2, ShieldCheck, ShieldOff } from 'lucide-react';
import { adminApi, type SecurityAlert, type SecurityBlock, type SecurityOverview, type SecurityProtectedNetwork, type SecurityRule } from '@/api/adminApi';
import { Breadcrumb, Button, Chart, Input, Modal as BaseModal, Select, Table, Tabs, Tag, Textarea, type TableColumn } from '@/components/ui';
import { Breadcrumb, Button, Input, Modal as BaseModal, Select, Table, Tabs, Tag, Textarea, type TableColumn } from '@/components/ui';
import { Chart } from '@/components/ui/Chart';
import './AdminSecurityDetectionPage.css';
const statusLabels: Record<string, string> = { open: '待处理', acknowledged: '已确认', blocked: '已封禁', block_failed: '封禁失败', ignored: '已忽略', requested: '执行中', failed: '失败', released: '已解封' };
@@ -24,7 +24,8 @@ import {
type InfrastructureMonitoringOverview,
type InfrastructureMonitoringRange,
} from '@/api/adminApi';
import { Breadcrumb, Button, Chart, Input, Modal, Table, Tag, type TableColumn } from '@/components/ui';
import { Breadcrumb, Button, Input, Modal, Table, Tag, type TableColumn } from '@/components/ui';
import { Chart } from '@/components/ui/Chart';
import './AdminSystemMonitoringPage.css';
const RANGE_OPTIONS: Array<{ value: InfrastructureMonitoringRange; label: string }> = [
+2 -1
View File
@@ -10,7 +10,8 @@ import {
WalletCards,
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { Button, Chart, Table, Tag, type TableColumn } from '@/components/ui';
import { Button, Table, Tag, type TableColumn } from '@/components/ui';
import { Chart } from '@/components/ui/Chart';
import { clientApi, type DashboardResponse } from '@/api/adminApi';
import { createLineOption, createPieOption } from '@/theme/chartOptions';
import { formatDateTime } from '@/utils/dateTime';