feat: 同步管理端、用户端与后端业务,并接通线上 Web 接口
补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
这个提交包含在:
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
NfcAntiTamperService,
|
||||
NfcSecurityAuditLog,
|
||||
NfcAuditLogListResult,
|
||||
NfcAuditLogRecordResult,
|
||||
} from '../../../src/modules/nfc/service/anti-tamper';
|
||||
|
||||
describe('Point 15: 实体 NFC 卡片动态防伪验签算法 (anti-tamper)', () => {
|
||||
@@ -280,28 +281,53 @@ describe('Point 15: 实体 NFC 卡片动态防伪验签算法 (anti-tamper)', ()
|
||||
});
|
||||
});
|
||||
|
||||
describe('8. 安全审计日志留痕', () => {
|
||||
it('拦截到恶意篡改、重放或非法请求时,应记录审计日志', () => {
|
||||
service.clearAuditLogs();
|
||||
|
||||
// 1. 触发一次篡改
|
||||
describe('8. 无持久化仓库时审计查询/记录不得伪装落库', () => {
|
||||
it('无 auditRepository 时 getAuditLogs 返回空列表,并标明 not_persisted / persisted:false', () => {
|
||||
// 触发验签失败,内存队列仍可入队,但对外查询不得当作已入库
|
||||
const token = service.generateDeviceToken(mockUid, mockCardId, mockSecret);
|
||||
service.verifyDeviceToken(mockUid, 9999, token, mockSecret);
|
||||
|
||||
// 2. 触发一次重放
|
||||
const validToken = service.generateDeviceToken(mockUid, mockCardId, mockSecret);
|
||||
service.verifyDeviceToken(mockUid, mockCardId, validToken, mockSecret);
|
||||
service.verifyDeviceToken(mockUid, mockCardId, validToken, mockSecret);
|
||||
const result: NfcAuditLogListResult = service.getAuditLogs();
|
||||
expect(result.logs).toEqual([]);
|
||||
expect(result.persisted).toBe(false);
|
||||
expect(result.not_persisted === true || result.persisted === false).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
const logs: NfcSecurityAuditLog[] = service.getAuditLogs();
|
||||
expect(logs.length).toBeGreaterThanOrEqual(2);
|
||||
it('无 auditRepository 时 recordAudit 不得返回成功落库', () => {
|
||||
const result: NfcAuditLogRecordResult = service.recordAudit(
|
||||
mockUid,
|
||||
mockCardId,
|
||||
'签名无效,检测到恶意参数篡改 (Invalid signature / Tampered)',
|
||||
'dummy.token'
|
||||
);
|
||||
|
||||
const tamperLog = logs.find(l => l.cardId === 9999);
|
||||
expect(tamperLog).toBeDefined();
|
||||
expect(tamperLog?.reason).toMatch(/签名无效|篡改/);
|
||||
expect(result.persisted).toBe(false);
|
||||
expect(result.not_persisted === true || result.persisted === false).toBe(
|
||||
true
|
||||
);
|
||||
// 不得出现已落库语义
|
||||
expect((result as { success?: boolean }).success).not.toBe(true);
|
||||
expect((result as { saved?: boolean }).saved).not.toBe(true);
|
||||
|
||||
const replayLog = logs.find(l => l.token === validToken && /重放|已使用/.test(l.reason));
|
||||
expect(replayLog).toBeDefined();
|
||||
const query = service.getAuditLogs();
|
||||
expect(query.logs).toEqual([]);
|
||||
expect(query.persisted).toBe(false);
|
||||
});
|
||||
|
||||
it('无库时内存防重放仍生效,但查询结果仍为空且未落库', () => {
|
||||
const token = service.generateDeviceToken(mockUid, mockCardId, mockSecret);
|
||||
expect(
|
||||
service.verifyDeviceToken(mockUid, mockCardId, token, mockSecret).valid
|
||||
).toBe(true);
|
||||
expect(
|
||||
service.verifyDeviceToken(mockUid, mockCardId, token, mockSecret).valid
|
||||
).toBe(false);
|
||||
|
||||
const result = service.getAuditLogs();
|
||||
expect(result.logs).toEqual([]);
|
||||
expect(result.persisted).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,10 @@ import {
|
||||
AdminNfcSecurityController,
|
||||
adminNfcSecurityCurdOption,
|
||||
} from '../../../src/modules/nfc/controller/admin/security';
|
||||
import {
|
||||
AUDIT_ACTION_NFC_FREEZE,
|
||||
BaseAuditLogService,
|
||||
} from '../../../src/modules/base/service/audit';
|
||||
|
||||
/**
|
||||
* CoolController 元数据(脚本路径解析为异步)
|
||||
@@ -114,12 +118,17 @@ class MemoryNfcDeviceRepo {
|
||||
}
|
||||
|
||||
function createService(
|
||||
seed: Array<Partial<NfcDeviceEntity>> = []
|
||||
seed: Array<Partial<NfcDeviceEntity>> = [],
|
||||
auditRecord?: jest.Mock
|
||||
): NfcSecurityService {
|
||||
const service = new NfcSecurityService();
|
||||
service.nfcDeviceEntity = new MemoryNfcDeviceRepo(
|
||||
seed
|
||||
) as unknown as NfcSecurityService['nfcDeviceEntity'];
|
||||
const auditService = new BaseAuditLogService();
|
||||
auditService.record = (auditRecord ||
|
||||
jest.fn(async dto => dto)) as unknown as BaseAuditLogService['record'];
|
||||
service.baseAuditLogService = auditService;
|
||||
return service;
|
||||
}
|
||||
|
||||
@@ -227,6 +236,43 @@ describe('T24 NFC 挂失冻结 API (nfc/security)', () => {
|
||||
CoolCommException
|
||||
);
|
||||
});
|
||||
|
||||
it('freeze 成功后应写入 nfc_freeze 审计(含操作者与 uid/设备 id)', async () => {
|
||||
const record = jest.fn(async dto => dto);
|
||||
const service = createService(seedDevices(), record);
|
||||
service.baseCtx = {
|
||||
admin: { userId: 42, username: 'SecAdmin' },
|
||||
} as unknown as NfcSecurityService['baseCtx'];
|
||||
|
||||
const frozen = await service.freeze({ uid: '04:7A:B2:8F:1C:6D:80' });
|
||||
|
||||
expect(frozen.status).toBe('suspended');
|
||||
expect(record).toHaveBeenCalledTimes(1);
|
||||
expect(record).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
module: 'nfc',
|
||||
action: AUDIT_ACTION_NFC_FREEZE,
|
||||
operatorId: 42,
|
||||
operatorName: 'SecAdmin',
|
||||
status: 1,
|
||||
})
|
||||
);
|
||||
const detail = JSON.parse(String(record.mock.calls[0][0].detail));
|
||||
expect(detail.uid).toBe('04:7A:B2:8F:1C:6D:80');
|
||||
expect(detail.deviceId).toBe(1);
|
||||
expect(detail).not.toHaveProperty('secret');
|
||||
expect(detail).not.toHaveProperty('phone');
|
||||
});
|
||||
|
||||
it('freeze 失败(设备不存在)时不得记成功审计', async () => {
|
||||
const record = jest.fn(async dto => dto);
|
||||
const service = createService(seedDevices(), record);
|
||||
|
||||
await expect(service.freeze({ id: 9999 })).rejects.toThrow(
|
||||
CoolCommException
|
||||
);
|
||||
expect(record).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. 后台控制器 /admin/nfc/security', () => {
|
||||
|
||||
在新工单中引用
屏蔽一个用户