fix: 完善回单失败引导及真实认证验证(09-08-reimbursement-payment-closeout)
这个提交包含在:
@@ -22,6 +22,7 @@ const docker = (args, input) => {
|
||||
|
||||
async function main() {
|
||||
assert.equal(process.env.RUN_ISOLATED_DB_TEST, "1");
|
||||
const realAuthentication = process.env.RUN_RECEIPT_REAL_AUTH === "1";
|
||||
let container;
|
||||
let prisma;
|
||||
let pool;
|
||||
@@ -428,20 +429,29 @@ async function main() {
|
||||
const app = serverRequire("fastify")();
|
||||
app.decorate("prisma", prisma);
|
||||
app.decorate("config", { ...config, paymentBatchEnabled: true, appTimezone: "Asia/Shanghai",
|
||||
erpSyncEnabled: false, emailEnabled: false, imNotifierEnabled: false, appOrigin: "http://127.0.0.1" });
|
||||
app.decorate("authenticate", async (request) => {
|
||||
const actorId = request.headers.authorization === "Bearer synthetic-finance" ? employee.id :
|
||||
request.headers.authorization === "Bearer synthetic-reviewer" ? reviewer.id : null;
|
||||
if (!actorId) throw new AppError({ code: "AUTH_001", message: "未登录", statusCode: 401 });
|
||||
const actor = await prisma.employee.findUnique({ where: { id: actorId }, include: { roles: true } });
|
||||
request.employee = { ...actor, roleCodes: actor.roles.map((role) => role.role_code) };
|
||||
});
|
||||
app.decorate("hasPermission", (actor, permission) => hasPermission(resolvePermissionsForRoles(actor.roleCodes), permission));
|
||||
erpSyncEnabled: false, emailEnabled: false, imNotifierEnabled: false, appOrigin: "http://127.0.0.1",
|
||||
jwtSecret: randomUUID(), accessTokenTtl: "15m", refreshTtlSeconds: 3600,
|
||||
refreshCookieName: "synthetic_refresh", cookieSecure: false,
|
||||
loginFailMaxAttempts: 5, loginFailWindowSeconds: 300, loginLockSeconds: 60 });
|
||||
if (realAuthentication) {
|
||||
await app.register(serverRequire("@fastify/cookie"));
|
||||
await app.register(serverRequire("./src/plugins/auth"));
|
||||
await app.register(serverRequire("./src/routes/auth"), { prefix: "/api/auth" });
|
||||
} else {
|
||||
app.decorate("authenticate", async (request) => {
|
||||
const actorId = request.headers.authorization === "Bearer synthetic-finance" ? employee.id :
|
||||
request.headers.authorization === "Bearer synthetic-reviewer" ? reviewer.id : null;
|
||||
if (!actorId) throw new AppError({ code: "AUTH_001", message: "未登录", statusCode: 401 });
|
||||
const actor = await prisma.employee.findUnique({ where: { id: actorId }, include: { roles: true } });
|
||||
request.employee = { ...actor, roleCodes: actor.roles.map((role) => role.role_code) };
|
||||
});
|
||||
app.decorate("hasPermission", (actor, permission) => hasPermission(resolvePermissionsForRoles(actor.roleCodes), permission));
|
||||
}
|
||||
app.setErrorHandler((error, request, reply) => reply.code(error instanceof AppError ? error.statusCode : 500)
|
||||
.send({ error: { code: error.code ?? "unexpected", details: error instanceof AppError ? error.details : undefined } }));
|
||||
await app.register(serverRequire("./src/routes/paymentBatches"), { prefix: "/api" });
|
||||
const financeHeaders = { authorization: "Bearer synthetic-finance" };
|
||||
const reviewerHeaders = { authorization: "Bearer synthetic-reviewer" };
|
||||
let financeHeaders = { authorization: "Bearer synthetic-finance" };
|
||||
let reviewerHeaders = { authorization: "Bearer synthetic-reviewer" };
|
||||
async function readyHttp(context) {
|
||||
await prisma.paymentBatch.update({ where: { id: context.batch.id }, data: {
|
||||
reviewed_by: reviewer.id, reviewed_at: new Date(), default_payment_channel: "bank",
|
||||
@@ -449,6 +459,30 @@ async function main() {
|
||||
return `/api/payment-batches/${context.batch.id}`;
|
||||
}
|
||||
try {
|
||||
if (realAuthentication) {
|
||||
const loginPassword = `Synthetic!${randomUUID()}`;
|
||||
const passwordHash = await serverRequire("./src/utils/crypto").hashPassword(loginPassword);
|
||||
await prisma.employee.updateMany({ where: { id: { in: [employee.id, reviewer.id] } },
|
||||
data: { password_hash: passwordHash, must_change_password: false } });
|
||||
async function loginActor(actorId) {
|
||||
const actor = await prisma.employee.findUniqueOrThrow({ where: { id: actorId } });
|
||||
const response = await app.inject({ method: "POST", url: "/api/auth/login",
|
||||
headers: { "x-device-fingerprint": `synthetic-${actorId}` },
|
||||
payload: { identifier: actor.email, password: loginPassword } });
|
||||
assert.equal(response.statusCode, 200, "synthetic password login failed");
|
||||
const token = response.json().data.accessToken;
|
||||
assert.equal(typeof token, "string");
|
||||
assert.ok(response.cookies.some(cookie => cookie.name === "synthetic_refresh"));
|
||||
return { authorization: `Bearer ${token}` };
|
||||
}
|
||||
financeHeaders = await loginActor(employee.id);
|
||||
reviewerHeaders = await loginActor(reviewer.id);
|
||||
passed.push("actual_password_login_jwt_and_refresh_cookie");
|
||||
const forged = await app.inject({ method: "GET", url: `/api/payment-batches/${randomUUID()}/receipt-readiness`,
|
||||
headers: { authorization: "Bearer synthetic-finance" } });
|
||||
assert.equal(forged.statusCode, 401);
|
||||
passed.push("synthetic_actor_header_rejected_by_real_authentication");
|
||||
}
|
||||
const httpManual = await manualFixture("SYN-HTTP-ORIGINAL");
|
||||
const httpBase = await readyHttp(httpManual);
|
||||
const httpProposalUrl = `${httpBase}/receipt-attachments/${httpManual.attachment.id}/manual-verification`;
|
||||
@@ -489,6 +523,12 @@ async function main() {
|
||||
assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: rollback.batch.items[0].entity_id } })).status, "approved");
|
||||
passed.push("real_db_http_source_amount_change_rolls_back_claim_alias_and_payment");
|
||||
} finally { await app.close(); }
|
||||
if (process.env.RUN_RECEIPT_BROWSER === "1") {
|
||||
assert.ok(realAuthentication);
|
||||
await require("./test-payment-receipt-browser.cjs")({
|
||||
root, prisma, config, employee, reviewer, manualFixture, serverRequire, passed,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
try { if (prisma) await prisma.$disconnect(); }
|
||||
finally {
|
||||
@@ -504,7 +544,9 @@ async function main() {
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify({ status: "PASS_ISOLATED_RECEIPT_DB", passed, cleanupComplete: true,
|
||||
productionAccess: false, cloudCalls: false, httpTested: true, authentication: "synthetic_actor_fixture" }));
|
||||
productionAccess: false, cloudCalls: false, httpTested: true,
|
||||
authentication: realAuthentication ? "actual_password_login_and_jwt" : "synthetic_actor_fixture",
|
||||
browserTested: process.env.RUN_RECEIPT_BROWSER === "1" }));
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
||||
在新工单中引用
屏蔽一个用户