/** * 客户级承运商官网登录账密 — 密码加密落库,Worker 运行时解密 */ import { prisma } from "@/lib/prisma"; import { decryptSecret, encryptSecret, } from "@/modules/customer/token-crypto"; export type ProviderCredentialProvider = "mothership" | "flock"; export type ProviderLogin = { email: string; password: string; }; export type ProviderCredentialDto = { provider: ProviderCredentialProvider; email: string | null; has_password: boolean; updated_at: string | null; }; /** 管理端查看:回传完整邮箱与解密后的密码(仅 GET) */ export type ProviderCredentialAdminViewDto = ProviderCredentialDto & { password: string | null; }; const PROVIDERS: ProviderCredentialProvider[] = ["mothership", "flock"]; export function isProviderCredentialProvider( raw: unknown, ): raw is ProviderCredentialProvider { return raw === "mothership" || raw === "flock"; } function maskEmail(email: string): string { const at = email.indexOf("@"); if (at <= 1) { return "***"; } return `${email.slice(0, 2)}***${email.slice(at)}`; } export function serializeProviderCredentialDto(row: { provider: string; loginEmail: string; passwordCiphertext: string; updatedAt: Date; isDeleted: boolean; }): ProviderCredentialDto { return { provider: row.provider as ProviderCredentialProvider, email: row.isDeleted ? null : maskEmail(row.loginEmail), has_password: !row.isDeleted && Boolean(row.passwordCiphertext), updated_at: row.isDeleted ? null : row.updatedAt.toISOString(), }; } export function serializeProviderCredentialAdminView(row: { provider: string; loginEmail: string; passwordCiphertext: string; updatedAt: Date; isDeleted: boolean; }): ProviderCredentialAdminViewDto { if (row.isDeleted) { return { provider: row.provider as ProviderCredentialProvider, email: null, password: null, has_password: false, updated_at: null, }; } const password = decryptSecret(row.passwordCiphertext); const email = row.loginEmail.trim() || null; return { provider: row.provider as ProviderCredentialProvider, email, password: password && !password.startsWith("cleared:") ? password : null, has_password: Boolean(email && password && !password.startsWith("cleared:")), updated_at: row.updatedAt.toISOString(), }; } export async function listCustomerProviderCredentials( customerId: string, ): Promise { const rows = await prisma.customerProviderCredential.findMany({ where: { customerId, isDeleted: false }, }); const byProvider = new Map(rows.map((r) => [r.provider, r])); return PROVIDERS.map((provider) => { const row = byProvider.get(provider); if (!row) { return { provider, email: null, has_password: false, updated_at: null, }; } return serializeProviderCredentialDto(row); }); } /** 管理端查看账密:完整邮箱 + 明文密码 */ export async function listCustomerProviderCredentialsForAdmin( customerId: string, ): Promise { const rows = await prisma.customerProviderCredential.findMany({ where: { customerId, isDeleted: false }, }); const byProvider = new Map(rows.map((r) => [r.provider, r])); return PROVIDERS.map((provider) => { const row = byProvider.get(provider); if (!row) { return { provider, email: null, password: null, has_password: false, updated_at: null, }; } return serializeProviderCredentialAdminView(row); }); } /** Worker 用:解密后的明文登录(无记录返回 null) */ export async function getCustomerProviderLogin( customerId: string, provider: ProviderCredentialProvider, ): Promise { const row = await prisma.customerProviderCredential.findUnique({ where: { customerId_provider: { customerId, provider }, }, }); if (!row || row.isDeleted) { return null; } const password = decryptSecret(row.passwordCiphertext); if (!password || !row.loginEmail.trim()) { return null; } return { email: row.loginEmail.trim(), password }; } /** 库中是否已配置该承运商账密(不论能否解密) */ export async function hasCustomerProviderCredential( customerId: string, provider: ProviderCredentialProvider, ): Promise { const row = await prisma.customerProviderCredential.findUnique({ where: { customerId_provider: { customerId, provider }, }, select: { isDeleted: true, passwordCiphertext: true, loginEmail: true }, }); return Boolean( row && !row.isDeleted && row.loginEmail.trim() && row.passwordCiphertext, ); } export async function upsertCustomerProviderCredential( customerId: string, provider: ProviderCredentialProvider, input: { email: string; password?: string; updatedBy?: string }, ): Promise { const email = input.email.trim(); if (!email || !email.includes("@")) { throw new Error("登录邮箱无效"); } const existing = await prisma.customerProviderCredential.findUnique({ where: { customerId_provider: { customerId, provider } }, }); const passwordTrimmed = input.password?.trim() ?? ""; if (!existing && !passwordTrimmed) { throw new Error("首次配置必须填写密码"); } if (existing && !existing.isDeleted && !passwordTrimmed) { const row = await prisma.customerProviderCredential.update({ where: { customerId_provider: { customerId, provider } }, data: { loginEmail: email, updatedBy: input.updatedBy ?? null, isDeleted: false, }, }); return serializeProviderCredentialDto(row); } const ciphertext = encryptSecret(passwordTrimmed); const row = await prisma.customerProviderCredential.upsert({ where: { customerId_provider: { customerId, provider } }, create: { customerId, provider, loginEmail: email, passwordCiphertext: ciphertext, updatedBy: input.updatedBy ?? null, isDeleted: false, }, update: { loginEmail: email, passwordCiphertext: ciphertext, updatedBy: input.updatedBy ?? null, isDeleted: false, }, }); return serializeProviderCredentialDto(row); } export async function clearCustomerProviderCredential( customerId: string, provider: ProviderCredentialProvider, updatedBy?: string, ): Promise { const existing = await prisma.customerProviderCredential.findUnique({ where: { customerId_provider: { customerId, provider } }, }); if (!existing) { return { provider, email: null, has_password: false, updated_at: null, }; } const row = await prisma.customerProviderCredential.update({ where: { customerId_provider: { customerId, provider } }, data: { isDeleted: true, updatedBy: updatedBy ?? null, passwordCiphertext: encryptSecret(`cleared:${Date.now()}`), }, }); return serializeProviderCredentialDto({ ...row, isDeleted: true }); }