Current Behavior
In workflow.service.ts, the advanceStep method accepts a performedBy parameter and writes it directly into the stepHistory JSON stored in the DB — with zero validation that the userId actually exists:
history.push({
step: instance.currentStep,
action,
note,
performedBy, // ← written raw, never checked against DB
performedAt: new Date().toISOString(),
});
This means any caller can pass performedBy: 99999 (a non-existent or deleted user) and it gets permanently stored as a trusted audit trail entry. The step history is supposed to track who took an action — but right now it can silently contain ghost user IDs.
Suggested Improvement
Before pushing to history, validate that the performedBy userId actually exists in the user table. If it doesn't, either throw an error or store null instead of the invalid ID.
Benefits
- 🛡️ Prevents corrupt audit trail entries with non-existent user IDs
- 🔍 Makes
stepHistory trustworthy for admin reviews and compliance
- 🧹 Zero breaking changes — just adds a guard before the existing
history.push()
- 📋 Consistent with how other services (e.g.
reimbursement.service.ts) verify entity existence before acting
Possible Implementation
workflow.service.ts
async advanceStep(
id: number,
action: string,
note?: string | undefined,
performedBy?: number | undefined,
) {
const instance = await prisma.workflowInstance.findUnique({
where: { id },
include: { definition: true },
});
if (!instance) throw new Error("Workflow instance not found");
if (instance.status !== "ACTIVE") throw new Error("Workflow is not active");
// ✅ Validate performedBy user exists
let validatedPerformedBy: number | undefined = undefined;
if (performedBy !== undefined) {
const userExists = await prisma.user.findUnique({
where: { id: performedBy },
select: { id: true },
});
if (!userExists) {
throw new Error(`User with id ${performedBy} does not exist`);
}
validatedPerformedBy = performedBy;
}
const steps = JSON.parse(instance.definition.steps as string) as unknown[];
const history = JSON.parse(instance.stepHistory as string) as StepHistoryEntry[];
history.push({
step: instance.currentStep,
action,
note,
performedBy: validatedPerformedBy, // ✅ only trusted IDs stored
performedAt: new Date().toISOString(),
});
const nextStep = instance.currentStep + 1;
const isComplete = action === "REJECT" || nextStep >= steps.length;
return prisma.workflowInstance.update({
where: { id },
data: {
currentStep: isComplete ? instance.currentStep : nextStep,
status:
action === "REJECT"
? "CANCELLED"
: isComplete
? "COMPLETED"
: "ACTIVE",
stepHistory: JSON.stringify(history),
},
});
}
Current Behavior
In
workflow.service.ts, theadvanceStepmethod accepts aperformedByparameter and writes it directly into thestepHistoryJSON stored in the DB — with zero validation that the userId actually exists:This means any caller can pass
performedBy: 99999(a non-existent or deleted user) and it gets permanently stored as a trusted audit trail entry. The step history is supposed to track who took an action — but right now it can silently contain ghost user IDs.Suggested Improvement
Before pushing to
history, validate that theperformedByuserId actually exists in theusertable. If it doesn't, either throw an error or storenullinstead of the invalid ID.Benefits
stepHistorytrustworthy for admin reviews and compliancehistory.push()reimbursement.service.ts) verify entity existence before actingPossible Implementation
workflow.service.ts