Skip to content

Harden step completion integrity, tenant isolation, and API error handling#36

Merged
OlympusLedgerOrg merged 2 commits into
mainfrom
copilot/fix-step-service-audit-transaction
Apr 15, 2026
Merged

Harden step completion integrity, tenant isolation, and API error handling#36
OlympusLedgerOrg merged 2 commits into
mainfrom
copilot/fix-step-service-audit-transaction

Conversation

Copilot AI commented Apr 15, 2026

Copy link
Copy Markdown

This PR addresses four backend gaps with outsized operational risk: step completion was not tenant-scoped, step state and audit writes were not atomic, equipment work-center reads used an N+1 downtime query pattern, and unhandled backend errors were not normalized. It also closes the insecure production footgun where JWT handling silently fell back to change-me.

  • Step completion: enforce tenant isolation + atomic audit trail

    • Scope step lookup by tenantId in addition to workOrderId and stepId
    • Wrap step status update and audit log insert in a single Prisma transaction
    • Emit the completion socket event only after a successful transactional commit
    • Persist tenantId on the audit record for consistency with the rest of the backend
  • Equipment service: remove N+1 downtime queries

    • Replace per-equipment findFirst() downtime lookups with one batched findMany()
    • Index active downtime by equipmentId via Map
    • Keep response shape unchanged while reducing query count from 1 + N to 2
  • JWT configuration: centralize secret handling

    • Introduce a shared JWT config helper used by auth middleware, auth route, Socket.IO auth, and Prisma middleware
    • Fail fast in production when JWT_SECRET is missing or still set to the insecure default
  • Express error handling: normalize unexpected failures

    • Add a global error handler returning consistent JSON responses
    • Convert malformed JSON bodies to 400
    • Prevent raw internal error details from leaking on 500s
  • Test coverage

    • Extend step completion tests to assert transactional behavior and tenant-scoped lookup
    • Update equipment tests to verify batched downtime retrieval
    • Add coverage for JWT production guardrails and sanitized unexpected auth failures

Example of the step completion change:

const result = await prisma.$transaction(async (tx) => {
  const step = await tx.step.findFirst({
    where: {
      id: opts.stepId,
      workOrderId: opts.workOrderId,
      tenantId: opts.tenantId,
    },
  });

  const updated = await tx.step.update({
    where: { id: step.id },
    data: { status: 'COMPLETED', notes: opts.notes ?? step.notes },
  });

  await tx.auditLog.create({
    data: {
      tenantId: opts.tenantId,
      workOrderId: opts.workOrderId,
      stepId: opts.stepId,
      actorUserId: opts.actorUserId,
      action: 'STEP_COMPLETED',
    },
  });

  return updated;
});

Copilot AI and others added 2 commits April 15, 2026 01:34
Agent-Logs-Url: https://github.com/OlympusLedgerOrg/TiM/sessions/9aecc93e-3ae2-4b8e-83d9-bb84283d81ab

Co-authored-by: OlympusLedgerOrg <240737312+OlympusLedgerOrg@users.noreply.github.com>
Agent-Logs-Url: https://github.com/OlympusLedgerOrg/TiM/sessions/9aecc93e-3ae2-4b8e-83d9-bb84283d81ab

Co-authored-by: OlympusLedgerOrg <240737312+OlympusLedgerOrg@users.noreply.github.com>
@OlympusLedgerOrg OlympusLedgerOrg marked this pull request as ready for review April 15, 2026 08:52
Copilot AI review requested due to automatic review settings April 15, 2026 08:52
@OlympusLedgerOrg OlympusLedgerOrg merged commit de3560d into main Apr 15, 2026
6 of 8 checks passed
@OlympusLedgerOrg OlympusLedgerOrg deleted the copilot/fix-step-service-audit-transaction branch April 15, 2026 08:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Hardens core backend behaviors around multi-tenant integrity, atomic step completion/auditing, JWT secret handling, and consistent error responses—reducing cross-tenant risk and improving operational safety for production.

Changes:

  • Make step completion tenant-scoped and transactional (including audit logging) and emit sockets only after commit.
  • Remove the equipment downtime N+1 query by batching active downtime reads.
  • Centralize JWT secret derivation + add production guardrails, and add a global Express error handler that normalizes unexpected failures.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
