@@ -213,41 +213,84 @@ func (m *OAuthManager) CallbackHandler(providerName string, db *gorm.DB, adminEm
213213 })
214214 }
215215
216- // In invite mode, block new OAuth users who don't have a valid invite code
217- // to prevent phantom pending records from accumulating in the DB.
218- // The first user (no users in DB yet) is always allowed through.
219- if registrationMode == "invite" && inviteCode == "" {
220- var existing User
221- if err := db .Where ("provider = ? AND subject = ?" , providerName , userInfo .Subject ).First (& existing ).Error ; err != nil {
222- // Check if this would be the first user (always allowed)
223- var userCount int64
224- db .Model (& User {}).Count (& userCount )
225- if userCount > 0 {
226- // New user without invite code — reject without creating a DB record
227- return c .Redirect (http .StatusTemporaryRedirect , "/login?error=invite_required" )
228- }
216+ // Check if user already exists
217+ var existingUser User
218+ userExists := db .Where ("provider = ? AND subject = ?" , providerName , userInfo .Subject ).First (& existingUser ).Error == nil
219+
220+ var user * User
221+ if userExists {
222+ // Existing user — update profile fields, no invite gating needed
223+ existingUser .Name = userInfo .Name
224+ existingUser .AvatarURL = userInfo .AvatarURL
225+ if userInfo .Email != "" {
226+ existingUser .Email = strings .ToLower (strings .TrimSpace (userInfo .Email ))
229227 }
230- }
228+ db .Save (& existingUser )
229+ user = & existingUser
230+ } else {
231+ // New user — validate invite BEFORE creating, inside a transaction
232+ var validInvite * InviteCode
233+ txErr := db .Transaction (func (tx * gorm.DB ) error {
234+ email := ""
235+ if userInfo .Email != "" {
236+ email = strings .ToLower (strings .TrimSpace (userInfo .Email ))
237+ }
231238
232- // Upsert user (with invite code support)
233- user , err := upsertOAuthUser (db , providerName , userInfo , adminEmail , registrationMode )
234- if err != nil {
235- return c .JSON (http .StatusInternalServerError , map [string ]string {"error" : "failed to create user" })
236- }
239+ role := AssignRole (tx , email , adminEmail )
240+ status := StatusActive
241+
242+ if NeedsInviteOrApproval (tx , email , adminEmail , registrationMode ) {
243+ if registrationMode == "invite" {
244+ if inviteCode == "" {
245+ return fmt .Errorf ("invite_required" )
246+ }
247+ invite , err := ValidateInvite (tx , inviteCode , hmacSecret )
248+ if err != nil {
249+ return fmt .Errorf ("invalid_invite" )
250+ }
251+ validInvite = invite
252+ status = StatusActive
253+ } else {
254+ // approval mode — create as pending
255+ status = StatusPending
256+ }
257+ }
237258
238- // For new users that are pending, check if they have a valid invite
239- if user .Status != StatusActive && inviteCode != "" {
240- if invite , err := ValidateInvite (db , inviteCode , hmacSecret ); err == nil {
241- user .Status = StatusActive
242- db .Model (user ).Update ("status" , StatusActive )
243- ConsumeInvite (db , invite , user .ID )
259+ newUser := User {
260+ ID : uuid .New ().String (),
261+ Email : email ,
262+ Name : userInfo .Name ,
263+ AvatarURL : userInfo .AvatarURL ,
264+ Provider : providerName ,
265+ Subject : userInfo .Subject ,
266+ Role : role ,
267+ Status : status ,
268+ }
269+ if err := tx .Create (& newUser ).Error ; err != nil {
270+ return fmt .Errorf ("failed to create user: %w" , err )
271+ }
272+
273+ if validInvite != nil {
274+ ConsumeInvite (tx , validInvite , newUser .ID )
275+ }
276+
277+ user = & newUser
278+ return nil
279+ })
280+
281+ if txErr != nil {
282+ msg := txErr .Error ()
283+ if msg == "invite_required" {
284+ return c .Redirect (http .StatusTemporaryRedirect , "/login?error=invite_required" )
285+ }
286+ if msg == "invalid_invite" {
287+ return c .Redirect (http .StatusTemporaryRedirect , "/login?error=invalid_invite" )
288+ }
289+ return c .JSON (http .StatusInternalServerError , map [string ]string {"error" : "failed to create user" })
244290 }
245291 }
246292
247293 if user .Status != StatusActive {
248- if registrationMode == "invite" {
249- return c .JSON (http .StatusForbidden , map [string ]string {"error" : "a valid invite code is required to register" })
250- }
251294 return c .JSON (http .StatusForbidden , map [string ]string {"error" : "account pending approval" })
252295 }
253296
@@ -393,58 +436,6 @@ func fetchGitHubPrimaryEmail(ctx context.Context, accessToken string) (string, e
393436 return "" , fmt .Errorf ("no verified email found" )
394437}
395438
396- func upsertOAuthUser (db * gorm.DB , provider string , info * oauthUserInfo , adminEmail , registrationMode string ) (* User , error ) {
397- // Normalize email from provider (#10)
398- if info .Email != "" {
399- info .Email = strings .ToLower (strings .TrimSpace (info .Email ))
400- }
401-
402- var user User
403- err := db .Where ("provider = ? AND subject = ?" , provider , info .Subject ).First (& user ).Error
404- if err == nil {
405- // Existing user — update profile fields
406- user .Name = info .Name
407- user .AvatarURL = info .AvatarURL
408- if info .Email != "" {
409- user .Email = info .Email
410- }
411- db .Save (& user )
412- return & user , nil
413- }
414-
415- // New user — empty registration mode defaults to "approval"
416- effectiveMode := registrationMode
417- if effectiveMode == "" {
418- effectiveMode = "approval"
419- }
420- status := StatusActive
421- if effectiveMode == "approval" || effectiveMode == "invite" {
422- status = StatusPending
423- }
424-
425- role := AssignRole (db , info .Email , adminEmail )
426- // First user is always active regardless of registration mode
427- if role == RoleAdmin {
428- status = StatusActive
429- }
430-
431- user = User {
432- ID : uuid .New ().String (),
433- Email : info .Email ,
434- Name : info .Name ,
435- AvatarURL : info .AvatarURL ,
436- Provider : provider ,
437- Subject : info .Subject ,
438- Role : role ,
439- Status : status ,
440- }
441-
442- if err := db .Create (& user ).Error ; err != nil {
443- return nil , err
444- }
445-
446- return & user , nil
447- }
448439
449440func generateState () (string , error ) {
450441 b := make ([]byte , 16 )
0 commit comments