You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

247 lines
7.0 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* 客户级承运商官网登录账密 — 密码加密落库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<ProviderCredentialDto[]> {
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<ProviderCredentialAdminViewDto[]> {
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<ProviderLogin | null> {
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<boolean> {
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<ProviderCredentialDto> {
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<ProviderCredentialDto> {
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 });
}