backend/src/services/stepService.ts Tenant-scoped step lookup; wraps step update + audit insert in a Prisma transaction; emit after commit.
backend/src/controllers/stepController.ts Passes tenantId into step completion service.
backend/src/services/equipmentService.ts Replaces per-equipment downtime lookups with a single batched findMany + indexing.
backend/src/config/jwt.ts Introduces shared JWT secret helpers + production validation.
backend/src/app.ts Calls JWT validation on startup; uses shared JWT secret in Socket.IO auth; adds global error handler.
backend/src/routes/auth.ts Uses shared JWT secret when signing login tokens.
backend/src/middleware/auth.ts Uses shared JWT secret for request auth verification.
backend/prisma/src/middleware/auth.ts Switches Prisma-side auth middleware to shared JWT secret helper.
backend/tests/completeStep.test.ts Adds coverage for tenant-scoped lookup and transactional + audit behavior.
backend/tests/equipment.test.ts Updates mocks/assertions to validate batched downtime retrieval.
backend/tests/auth.test.ts Adds coverage asserting sanitized 500 response on unexpected auth failures.
backend/tests/jwtConfig.test.ts Adds tests for production JWT secret guardrails and non-prod allowances.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread backend/src/config/jwt.ts
}
}

export function getJwtSecret() {

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getJwtSecret() can still return the insecure default in production if some code path imports it without having called validateJwtSecret() (validation currently only happens in app.ts). Consider running validation inside getJwtSecret() (or at module initialization in config/jwt.ts) so the production guardrail can’t be bypassed by alternative entrypoints.

Suggested change
export function getJwtSecret() {
export function getJwtSecret() {
validateJwtSecret();

Copilot uses AI. Check for mistakes.
Comment thread backend/src/config/jwt.ts
Comment on lines +10 to +11
export function getJwtSecret() {
return new TextEncoder().encode(process.env.JWT_SECRET || INSECURE_DEFAULT_JWT_SECRET);

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getJwtSecret() allocates a new Uint8Array on every call (new TextEncoder().encode(...)). Since this runs per-request in requireAuth, consider memoizing the encoded secret (and optionally refreshing it only when JWT_SECRET changes) to avoid repeated allocations.

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +87
const downtimeByEquipment = new Map<string, typeof activeDowntimeEvents[0]>();
for (const event of activeDowntimeEvents) {
if (!downtimeByEquipment.has(event.equipmentId)) {
downtimeByEquipment.set(event.equipmentId, event);
}

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Map value type typeof activeDowntimeEvents[0] is hard to read and can behave unexpectedly with stricter TS options (e.g., noUncheckedIndexedAccess). Prefer a clearer element type like (typeof activeDowntimeEvents)[number] (or a Prisma DowntimeEvent type) for maintainability.

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +11
const originalEnv = process.env;

beforeEach(() => {
process.env = { ...originalEnv };
});

afterAll(() => {
process.env = originalEnv;

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test reassigns process.env (not just individual keys). Replacing the process.env object can cause subtle cross-test interference in a shared Jest worker; consider saving/restoring only JWT_SECRET/NODE_ENV (or using a helper) instead of overwriting process.env entirely.

Suggested change
const originalEnv = process.env;
beforeEach(() => {
process.env = { ...originalEnv };
});
afterAll(() => {
process.env = originalEnv;
const originalJwtSecret = process.env.JWT_SECRET;
const originalNodeEnv = process.env.NODE_ENV;
beforeEach(() => {
if (originalJwtSecret === undefined) {
delete process.env.JWT_SECRET;
} else {
process.env.JWT_SECRET = originalJwtSecret;
}
if (originalNodeEnv === undefined) {
delete process.env.NODE_ENV;
} else {
process.env.NODE_ENV = originalNodeEnv;
}
});
afterAll(() => {
if (originalJwtSecret === undefined) {
delete process.env.JWT_SECRET;
} else {
process.env.JWT_SECRET = originalJwtSecret;
}
if (originalNodeEnv === undefined) {
delete process.env.NODE_ENV;
} else {
process.env.NODE_ENV = originalNodeEnv;
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants