Skip to content

[ENHANCEMENT]: validate performedBy user existence in WorkflowService.advanceStep #869

@Priyanshi-untitled

Description

@Priyanshi-untitled

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),
    },
  });
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions