|
|
/**
|
|
|
* 客户级承运商官网登录账密 — 密码加密落库,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 });
|
|
|
}
|