补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
391 行
12 KiB
Plaintext
391 行
12 KiB
Plaintext
/**
|
||
* OmniCard AI (CRMmp) - 名片主移动端 NFC 安全与远程挂失业务驱动层
|
||
* 管理 NFC 资产设备列表、在线/挂失冻结状态流转、防伪 UID 格式校验与安全哈希查验
|
||
* 遵循 AGENTS.md 规范与 uts 强类型约束
|
||
*/
|
||
|
||
import { NFCRecord } from '../../../common/types.uts';
|
||
import { freezeNfcDevice, unfreezeNfcDevice } from '../../../common/api.uts';
|
||
import { getCurrentLocale, getOmniText } from '../../../common/locale.uts';
|
||
|
||
export type SecurityDeviceStatus = 'active' | 'frozen';
|
||
|
||
export interface NfcSecurityDevice {
|
||
id: number;
|
||
uid: string;
|
||
cardId?: number | null;
|
||
status: SecurityDeviceStatus;
|
||
securityHash?: string;
|
||
batchNo?: string;
|
||
material?: string;
|
||
lastScannedAt?: string | null;
|
||
}
|
||
|
||
export interface SecurityState {
|
||
devices: NfcSecurityDevice[];
|
||
selectedId: number | null;
|
||
submitting: boolean;
|
||
errorMessage: string;
|
||
}
|
||
|
||
export interface SecurityApi {
|
||
freeze: (params: { id: number; uid?: string }) => Promise<unknown>;
|
||
unfreeze: (params: { id: number; uid?: string }) => Promise<unknown>;
|
||
}
|
||
|
||
export type SecurityCopyKey =
|
||
| 'title'
|
||
| 'desc'
|
||
| 'statusActive'
|
||
| 'statusFrozen'
|
||
| 'freezeBtn'
|
||
| 'unfreezeBtn'
|
||
| 'warningTitle'
|
||
| 'warningDesc'
|
||
| 'deviceEmpty'
|
||
| 'uidLabel'
|
||
| 'batchLabel'
|
||
| 'antiCounterfeitValid'
|
||
| 'antiCounterfeitInvalid'
|
||
| 'errorNotFound'
|
||
| 'errorAlreadyFrozen'
|
||
| 'errorNotFrozen'
|
||
| 'freezeSuccess'
|
||
| 'unfreezeSuccess'
|
||
| 'submitting'
|
||
| 'operationError'
|
||
| 'shieldTitleActive'
|
||
| 'shieldDescActive'
|
||
| 'shieldTitleFrozen'
|
||
| 'shieldDescFrozen'
|
||
| 'chipDetailsTitle'
|
||
| 'securityHashLabel'
|
||
| 'materialLabel'
|
||
| 'protocolLabel'
|
||
| 'policyPoint1'
|
||
| 'policyPoint2'
|
||
| 'policyPoint3'
|
||
| 'confirmFreezeTitle'
|
||
| 'confirmFreezeDesc'
|
||
| 'confirmUnfreezeTitle'
|
||
| 'confirmUnfreezeDesc'
|
||
| 'confirmBtn'
|
||
| 'cancelBtn'
|
||
| 'cardListTitle';
|
||
|
||
const DEFAULT_SALT = 'OMNICARD_SECURE_SALT_2026';
|
||
|
||
/**
|
||
* 规范化 UID:去除冒号、空格、连字符并转大写
|
||
*/
|
||
export function normalizeUid(raw: string | null | undefined): string {
|
||
if (raw == null) {
|
||
return '';
|
||
}
|
||
return String(raw).replace(/[\s:-]/g, '').toUpperCase();
|
||
}
|
||
|
||
/**
|
||
* 防伪 UID 格式校验:
|
||
* 严格校验十六进制字符,仅放行标准 4 字节(8 字符)或 7 字节(14 字符)NFC 芯片 UID
|
||
*/
|
||
export function validateNfcUid(raw: string | null | undefined): {
|
||
valid: boolean;
|
||
normalizedUid: string;
|
||
error?: string;
|
||
} {
|
||
const normalized = normalizeUid(raw);
|
||
if (!normalized) {
|
||
return {
|
||
valid: false,
|
||
normalizedUid: '',
|
||
error: 'UID 不能为空',
|
||
};
|
||
}
|
||
|
||
// 严格检查是否为纯十六进制字符
|
||
if (!/^[0-9A-F]+$/.test(normalized)) {
|
||
return {
|
||
valid: false,
|
||
normalizedUid: normalized,
|
||
error: 'UID 必须为有效十六进制格式',
|
||
};
|
||
}
|
||
|
||
// 校验长度:标准 4 字节 (8 字符) 或 7 字节 (14 字符)
|
||
if (normalized.length !== 8 && normalized.length !== 14) {
|
||
return {
|
||
valid: false,
|
||
normalizedUid: normalized,
|
||
error: 'UID 长度不符合标准 NFC 规范 (需为 8 或 14 字符)',
|
||
};
|
||
}
|
||
|
||
return {
|
||
valid: true,
|
||
normalizedUid: normalized,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 计算确定性防伪安全哈希码
|
||
* 基于 UID 与安全盐生成 32 位 Hex 防伪数字签名
|
||
*/
|
||
export function computeSecurityHash(
|
||
uid: string,
|
||
salt: string = DEFAULT_SALT
|
||
): string {
|
||
const cleanUid = normalizeUid(uid);
|
||
if (!cleanUid) {
|
||
return '';
|
||
}
|
||
const text = cleanUid + ':' + (salt || DEFAULT_SALT);
|
||
|
||
let h1 = 0x811c9dc5;
|
||
let h2 = 0x9e3779b9;
|
||
|
||
for (let i = 0; i < text.length; i++) {
|
||
const code = text.charCodeAt(i);
|
||
h1 = Math.imul(h1 ^ code, 0x01000193);
|
||
h2 = Math.imul(h2 ^ code, 0x5bd1e995);
|
||
h1 = (h1 << 13) | (h1 >>> 19);
|
||
h2 = (h2 << 15) | (h2 >>> 17);
|
||
}
|
||
|
||
const p1 = (h1 >>> 0).toString(16).padStart(8, '0');
|
||
const p2 = (h2 >>> 0).toString(16).padStart(8, '0');
|
||
const p3 = ((h1 ^ h2) >>> 0).toString(16).padStart(8, '0');
|
||
const p4 = ((h1 + h2) >>> 0).toString(16).padStart(8, '0');
|
||
|
||
return (p1 + p2 + p3 + p4).toUpperCase();
|
||
}
|
||
|
||
/**
|
||
* 查验防伪安全哈希是否匹配,防止物理克隆伪造
|
||
*/
|
||
export function verifySecurityHash(
|
||
uid: string,
|
||
hash: string,
|
||
salt: string = DEFAULT_SALT
|
||
): boolean {
|
||
if (!uid || !hash) {
|
||
return false;
|
||
}
|
||
const cleanUid = normalizeUid(uid);
|
||
if (!cleanUid) {
|
||
return false;
|
||
}
|
||
const expected = computeSecurityHash(cleanUid, salt);
|
||
return expected.toUpperCase() === String(hash).trim().toUpperCase();
|
||
}
|
||
|
||
/**
|
||
* 初始化状态对象
|
||
*/
|
||
export function createSecurityState(
|
||
initialDevices?: NfcSecurityDevice[]
|
||
): SecurityState {
|
||
const devices = initialDevices ? initialDevices.map((d) => ({ ...d })) : [];
|
||
const selectedId = devices.length > 0 ? devices[0].id : null;
|
||
return {
|
||
devices,
|
||
selectedId,
|
||
submitting: false,
|
||
errorMessage: '',
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 选中指定设备
|
||
*/
|
||
export function selectDevice(state: SecurityState, id: number): void {
|
||
state.selectedId = id;
|
||
state.errorMessage = '';
|
||
}
|
||
|
||
/**
|
||
* 远程挂失冻结设备 (active -> frozen)
|
||
*/
|
||
export async function freezeDevice(
|
||
state: SecurityState,
|
||
id: number,
|
||
api?: SecurityApi
|
||
): Promise<NfcSecurityDevice> {
|
||
const device = state.devices.find((d) => d.id === id);
|
||
if (!device) {
|
||
throw new Error('NFC 设备不存在');
|
||
}
|
||
|
||
if (device.status === 'frozen') {
|
||
throw new Error('设备已处于挂失冻结状态');
|
||
}
|
||
|
||
state.submitting = true;
|
||
state.errorMessage = '';
|
||
|
||
try {
|
||
if (api != null && typeof api.freeze === 'function') {
|
||
await api.freeze({ id, uid: device.uid });
|
||
} else {
|
||
await freezeNfcDevice({ id, uid: device.uid });
|
||
}
|
||
|
||
device.status = 'frozen';
|
||
return device;
|
||
} catch (err) {
|
||
const message = err instanceof Error ? err.message : '远程挂失失败';
|
||
state.errorMessage = message;
|
||
throw new Error(message);
|
||
} finally {
|
||
state.submitting = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 远程解除挂失冻结恢复 (frozen -> active)
|
||
*/
|
||
export async function unfreezeDevice(
|
||
state: SecurityState,
|
||
id: number,
|
||
api?: SecurityApi
|
||
): Promise<NfcSecurityDevice> {
|
||
const device = state.devices.find((d) => d.id === id);
|
||
if (!device) {
|
||
throw new Error('NFC 设备不存在');
|
||
}
|
||
|
||
if (device.status !== 'frozen') {
|
||
throw new Error('设备未处于挂失冻结状态');
|
||
}
|
||
|
||
state.submitting = true;
|
||
state.errorMessage = '';
|
||
|
||
try {
|
||
if (api != null && typeof api.unfreeze === 'function') {
|
||
await api.unfreeze({ id, uid: device.uid });
|
||
} else {
|
||
await unfreezeNfcDevice({ id, uid: device.uid });
|
||
}
|
||
|
||
device.status = 'active';
|
||
return device;
|
||
} catch (err) {
|
||
const message = err instanceof Error ? err.message : '解除挂失失败';
|
||
state.errorMessage = message;
|
||
throw new Error(message);
|
||
} finally {
|
||
state.submitting = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 国际化多语言文案字典解析
|
||
*/
|
||
export function resolveSecurityText(key: SecurityCopyKey, locale?: string): string {
|
||
const lang =
|
||
locale != null && String(locale).trim() !== ''
|
||
? String(locale).trim()
|
||
: getCurrentLocale();
|
||
const isZh = lang === 'zh-CN';
|
||
|
||
switch (key) {
|
||
case 'title':
|
||
return isZh ? 'NFC 安全与挂失' : 'NFC Security & Remote Lock';
|
||
case 'desc':
|
||
return isZh
|
||
? '物理名片丢失或遭遇异常,可立即远程挂失冻结,保护商业资产安全。'
|
||
: 'Remotely freeze lost cards instantly to prevent unauthorized access.';
|
||
case 'statusActive':
|
||
return isZh ? '正常使用中' : 'Active';
|
||
case 'statusFrozen':
|
||
return isZh ? '已挂失冻结' : 'Frozen';
|
||
case 'freezeBtn':
|
||
return isZh ? '远程挂失' : 'Freeze Card';
|
||
case 'unfreezeBtn':
|
||
return isZh ? '解除挂失恢复' : 'Unfreeze Card';
|
||
case 'warningTitle':
|
||
return isZh ? '安全保护机制' : 'Security Policy';
|
||
case 'warningDesc':
|
||
return isZh
|
||
? '挂失后将立即切断该实体 NFC 芯片的触碰跳转与数据传输。如已找回芯片,可在此安全解冻。'
|
||
: 'Freezing immediately halts touch response and data access on this physical card.';
|
||
case 'deviceEmpty':
|
||
return isZh ? '暂无名下 NFC 硬件设备' : 'No NFC hardware registered';
|
||
case 'uidLabel':
|
||
return isZh ? '芯片 UID' : 'Chip UID';
|
||
case 'batchLabel':
|
||
return isZh ? '出厂批次' : 'Batch No';
|
||
case 'antiCounterfeitValid':
|
||
return isZh ? '防伪验签通过' : 'Authenticity Verified';
|
||
case 'antiCounterfeitInvalid':
|
||
return isZh ? '防伪校验异常' : 'Verification Failed';
|
||
case 'errorNotFound':
|
||
return isZh ? 'NFC 设备不存在' : 'Device not found';
|
||
case 'errorAlreadyFrozen':
|
||
return isZh ? '设备已处于挂失冻结状态' : 'Device already frozen';
|
||
case 'errorNotFrozen':
|
||
return isZh ? '设备未处于挂失冻结状态' : 'Device is not frozen';
|
||
case 'freezeSuccess':
|
||
return isZh ? '设备已成功远程挂失冻结' : 'Card frozen successfully';
|
||
case 'unfreezeSuccess':
|
||
return isZh ? '设备已成功解冻恢复' : 'Card unfrozen successfully';
|
||
case 'submitting':
|
||
return isZh ? '处理中...' : 'Processing...';
|
||
case 'operationError':
|
||
return isZh ? '操作异常' : 'Something went wrong';
|
||
case 'shieldTitleActive':
|
||
return isZh ? '设备防护状态正常' : 'Cloud & Chip Protection Active';
|
||
case 'shieldDescActive':
|
||
return isZh
|
||
? '物理近场触碰正常联通,触碰即可即时唤起您的专属微官网。'
|
||
: 'Physical tap-to-share is active and securely linked.';
|
||
case 'shieldTitleFrozen':
|
||
return isZh ? '物理芯片已远程挂失冻结' : 'Hardware Frozen & Locked';
|
||
case 'shieldDescFrozen':
|
||
return isZh
|
||
? '已切断物理触碰与数据传输,微官网与访客雷达资产依然安全隔离。'
|
||
: 'Physical tap-to-share is blocked. Cloud portal data remains safe.';
|
||
case 'chipDetailsTitle':
|
||
return isZh ? '芯片防伪与技术参数' : 'Chip Authenticity & Technical Specs';
|
||
case 'securityHashLabel':
|
||
return isZh ? '防伪安全哈希' : 'Security Hash';
|
||
case 'materialLabel':
|
||
return isZh ? '物理材质' : 'Material';
|
||
case 'protocolLabel':
|
||
return isZh ? '通信协议' : 'Protocol';
|
||
case 'policyPoint1':
|
||
return isZh
|
||
? '立即阻断近场触碰响应,拾获者无法获取名片信息或盗用身份'
|
||
: 'Instantly halts physical tap-to-share to prevent identity theft';
|
||
case 'policyPoint2':
|
||
return isZh
|
||
? '云端微官网、商业资料与雷达轨迹数据不受影响,完全安全留存'
|
||
: 'Cloud portal data, assets, and radar traces remain intact';
|
||
case 'policyPoint3':
|
||
return isZh
|
||
? '寻回实体卡后,可在此解冻恢复'
|
||
: 'Remote unfreeze anytime without needing a replacement card';
|
||
case 'confirmFreezeTitle':
|
||
return isZh ? '确认远程挂失此实体卡?' : 'Freeze this physical card?';
|
||
case 'confirmFreezeDesc':
|
||
return isZh
|
||
? '挂失将立即阻断该实体卡的一切近场触碰功能,拾获者将无法读取任何数据。'
|
||
: 'Freezing will immediately halt all touch responses for this physical card.';
|
||
case 'confirmUnfreezeTitle':
|
||
return isZh ? '确认解除挂失恢复?' : 'Unfreeze and restore card?';
|
||
case 'confirmUnfreezeDesc':
|
||
return isZh
|
||
? '解除挂失后,该实体卡将立即恢复触碰唤起微官网与交换名片功能。'
|
||
: 'Unfreezing will restore tap-to-share and portal access immediately.';
|
||
case 'confirmBtn':
|
||
return isZh ? '确认执行' : 'Confirm';
|
||
case 'cancelBtn':
|
||
return isZh ? '取消' : 'Cancel';
|
||
case 'cardListTitle':
|
||
return isZh ? 'NFC 设备列表' : 'Registered NFC Cards';
|
||
default:
|
||
return '';
|
||
}
|
||
}
|