Skip to content

timeentry: inverted teStartedAt/teEndedAt is silently accepted with teMinutes=null #129

@CryptoJones

Description

@CryptoJones

Problem

POST /v1/timeentry and PATCH /v1/timeentry/:id accept a body where teEndedAt is strictly before teStartedAt. The controller's computeMinutes helper notices the inversion and short-circuits to teMinutes = null, but no 400 is returned — the row is created/updated with both bounds intact and the duration column blank. From the client's perspective the request succeeded; from the database's perspective it now has a time entry where the worker clocked out before clocking in.

// app/controllers/timeentrycontroller.js
function computeMinutes(startedAt, endedAt) {
    if (!startedAt || !endedAt) return null;
    const start = new Date(startedAt).getTime();
    const end = new Date(endedAt).getTime();
    if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return null;
    return Math.round((end - start) / 60000);
}

Proposed fix

Add a zod .refine() cross-field check on both createTimeEntryBody and updateTimeEntryBody in app/schemas/timeentry.schema.js. When both bounds are present in the same request body, reject with a 400 if teEndedAt < teStartedAt. Equality (zero-minute entries) stays allowed — that's a valid edge case, not a bug.

The single-bound PATCH case (PATCH with only teEndedAt, validated against the row's existing teStartedAt) can't be enforced at the schema layer without a DB read. Leave that path on the controller layer for a follow-up.

Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions