feat(web): self-serve account deletion; hide GitHub sign-in until enabled - #255
Merged
Merged
Conversation
…session /api/auth/login (signInWithPassword), /api/auth/refresh (refreshSession) and /api/auth/signup (signUp) ran on the process-wide anon Supabase client from getSupabaseClient(). supabase-js keeps the resulting session in that client's memory, and GET/PATCH /api/auth/me and GET /api/auth/check fell back to supabase.auth.getSession() when a request had no bearer token. Any unauthenticated request was therefore answered as whoever last signed in: their profile and phone were readable, and PATCH /api/auth/me could rewrite their wallet_address (referral payouts) and notification webhook. Session-creating calls now use a fresh per-request client (createSupabaseAuthClient), and the getSession() fallbacks are gone, so each layer alone closes the leak.
"Continue with GitHub" led production users into Supabase's "400 Unsupported provider: provider is not enabled", because the GitHub provider is not enabled in Supabase Auth. The button (and its divider) on login/signup now render only with NEXT_PUBLIC_GITHUB_OAUTH_ENABLED=true, and /api/auth/github answers a clear 404 instead of redirecting into that 400, so the owner can switch it on after enabling the provider.
The privacy policy promises deletion, but /api/auth/me had only GET and PATCH. DELETE /api/auth/me now deletes the caller's account after they re-enter their password (OAuth-only accounts type their email instead). It refuses while they are the only owner of an org with other members, deletes the orgs only they belong to (servers, detections, etc. cascade), hands shared orgs they created to a remaining owner, then deletes the GoTrue user, which cascades to the profile, memberships and push subscriptions. /account gets a danger-zone section for it that signs out and returns home. The migration fixes two FKs that made this fail or destroy other people's data: servers.created_by was NOT NULL yet ON DELETE SET NULL, and organizations.created_by cascaded the whole org away when its creator was deleted; it is now RESTRICT.
ThreatCrush Security Scan16 finding(s) HIGH/CRITICAL: 1 | MEDIUM: 7 | LOW: 8
Snippets are redacted; ThreatCrush never prints matched credential material. |
# Conflicts: # apps/web/src/app/api/auth/me/route.ts # apps/web/src/app/auth/signup/page.tsx
ralyodio
added a commit
that referenced
this pull request
Sep 26, 2026
`sh -c 'true & ...; exec sleep 30'` only leaves a zombie if `true` exits after the exec. When it exits first, sh reaps it and /proc/<pid>/stat is gone, so the poll threw ENOENT (failed twice in a row on #255's CI). Background a short sleep instead so the child outlives the exec, tolerate a missing stat file while polling, and assert the zombie state was reached. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts: # docs/SELF_HOSTING.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
400 Unsupported provider: provider is not enabled, because the GitHub provider isn't enabled in Supabase Auth. The button and its "or continue with" divider on/auth/loginand/auth/signupnow render only whenNEXT_PUBLIC_GITHUB_OAUTH_ENABLED=true. When the flag is off,GET /api/auth/githubreturns404 {"error":"GitHub sign-in is not enabled. Log in with your email and password instead."}instead of redirecting into the Supabase 400. Nothing else about OAuth changes. The flag is documented in.env.exampleanddocs/SELF_HOSTING.md.DELETE /api/auth/me). The privacy policy promises deletion, but/api/auth/meonly had GET and PATCH.organizations: [{id,name,slug}]) and tells the user to make another member an owner or delete the org. The refusal happens before the password check, so a refused attempt never mints a session.ON DELETE CASCADEs. For shared orgs the caller created,created_bymoves to the longest-standing remaining owner, or the longest-standing member if there is no other owner. Then the GoTrue admin API deletes the auth user, which cascades touser_profiles, org memberships,push_subscriptionsand the otherauth.userscascades listed under Decisions.GET /api/auth/menow also returnshas_password, so the UI knows which confirmation to ask for.Delete account…section at the bottom of/accountopens a confirmation form: password, or the typed email for OAuth-only accounts. On success it signs out and lands on/. On a 409 it shows the error plus links to each blocking org's settings page, where roles are managed.20260925130000_account_deletion.sql. Two FKs ontouser_profilesmade deletion fail or destroy other people's data:servers.created_bywasNOT NULLbutON DELETE SET NULL. Deleting anyone who ever added a server to an org they share failed with a NOT NULL violation. The column is now nullable, and the server stays with the org.Server.created_byinlib/organizations.tsis now typedstring | null.organizations.created_bywasON DELETE CASCADE. Deleting an org's creator silently deleted the whole org, including every other member's servers and detections, even after ownership had been shared. It is nowON DELETE RESTRICT: every deletion path must first delete or re-home the orgs the user created, which the route does.How verified
pnpm --filter @profullstack/threatcrush-web test: 57 files, 503 tests pass. The new tests are:me/__tests__/delete.test.ts, which runs against an in-memory org/member store. It covers: 401 without a token; 400 for a bad body or missing password; 403 for a wrong password with nothing deleted; a typed email not accepted in place of a password; OAuth-only email confirmation (case-insensitive); the 409 sole-owner refusal, with no password check and nothing deleted; solo org deleted while a shared org is untouched;created_byhanded to the longest-standing other owner; a membership-less created org deleted; a failed GoTrue delete returning 500.has_passwordon GET./api/auth/githubreturning 404 without redirecting for flag values unset,"",falseand1.psql, twice (idempotent). Result:organizations_created_by_fkeyhasconfdeltype = randservers.created_byis nullable.next devon :3446 against local Supabase, headless Chromium:/auth/loginand/auth/signuphave 0 "Continue with GitHub" elements (screenshots inspected: no divider, no button).GET /api/auth/githubreturns404 {"error":"GitHub sign-in is not enabled. Log in with your email and password instead."}.NEXT_PUBLIC_GITHUB_OAUTH_ENABLED=true: 1 button on each page (screenshot inspected: divider plus button, as before).GET /api/auth/githubreturns 307 to…/auth/v1/authorize?provider=github…, unchanged behaviour./account→ "Delete account…" → wrong password shows "Incorrect password" (DELETE → 403) → correct password (DELETE → 200) → landed on/with the token gone from localStorage. Afterwards:GET /admin/users/Xreturned 404.auth.users,user_profiles,organization_membersandpush_subscriptionsrows for X: 0.created_by= Y and only Y's owner membership left. The server X had added to it survived withcreated_byNULL./org/y-team-1252/settings(screenshot inspected). Y's auth user, orgs and memberships were unchanged.Blocked on the owner
20260925130000_account_deletion.sqlto production Supabase before deploying. Without it, a deletion that touches a shared org'screated_byor a server the user added to a shared org fails partway, after the user's solo orgs are already gone.https://supabase.threatcrush.com/auth/v1/callback), then setNEXT_PUBLIC_GITHUB_OAUTH_ENABLED=truein the environment used bynext buildon the server and redeploy. It's aNEXT_PUBLIC_variable, so it is inlined at build time./account"Scan announcements" section still says "Sign in with GitHub to see the installations you manage". I left it unchanged. It's accurate once the flag is on, but misleading until then.Decisions
organizations.created_by→ RESTRICT rather than SET NULL. With SET NULL the surviving org would have no creator, andDELETE /api/orgs/:id(creator-only) could never delete it. RESTRICT makes a missed re-home fail loudly instead of deleting data. Side effect: deleting a user in the Supabase dashboard now errors if they still created an org. Delete or re-home the org first.auth.userscascades, left unchanged:license_purchases,credit_deposits,usage_events,referral_wallets,user_settings,user_installed_modules,phone_verification_codes. If you need payment records kept for accounting, change those FKs before shipping this.module_reviews.user_email,module_installs.user_email),funding_payments, and waitlist/contact/whitepaper rows. Those have no FK to the user. Say if reviews should go too./api/limit applies.