fix(auth): unblock invited users and bootstrap first admin#2
Merged
anthonybailey merged 1 commit intomainfrom May 3, 2026
Merged
fix(auth): unblock invited users and bootstrap first admin#2anthonybailey merged 1 commit intomainfrom
anthonybailey merged 1 commit intomainfrom
Conversation
7 tasks
411e668 to
38cd8a1
Compare
This was referenced May 2, 2026
7e5ae8a to
e0901ab
Compare
Two related sign-in failures, both stemming from "user row exists in DB but auth flow refuses to proceed": 1. Invited users could not sign in. Auth.js throws OAuthAccountNotLinked before the signIn callback runs whenever an email matches an existing user with no linked account row — exactly the state every invited user is in. Re-enable allowDangerousEmailAccountLinking on the Google provider and compensate with a profile.email_verified === true guard plus the existing invite-only check. Trade-off documented in BUGS.md #24; structural fix tracked in #3. 2. First-admin chicken-and-egg. ADMIN_EMAILS auto-promotes existing users to admin in the jwt callback, but if no row exists the signIn callback rejects them as not-invited. Auto-create the user row when the email is in ADMIN_EMAILS, and add them to the Global workspace so hasWorkspaceMembership() callers don't treat them as non-members. Race-safe via onConflictDoNothing + re-SELECT. The ADMIN_EMAILS parser is hoisted to module scope so signIn and jwt callbacks share it instead of duplicating the same parse chain.
e0901ab to
2dbc405
Compare
anthonybailey
approved these changes
May 3, 2026
Collaborator
anthonybailey
left a comment
There was a problem hiding this comment.
Unblocking since looks like Maxime hasn't time.
The approach makes sense. The implementation smells a little "oh surely it should need to be this hard" but I don't see the simpler path, and I buy all the arguments re setting the flag, appreciate the issue to make it unnecessary, and the reported testing looks sound.
Approving.
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.
Summary
Fixes invited-user sign-in and bootstraps the first admin on a fresh install.
OAuthAccountNotLinkedbefore thesignIncallback runs whenever a user row exists by email but has no linked OAuthaccountrow — exactly the state every invited user is in afterinviteUser()pre-creates their row. Re-enableallowDangerousEmailAccountLinkingon the Google provider and compensate in thesignIncallback with aprofile.email_verified === trueguard plus the existing invite-only check. Trade-off documented inBUGS.md#24 (REOPENED & MITIGATED). Issue Refactor invite flow: use pending_invites table, drop allowDangerousEmailAccountLinking #3 tracks the longer-term fix to drop the dangerous flag entirely via apending_invitestable.ADMIN_EMAILSauto-promotes existing users to admin in thejwtcallback, but on a fresh DB nobody has a row yet, sosignInrejects the very first admin as not-invited. Auto-create the user row when the email is inADMIN_EMAILS, race-safe viaonConflictDoNothing+ re-SELECT, and add the new admin to the Global workspace as admin (sohasWorkspaceMembership()callers don't treat them as a non-member). TheADMIN_EMAILSparser is hoisted to module scope and shared between thesignInandjwtcallbacks instead of being duplicated.Test plan
userrow for an invitee Google account with no linkedaccountrow (verified: 1 user, 0 accounts). Signed out admin, signed in with the invitee Google account → landed in dashboard. Re-checked DB: 1 user, 1 account withprovider = 'google'. TheOAuthAccountNotLinkedfailure mode is gone.userrow, signed in via Google →usersrow auto-created withrole = admin,user_workspacesrow created withrole = adminagainst the Global workspace,accountrow linked to the GoogleproviderAccountId. All three rows present in one sign-in.