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.

127 lines
3.9 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.

import { cookies } from "next/headers";
import { fail, ok } from "@/lib/response";
import {
EMBED_DEMO_COOKIE,
verifyEmbedDemoToken,
} from "@/modules/embed-demo/auth";
import { isKnownCustomer } from "@/modules/auth/service-token-env";
import { isDbCustomerActive } from "@/modules/customer/customer-service";
import { writeAudit } from "@/modules/audit/service";
import {
isProviderCredentialProvider,
listCustomerProviderCredentialsForAdmin,
serializeProviderCredentialAdminView,
upsertCustomerProviderCredential,
} from "@/modules/customer/provider-credentials";
import { prisma } from "@/lib/prisma";
async function isEmbedCustomerAllowed(customerId: string): Promise<boolean> {
if (isKnownCustomer(customerId)) {
return true;
}
return isDbCustomerActive(customerId);
}
/** 从 Cookie 解析当前演示会话客户(越权由 sub 决定,禁止 body 指定 customer_id */
async function resolveEmbedCustomer(): Promise<
{ customerId: string } | { error: ReturnType<typeof fail> }
> {
const cookieStore = await cookies();
const raw = cookieStore.get(EMBED_DEMO_COOKIE)?.value;
if (!raw) {
return { error: fail("UNAUTHORIZED", "未登录", 401) };
}
let customerId: string;
try {
const payload = await verifyEmbedDemoToken(raw);
customerId = payload.sub;
} catch {
return { error: fail("UNAUTHORIZED", "演示会话无效或已过期", 401) };
}
if (!(await isEmbedCustomerAllowed(customerId))) {
return { error: fail("FORBIDDEN", "客户已停用", 403) };
}
return { customerId };
}
/** GET /api/embed-demo/provider-credentials — 明文回传邮箱+密码,便于用户核对准确性 */
export async function GET() {
const resolved = await resolveEmbedCustomer();
if ("error" in resolved) {
return resolved.error;
}
const list = await listCustomerProviderCredentialsForAdmin(resolved.customerId);
await writeAudit(
"embed:provider_credential_view",
`embed:${resolved.customerId}`,
resolved.customerId,
{ providers: list.filter((x) => x.has_password).map((x) => x.provider) },
);
return ok({ customer_id: resolved.customerId, list });
}
/** PUT /api/embed-demo/provider-credentials — 保存/修改当前客户承运商账密 */
export async function PUT(request: Request) {
const resolved = await resolveEmbedCustomer();
if ("error" in resolved) {
return resolved.error;
}
const { customerId } = resolved;
let body: unknown;
try {
body = await request.json();
} catch {
return fail("VALIDATION_FAILED", "请求体格式无效", 400);
}
const payload = body as {
provider?: unknown;
email?: unknown;
password?: unknown;
};
if (!isProviderCredentialProvider(payload.provider)) {
return fail("VALIDATION_FAILED", "provider 须为 mothership 或 flock", 400);
}
if (typeof payload.email !== "string" || !payload.email.trim()) {
return fail("VALIDATION_FAILED", "请填写登录邮箱", 400);
}
const password =
typeof payload.password === "string" ? payload.password : undefined;
try {
await upsertCustomerProviderCredential(customerId, payload.provider, {
email: payload.email,
password,
updatedBy: `embed:${customerId}`,
});
const row = await prisma.customerProviderCredential.findUnique({
where: {
customerId_provider: {
customerId,
provider: payload.provider,
},
},
});
if (!row) {
return fail("INTERNAL_ERROR", "保存后读取账密失败", 500);
}
const saved = serializeProviderCredentialAdminView(row);
await writeAudit(
"embed:provider_credential_upsert",
`embed:${customerId}`,
customerId,
{
provider: payload.provider,
email: saved.email,
password_updated: Boolean(password?.trim()),
},
);
return ok(saved);
} catch (error) {
const message = error instanceof Error ? error.message : "保存失败";
return fail("VALIDATION_FAILED", message, 400);
}
}