Skip to content

Fix #27: soft deletes for auditability#79

Open
TusharW4ni wants to merge 3 commits into
fix/33-adopt-migrationsfrom
fix/27-soft-deletes
Open

Fix #27: soft deletes for auditability#79
TusharW4ni wants to merge 3 commits into
fix/33-adopt-migrationsfrom
fix/27-soft-deletes

Conversation

@TusharW4ni

Copy link
Copy Markdown
Contributor

What was broken

DELETE endpoints for clients/volunteers/admins/rides hard-delete()d rows, destroying historical metrics and any chance of recovery. Issue #27 asks for soft deletes: archive records instead of destroying them, hide them from active views, but keep them for historical metrics.

What changed

  • Schema (prisma/schema/*): added nullable deletedAt DateTime? to Ride, User, Client, Volunteer, plus deletedEmail/deletedPhone on User. Migration 20260710035425_add_soft_deletes is purely additive — five ALTER TABLE ... ADD COLUMN, all nullable (safe on existing prod rows). Existing @unique/@@unique declarations kept as-is (no partial-index drift).
  • Delete endpoints → soft delete: each .delete() replaced with an .update() setting deletedAt. For clients/volunteers/admins we soft-delete the User and the profile row, and release the unique contact values — copy emaildeletedEmail, phonedeletedPhone, then null email/phone. SQLite allows many NULLs under a unique index, so the same email/phone is immediately reusable (decision Privilege Escalation via Unprotected Admin Creation API #2). Ride delete guards deletedAt: null so a second delete 404s. The client-with-rides #23 409 block is preserved.
  • Read-path filtering (audited every path): active views filter deletedAt: null; metrics endpoints deliberately do not (historical preservation). See the audit table below.
  • Blocked login (decision Complete Lack of Authentication on Core CRUD APIs #1): server/utils/auth.ts — the OTP-send hook now rejects archived users (findFirst({ where: { email, deletedAt: null }})). Since soft-delete also nulls the email, both the OTP-send and sign-in steps fail for an archived user, so the login flow can never complete.
  • Guards against acting on soft-deleted records: signup / unsignup / PUT ride / PUT client / PUT volunteer / ride estimate lookups all filter deletedAt: null.
  • Notifications & reminders: broadcastNotification and sendNotification skip archived volunteers; the reminder scan in scheduler.ts skips archived rides and archived volunteers.

Read-path audit

Path Decision
get/clients/index.ts HIDE (deletedAt: null)
get/volunteers/index.ts HIDE
get/admins/index.ts HIDE
get/users/index.ts HIDE
get/users/byId/[id] HIDE
get/users/byEmail/[email] HIDE
get/rides/index.ts HIDE (ANDed with #3 scoping)
get/rides/byId/[id] HIDE
get/rides/estimate/[id] HIDE
get/volunteers/bySession/index.ts HIDE
post/rides/[id]/signup.ts (ride lookup + atomic update) HIDE (can't sign up for archived ride)
post/rides/[id]/unsignup.ts HIDE
put/rides/[id].ts HIDE (can't edit/complete archived ride)
put/clients/[id].ts, put/volunteers/[id].ts HIDE (can't edit archived record)
utils/scheduler.ts (reminder scan) HIDE (ride + volunteer)
utils/notification.ts (sendNotification, broadcastNotification) HIDE (volunteer)
utils/auth.ts (login) HIDE / BLOCK
get/metrics/completionRate/index.ts KEEP — preserve historical rate
get/metrics/hours/index.ts KEEP — preserve historical hours
get/metrics/topRiders/index.ts (rides + client name lookup) KEEP — preserve history + resolve archived client names
get/addresses/index.ts n/a — Address is not soft-deletable
post/{clients,volunteers,admins} (user lookup by email) unchanged — released email is null, so lookup misses and a fresh user is created (this is what makes reuse work)

Verification

Regression test: tests/e2e/soft-deletes.test.ts (4 cases).

  1. archives a ride-less client … — client disappears from GET /api/get/clients but the row still exists with deletedAt set (direct better-sqlite3 read); email released to deletedEmail.
  2. unique reuse … — a new client with the same email as a soft-deleted one is created (no 409/500). Decision Privilege Escalation via Unprotected Admin Creation API #2.
  3. blocked login … — after archiving, send-verification-otp and sign-in/email-otp both fail. Decision Complete Lack of Authentication on Core CRUD APIs #1.
  4. metrics preserved … — a soft-deleted COMPLETED ride still counts in completionRate/hours.

Revert check (columns kept, behavioral source reverted to base): tests 1 and 4 fail pre-fix:

  • Test 1: AssertionError: client row must still exist (soft delete): expected undefined to be truthy (base hard-deletes the row).
  • Test 4: AssertionError: soft-deleted ride still counts toward total: expected 9 to be 10 (base hard-deletes the ride, dropping it from metrics).

Tests 2 and 3 pass on base too — hard-delete also frees the unique email and removes the user, so those decisions trivially hold; they exist to pin that soft-delete does not regress them.

Full suite: 30 files / 115 tests green (including the 4 new).

Risk files touched (flagged for human review)

Notes for the reviewer

Fixes #27

TusharW4ni and others added 3 commits July 9, 2026 23:06
Add deletedAt to Ride/User/Client/Volunteer (plus deletedEmail/deletedPhone
on User to preserve released contact values). DELETE endpoints now archive
rows instead of destroying them, releasing the unique email/phone so records
can be re-added. Active read paths filter deletedAt: null; metrics endpoints
deliberately do not, preserving historical totals. Soft-deleted users are
blocked from logging in.

Migration 20260710035425_add_soft_deletes is additive (nullable ADD COLUMN).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address code-review follow-ups: PUT clients/volunteers and the ride estimate
endpoint now treat an archived record as 404 (findFirst with deletedAt: null),
so soft-deleted rows can't be edited or re-surfaced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…kups (#27 review)

Address independent-review HIGH findings:
- Session revocation: hard delete used to cascade to the session table (auth
  onDelete: Cascade), logging the user out. Soft-delete now deletes the user's
  session rows in the same transaction (admins/clients/volunteers), so an
  archived user's live cookie no longer retains API access — closing the gap in
  the decision-#1 login block.
- Filter deletedAt: null on the volunteer lookups that resolve by userId from a
  session (signup, unsignup, bySession status/reminders/notifications) so a
  stale session can't act as an archived volunteer.
- Guard the soft-delete update where-clauses (clients/volunteers) with
  deletedAt: null so a double-delete can't overwrite deletedEmail/deletedPhone.

New regression test: "revokes sessions on soft-delete" — a previously valid
volunteer cookie is rejected with 401 on GET /api/get/rides after the volunteer
is archived, and no session rows remain. Fails pre-fix (expected 200 to be 401).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@TusharW4ni TusharW4ni added the needs-human-review Automated gate declined to auto-merge — see PR comment for which gates failed label Jul 10, 2026
@TusharW4ni

Copy link
Copy Markdown
Contributor Author

🚩 FLAGGED for human review — schema + migration + auth risk files (feature verified correct & complete)

Built per the owner's decisions (block login for soft-deleted users; solve unique-reuse now). Went through full /issue-loop verification with an independent reviewer + two /review finders (read-path completeness + auth/unique/delete correctness). The first pass found two real HIGH-severity gaps, which the author then fixed and I re-verified independently. Flagged only for the mandatory schema/migration/auth risk-file review.

Stacked on #76 (base = fix/33-adopt-migrations) — merge after #76.

HIGH-severity gaps found and fixed:

  1. Unrevoked sessions — soft-delete replaced a hard delete that used to cascade to the session table, so an archived user's live cookie kept full API access (defeating the login block). Fixed: all 3 user-soft-deleting endpoints now session.deleteMany({ where: { userId } }) transactionally. Proven load-bearing by a revert-check (remove the line → archived cookie accepted 200 instead of 401).
  2. Unfiltered volunteer lookupssignup/unsignup + the 3 bySession PUTs resolved the volunteer by userId without deletedAt, so an archived volunteer with a live session could self-assign/act. Fixed: all 5 now findFirst({ where: { userId, deletedAt: null } }).

Final gate results (post-fix):

  • Suite (reviewer-run): green — 30 files / 116 tests.
  • Revert-check: PASS for both the original soft-delete behavior (row persists with deletedAt; soft-deleted ride still counts in metrics) and the new session-revocation test.
  • Migration: 6 additive nullable ADD COLUMN (deletedAt on ride/user/client/volunteer + deletedEmail/deletedPhone on user) — safe on prod rows; applies cleanly on the Prisma Migrations Are Gitignored — No Reproducible Schema History #33 baseline; schema-consistent.
  • Read-path completeness: every active-view read filters deletedAt: null (rosters, byId/byEmail, ride list+scoping, signup/unsignup/PUT lookups, bySession, notifications, reminders, login). Metrics (completionRate/hours/topRiders) intentionally do NOT filter — preserving historical metrics is the whole point of the issue.
  • Unique-reuse: on delete, email/phone are moved to deletedEmail/deletedPhone and nulled, freeing the unique slots so a same-email re-add creates a fresh user (no partial-index/Prisma-drift needed). Verified by test.
  • Scope: 33 files, net non-test +172. Risk files: prisma/schema/* + migration + server/utils/auth.ts. No CI/docker/deps.

Note (intentional, no change): ride detail/list includes still show an archived client/volunteer on a completed ride — historical record, by design (deletes detach volunteers from ASSIGNED rides and block client-delete while active rides exist).

Automated /issue-loop verification (find → fix → re-verify). Flagged per the risk-file rule; feature verified correct, complete, and safe.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-human-review Automated gate declined to auto-merge — see PR comment for which gates failed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant