Skip to content

#387 Add an Account Deactivated Screen and Admin Reactivation - #500

Merged
cielbellerose merged 7 commits into
devfrom
387-add-account-deactivated-screen-and-admin-reactivation
Aug 19, 2026
Merged

#387 Add an Account Deactivated Screen and Admin Reactivation#500
cielbellerose merged 7 commits into
devfrom
387-add-account-deactivated-screen-and-admin-reactivation

Conversation

@cielbellerose

Copy link
Copy Markdown
Collaborator

Closes #387

Summary

  • A deactivated user with a still-valid session used to be bounced back to /login forever with no explanation. getCurrentUser now routes that specific case to a dedicated /login/deactivated screen instead.
  • Admins can now reactivate a deactivated account from a new Users → Deactivated accounts page — previously deactivation was one-way.

Changes

  • lib/auth/server.ts — extracted a cached resolveSessionUserRow(); added getDeactivatedSessionUser(); getCurrentUser redirects a deactivated live session to /login/deactivated (routing, not denial).
  • app/login/deactivated/page.tsx + components/features/deactivated-sign-out.tsx — the explanatory screen with a sign-out escape and a link to browse open positions.
  • app/login/page.tsx — bounces a deactivated session to /login/deactivated instead of rendering the sign-in form.
  • prisma/actions/auth.tssignOutDeactivatedSession(), since signOutUser's own getCurrentUser() call would redirect a deactivated caller away before it could sign them out.
  • prisma/data/users.ts / lib/types.tsgetDeactivatedUsersForAdmin() / AdminDeactivatedUserListItem.
  • prisma/actions/users.tsreactivateUser() (admin-gated, scoped to deletedAt: { not: null } so a double-submit is a no-op); createUser's duplicate-email error now points admins at reactivation when the existing row is soft-deleted.
  • app/(main)/(auth)/users/deactivated/{page,loading}.tsx + components/features/deactivated-users-table.tsx — admin-only listing with search, sortable columns, and a reactivate confirm dialog.
  • app/(main)/(auth)/users/page.tsx — "Deactivated accounts" header link.
  • components/features/users-table.tsx — deactivate dialog copy no longer claims the action is irreversible.
  • tests/db/authorization.test.ts / tests/stubs/auth-server.tsreactivateUser authorization coverage.

No Prisma schema change — deletedAt/deletedById/updatedById already existed on User.

Testing plan

  • As admin, deactivate a test user from /users → toast, row disappears from /users.
  • Open Deactivated accounts → the user is listed with the correct date and "Deactivated by"; search filters by name/email; both column sorts work.
  • Reactivate → confirm dialog → toast "User reactivated." → row disappears from the deactivated list and reappears on /users; DB shows deletedAt/deletedById null and updatedById set to the reactivating admin.
  • Empty state: reactivate everyone → "No deactivated accounts"; search with no match → "No accounts match your search."
  • Live-session lockout: sign in as a user in a second browser profile, deactivate that user as admin, then refresh any gated page in the second profile → lands on /login/deactivated (not the sign-in form), showing that user's own email.
  • From /login/deactivated: Browse open positions works; Sign out toasts "Signed out." and lands on /login; signing in again with that email shows "Your account has been deactivated. Please contact an administrator."
  • Reactivate that user, then sign in again in the second profile → normal access restored.
  • Direct-navigate to /login/deactivated while signed in normally → redirected to /positions; while signed out → redirected to /login (no loop either way).
  • /users/deactivated as a non-admin (applicant and manager bypass users) → 404; signed out → login redirect.
  • Create a user with a deactivated account's email → toast pointing at Deactivated accounts; with an active user's email → the existing duplicate message.
  • reactivateUser rejects a non-admin caller and throws for an already-active user (covered by tests/db/authorization.test.ts, run in CI).
  • 375px / 768px / 1280px, light + dark: the deactivated screen and the accounts table stay readable, buttons wrap rather than overflow.

Automated checks

  • npm run prettier:check — pass
  • npm run eslint:check — pass
  • npm run tsc:check — pass
  • npm run test:unit — pass (71 tests); the db project could not run locally — Postgres wasn't reachable in this environment (port already bound by another worktree, and hand-writing .env is out of scope for this agent) — CI's run-test-check will exercise the new reactivateUser cases against real Postgres.

Notes

  • Clearing deletedById on reactivation drops the record of who deactivated the account (there's no audit-log table); updatedById records the reactivating admin instead, which is what the acceptance criteria asks for.
  • The email/neonAuthId total unique constraints are unchanged — reactivation is the workaround, and the improved createUser copy makes it discoverable. Partial unique indexes stay deferred to schema.prisma data-integrity & indexing gaps #306.
  • A dev-bypass cookie pointing at a deactivated user still lands on /login/bypass, not the new screen — dev-only, and the three bypass identities are never deactivated.
  • Public pages continue to treat a deactivated session as anonymous; only gated routes route to the new screen.

@cielbellerose cielbellerose self-assigned this Aug 18, 2026
@vercel

vercel Bot commented Aug 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
aplio Ready Ready Preview Aug 19, 2026 10:02pm

@cielbellerose cielbellerose added ready for review PR ready for review agent reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Aug 18, 2026

@cielbellerose cielbellerose left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review — Cycle 1 · needs revision

2 open — 2 🟡 Low (see inline)

Comment thread lib/auth/server.ts Outdated
Comment thread prisma/actions/users.ts Outdated
@cielbellerose cielbellerose added needs revision Review found issues that need fixing revising Revise agent working (in-flight) and removed reviewing Review agent working (in-flight) needs revision Review found issues that need fixing labels Aug 18, 2026
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 1

fixed R1-L1, R1-L2 · fa07f1c

@cielbellerose cielbellerose added ready for review PR ready for review agent reviewing Review agent working (in-flight) and removed revising Revise agent working (in-flight) ready for review PR ready for review agent labels Aug 18, 2026

@cielbellerose cielbellerose left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review — Cycle 2 · approved

0 open — both R1 findings resolved, no regressions in the revision

@cielbellerose cielbellerose added approved Review passed, ready to merge and removed reviewing Review agent working (in-flight) labels Aug 18, 2026
@b-at-neu

Copy link
Copy Markdown
Collaborator

Scope change — please do not merge as-is

#387 has been rescoped: no in-app insight into deactivated users, and no in-app reactivation. Reactivation is a database operation for now. Details in the issue comment.

Remove from this PR

  • app/(main)/(auth)/users/deactivated/page.tsx and loading.tsx
  • components/features/deactivated-users-table.tsx
  • The reactivation action added to prisma/actions/users.ts
  • The deactivated-users query added to prisma/data/users.ts
  • The link/entry point added to app/(main)/(auth)/users/page.tsx
  • The lib/types.ts additions, if they only serve the removed table

Keep

  • app/login/deactivated/page.tsx — the explanatory screen for the affected user
  • components/features/deactivated-sign-out.tsx
  • The lib/auth/server.ts routing that sends a deactivated session there rather than looping on /login
  • prisma/actions/auth.ts changes supporting sign-out from that screen
  • tests/db/authorization.test.ts — worth keeping and extending to assert deactivated users are not returned by any admin query

One copy check: the retained screen must not offer or imply reactivation through the app. It should state the account is deactivated and to contact an administrator.

@cielbellerose cielbellerose added needs revision Review found issues that need fixing revising Revise agent working (in-flight) and removed approved Review passed, ready to merge needs revision Review found issues that need fixing revising Revise agent working (in-flight) labels Aug 19, 2026
@cielbellerose
cielbellerose force-pushed the 387-add-account-deactivated-screen-and-admin-reactivation branch from fa07f1c to 1cba121 Compare August 19, 2026 14:21
@cielbellerose cielbellerose added reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Aug 19, 2026

@cielbellerose cielbellerose left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review — Cycle 4 · approved

0 open — all R3 findings resolved, no regressions in the revision

@cielbellerose cielbellerose added approved Review passed, ready to merge and removed reviewing Review agent working (in-flight) labels Aug 19, 2026
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Bug found in manual testing — fixed

Deactivated dev-bypass users weren't seeing /login/deactivated — they just kept bouncing back to /login/bypass.

Root cause: resolveSessionUserRow only checked the real Better Auth session (auth.api.getSession), never the dev-bypass-user-id cookie. getOptionalUser had its own separate bypass-cookie check (which is why it correctly excluded a deactivated bypass user), but getDeactivatedSessionUser — which drives the /login/deactivated vs. plain-login routing decision — relied solely on resolveSessionUserRow, so it never saw a bypass session at all and always returned null.

Fix: moved the bypass-cookie check into resolveSessionUserRow itself (checked first, same as getOptionalUser used to do independently), so both resolveRealUser and getDeactivatedSessionUser see the same resolved row regardless of auth method. getOptionalUser simplifies to just resolveRealUser() now that the duplication is gone.

npm run prettier:check / eslint:check / tsc:check all pass. Pushed as 2714b4a.

@cielbellerose cielbellerose added ready for review PR ready for review agent and removed approved Review passed, ready to merge labels Aug 19, 2026
cielbellerose and others added 5 commits August 19, 2026 16:54
resolveRealUser used to return null after a valid session had already
resolved, silently bouncing a deactivated user back to /login forever.
Distinguishes that case and sends it to /login/deactivated instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deactivation was previously one-way, with no way to restore access.
Adds Users → Deactivated accounts, listing soft-deleted rows with a
reactivate action scoped by the existing authorization helpers, and
points createUser's duplicate-email error at it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
#387 was rescoped: reactivation stays a database operation, so this
drops the admin-only deactivated-accounts page/table, the
reactivateUser action and its query, the entry point link, and the
now-unused type. Copy that referenced the removed page is updated to
point at contacting an administrator instead. Extends the
authorization test suite to assert deactivated users are excluded
from admin-facing queries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Points admin-facing deactivation copy at the real remediation
(database change, contact engineering) instead of "an administrator,"
trims an over-length comment, and fixes deactivated-screen text hierarchy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@cielbellerose
cielbellerose force-pushed the 387-add-account-deactivated-screen-and-admin-reactivation branch from 2714b4a to 3e5a5b7 Compare August 19, 2026 21:58
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Rebase conflict resolved — #497 merged into dev

#497 merged into dev while this PR's bugfix was in flight, and it brought its own deactivation-detection code (resolveUser's three-state model, redirecting to /login?reason=deactivated) that collided with this PR's separate implementation (resolveSessionUserRow/getDeactivatedSessionUser, redirecting to a dedicated /login/deactivated screen).

Resolution, per human direction:

  • lib/auth/server.ts — adopted dev's resolveUser three-state resolver as the base (bypass-cookie-aware, so this also fully subsumes the bypass-detection fix from 2714b4a — that commit became a no-op and was dropped by the rebase). Extended UserResolution's deactivated case to carry the User row (dev didn't need it; this PR's /login/deactivated screen does, for deactivatedUser.email). getCurrentUser's deactivated branch still redirects to /login/deactivated, not dev's query-param banner — this PR's dedicated screen is the surviving UX per human decision.
  • app/login/page.tsx — kept this PR's own version (the getDeactivatedSessionUser redirect-to-dedicated-screen check), dropped dev's inline ?reason=deactivated banner entirely (unreachable now that getCurrentUser routes away before this page renders it). lib/constants.ts's LOGIN_DEACTIVATED_REASON had no other callers left, so it's removed too; ACCOUNT_DEACTIVATED_MESSAGE stays (still used by prisma/actions/auth.ts and login-view.tsx for the OTP sign-in-attempt error, unrelated to this banner).
  • components/features/users-table.tsx — the deactivate-confirm dialog copy conflicted 3 times across this branch's own history (its original text, a review-feedback revision, and dev's #388 Revoke Access Immediately When A User Is Deactivated #497 text). Synthesized the two substantively-different, both-true facts rather than picking one side: "will be signed out immediately and blocked from signing back in" (dev/#388 Revoke Access Immediately When A User Is Deactivated #497 — accurate now that deactivation revokes sessions immediately) + "Reactivating requires a direct database change — contact engineering" (this PR's own review-approved remediation copy, R3-M1).

npm run prettier:check / eslint:check / tsc:check all pass. Pushed as 3e5a5b7.

@cielbellerose cielbellerose added reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Aug 19, 2026
@cielbellerose

Copy link
Copy Markdown
Collaborator Author
Screenshot 2026-08-19 at 6 03 46 PM

@cielbellerose cielbellerose left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review — Cycle 5 · approved

0 open — the #497 rebase and dev-bypass bugfix introduced no regressions

@cielbellerose cielbellerose added approved Review passed, ready to merge and removed reviewing Review agent working (in-flight) labels Aug 19, 2026
@cielbellerose
cielbellerose merged commit d96c83d into dev Aug 19, 2026
10 of 11 checks passed
@cielbellerose
cielbellerose deleted the 387-add-account-deactivated-screen-and-admin-reactivation branch August 19, 2026 22:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Review passed, ready to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add an Account Deactivated Screen and Admin Reactivation

2 participants