Skip to content

feat: multiple oauth providers#355

Merged
steveiliop56 merged 12 commits into
mainfrom
feat/multi-oauth
Sep 16, 2025
Merged

feat: multiple oauth providers#355
steveiliop56 merged 12 commits into
mainfrom
feat/multi-oauth

Conversation

@steveiliop56
Copy link
Copy Markdown
Member

@steveiliop56 steveiliop56 commented Sep 12, 2025

Fixes #62

Summary by CodeRabbit

  • New Features

    • OAuth auto-redirect handover page with a manual "Redirect now" button and longer reveal delay.
  • Improvements

    • Login UI renders OAuth providers dynamically and shows friendly provider names (also on logout).
    • New provider icons: Microsoft, Tailscale, Pocket ID.
    • Added UI text/localization for the auto-redirect flow.
  • Chores

    • Streamlined provider configuration sources and removed multiple provider-specific CLI flags.
    • CLI now tolerates unknown flags; trusted-proxies description clarified.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 12, 2025

Walkthrough

Replaces hard-coded single-provider OAuth with runtime-configured multi-provider support (env/flags/files), surfaces provider display names through sessions and APIs, updates frontend for dynamic providers and an OAuth auto-redirect handover, and adds decoders, tests, migrations, and new icon assets.

Changes

Cohort / File(s) Summary
CLI / root
cmd/root.go
Remove provider-specific CLI flags and runtime secret-file resolution; enable unknown flag acceptance; update trusted-proxies help text.
Bootstrap / provider discovery
internal/bootstrap/app_bootstrap.go, internal/utils/app_utils.go, internal/utils/app_utils_test.go
Replace static provider config with GetOAuthProvidersConfig(env,args,appURL); build typed controller.Provider list; tests added/updated (duplicate test observed).
Config types / models
internal/config/config.go, internal/model/session_model.go
Add Providers wrapper and OAuthServiceConfig.Name/ClientSecretFile; add OAuthName to Session, SessionCookie, and UserContext.
Controller / API shape
internal/controller/context_controller.go, internal/controller/context_controller_test.go
Introduce Provider {id,name,oauth}; replace ConfiguredProviders []string with Providers []Provider; expose oauthName on user responses; update tests.
OAuth services & broker
internal/service/oauth_broker_service.go, internal/service/google_oauth_service.go, internal/service/github_oauth_service.go, internal/service/generic_oauth_service.go
Add GetName() string to OAuthService interface and implementations; store provider display name in services.
Session flow & middleware
internal/controller/oauth_controller.go, internal/service/auth_service.go, internal/middleware/context_middleware.go
Persist oauth_name on session creation, return it when reading sessions, and populate UserContext.OAuthName from cookie.
DB migrations
internal/assets/migrations/000002_oauth_name.up.sql, internal/assets/migrations/000002_oauth_name.down.sql
Add oauth_name TEXT to sessions, backfill from provider (GitHub/Google/capitalized provider); down migration drops column.
Decoders (env/flags)
internal/utils/decoders/decoders.go, internal/utils/decoders/env_decoder.go, internal/utils/decoders/flags_decoder.go, internal/utils/decoders/*.test.go
New normalization and decoders to map PROVIDERS_* env vars and --providers-* flags to config.Providers via parser; unit tests added.
Frontend schemas / context
frontend/src/schemas/app-context-schema.ts, frontend/src/schemas/user-context-schema.ts
Replace configuredProviders with providers: Provider[]; remove genericName; add oauthName to user schema; loosen oauthAutoRedirect typing.
Frontend pages & UI
frontend/src/pages/login-page.tsx, frontend/src/pages/logout-page.tsx, frontend/src/pages/continue-page.tsx
Render OAuth providers dynamically with icons; add oauthAutoRedirect handover flow and timers; continue page reveal delayed to 5s; logout uses oauthName.
Frontend i18n & icons
frontend/src/lib/i18n/locales/en.json, frontend/src/lib/i18n/locales/en-US.json, frontend/src/components/icons/*.tsx
Add OAuth auto-redirect strings; add Microsoft, PocketID, Tailscale icons and rename GenericIconOAuthIcon.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant FE as Frontend (Login)
  participant BE as Backend (Context API)
  participant OB as OAuth Broker

  Note over BE,OB: Runtime provider discovery (env/flags/files)
  U->>FE: GET /login
  FE->>BE: GET /api/context
  BE-->>FE: {providers: [{id,name,oauth},...], oauthAutoRedirect}

  alt oauthAutoRedirect enabled for provider P
    FE->>OB: POST /api/oauth/start {providerId: P}
    OB-->>FE: {authURL}
    FE-->>U: Show handover UI (5s timer)
    U->>OB: Navigate to authURL (auto or click)
  else manual selection
    U->>FE: Click provider button
    FE->>OB: POST /api/oauth/start {providerId}
    OB-->>FE: {authURL}
    FE->>U: Redirect immediately
  end
Loading
sequenceDiagram
  autonumber
  participant OP as OAuth Provider
  participant OB as OAuth Broker
  participant AS as Auth Service
  participant DB as Database
  participant MW as Context Middleware
  participant FE as Frontend

  OP->>OB: Callback (code,state)
  OB->>AS: Verify code
  AS->>DB: Insert session {oauth_name: service.GetName(), ...}
  DB-->>AS: OK
  AS-->>OB: Set-Cookie (session + OAuthName)
  FE->>BE: Subsequent request with cookie
  BE-->>FE: UserContext {oauthName, username, provider, ...}
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

I nibble keys and hop through code,
Providers bloom where configs flowed.
Names tuck into sessions neat,
Icons dance on login sheet.
Handover set — the redirect's sweet. 🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning Although the multi-provider configuration work is present, the PR also adds UI/UX and behavioral changes that fall outside the linked issue's scope, notably a new OAuth auto-redirect handshake and card in the login flow, a 5‑second reveal delay in ContinuePage, added localization keys and CardFooter export, and new presentational icons; these UX/timing changes expand scope beyond the env-driven provider parsing requested in [#62]. There is also an accidental duplicate TestGetOAuthProvidersConfig insertion in internal/utils/app_utils_test.go that will cause test failures. Backend schema, service, migration and decoding logic are in-scope, but the unrelated front-end UX/timing changes and the duplicated test should be reviewed or separated. Split or remove unrelated UX/timing changes (auto-redirect card, ContinuePage delay, and similar UI tweaks) into a separate PR or document and justify them here, and remove the duplicated test entry so CI can pass; also add documentation for the expected env/flag naming convention.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "feat: multiple oauth providers" is concise, follows conventional commit style, and directly summarizes the PR's primary change—adding support for multiple OAuth providers—so it accurately reflects the changeset and is understandable to teammates scanning history.
Linked Issues Check ✅ Passed The PR implements the core objectives of [#62]: it adds env- and flag-based decoders, a GetOAuthProvidersConfig helper that merges env/flags and resolves secrets, converts configuration to a Providers map, updates backend controllers/services and frontend schemas to emit typed providers and provider names, and includes tests and a DB migration to persist oauth_name, thereby enabling arbitrary multiple OAuth providers driven from configuration rather than hard-coded defaults. These changes collectively satisfy the requirement to derive provider names from configuration and support multiple providers. One minor caveat is that the implementation/tests use a PROVIDERS_* naming convention (e.g., PROVIDERS_CLIENT1_CLIENT_ID) rather than the SERVICE_* example shown in the issue, so the naming expectation should be documented.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/multi-oauth

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Sep 12, 2025

Codecov Report

❌ Patch coverage is 63.53591% with 66 lines in your changes missing coverage. Please review.
✅ Project coverage is 25.95%. Comparing base (2d78e6b) to head (7795a98).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
internal/bootstrap/app_bootstrap.go 0.00% 33 Missing ⚠️
internal/utils/app_utils.go 88.00% 4 Missing and 2 partials ⚠️
internal/utils/decoders/decoders.go 88.88% 3 Missing and 3 partials ⚠️
internal/service/generic_oauth_service.go 0.00% 3 Missing ⚠️
internal/service/github_oauth_service.go 0.00% 3 Missing ⚠️
internal/service/google_oauth_service.go 0.00% 3 Missing ⚠️
internal/utils/decoders/env_decoder.go 70.00% 2 Missing and 1 partial ⚠️
internal/utils/decoders/flags_decoder.go 82.35% 2 Missing and 1 partial ⚠️
cmd/root.go 0.00% 2 Missing ⚠️
internal/service/auth_service.go 0.00% 2 Missing ⚠️
... and 2 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #355      +/-   ##
==========================================
+ Coverage   22.61%   25.95%   +3.33%     
==========================================
  Files          33       36       +3     
  Lines        2397     2524     +127     
==========================================
+ Hits          542      655     +113     
- Misses       1826     1833       +7     
- Partials       29       36       +7     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@steveiliop56 steveiliop56 marked this pull request as ready for review September 12, 2025 14:31
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
internal/controller/context_controller.go (1)

71-97: Fix potential nil dereference: check GetContext error before accessing fields

You build the response using context.* before checking err. If utils.GetContext(c) returns an error and a nil context, this will panic.

Apply:

 func (controller *ContextController) userContextHandler(c *gin.Context) {
-	context, err := utils.GetContext(c)
-
-	userContext := UserContextResponse{
-		Status:      200,
-		Message:     "Success",
-		IsLoggedIn:  context.IsLoggedIn,
-		Username:    context.Username,
-		Name:        context.Name,
-		Email:       context.Email,
-		Provider:    context.Provider,
-		OAuth:       context.OAuth,
-		TotpPending: context.TotpPending,
-		OAuthName:   context.OAuthName,
-	}
-
-	if err != nil {
-		log.Debug().Err(err).Msg("No user context found in request")
-		userContext.Status = 401
-		userContext.Message = "Unauthorized"
-		userContext.IsLoggedIn = false
-		c.JSON(200, userContext)
-		return
-	}
-
-	c.JSON(200, userContext)
+	ctx, err := utils.GetContext(c)
+	if err != nil || ctx == nil {
+		log.Debug().Err(err).Msg("No user context found in request")
+		c.JSON(200, UserContextResponse{
+			Status:     401,
+			Message:    "Unauthorized",
+			IsLoggedIn: false,
+		})
+		return
+	}
+
+	userContext := UserContextResponse{
+		Status:      200,
+		Message:     "Success",
+		IsLoggedIn:  ctx.IsLoggedIn,
+		Username:    ctx.Username,
+		Name:        ctx.Name,
+		Email:       ctx.Email,
+		Provider:    ctx.Provider,
+		OAuth:       ctx.OAuth,
+		TotpPending: ctx.TotpPending,
+		OAuthName:   ctx.OAuthName,
+	}
+	c.JSON(200, userContext)
 }
internal/service/github_oauth_service.go (1)

52-56: Add HTTP client timeouts to prevent indefinite hangs

OAuth and userinfo calls shouldn’t rely on a client without a timeout.

 func (github *GithubOAuthService) Init() error {
-	httpClient := &http.Client{}
+	httpClient := &http.Client{Timeout: 10 * time.Second}
 	ctx := context.Background()
 	ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
 	verifier := oauth2.GenerateVerifier()
🧹 Nitpick comments (29)
internal/utils/decoders/label_decoder_test.go (1)

66-67: DeepEqual arg order (actual, expected).
Use actual first, expected second for clearer diffs: assert.DeepEqual(t, result, expected). This matches the pkg docs signature func DeepEqual(t, x, y ...). (pkg.go.dev)

-	assert.DeepEqual(t, expected, result)
+	assert.DeepEqual(t, result, expected)
frontend/src/components/icons/tailscale.tsx (1)

7-7: Drop xmlSpace attribute (unused).
Reduces noise; not needed for inline SVG.

-      xmlSpace="preserve"
frontend/src/components/icons/microsoft.tsx (1)

7-9: Default to 1em for inline sizing (optional).
Using em-based defaults tends to fit better inside buttons; callers can still override.

-      width="2em"
-      height="2em"
+      width="1em"
+      height="1em"
frontend/src/pages/logout-page.tsx (1)

18-18: Guard against missing oauthName to avoid 'undefined' in UI

If backend omits oauthName for an OAuth session, the subtitle will render "undefined". Add a fallback to the provider id.

-              values={{
-                username: email,
-                provider: oauthName,
-              }}
+              values={{
+                username: email,
+                provider: oauthName ?? provider,
+              }}

Also applies to: 66-68

frontend/src/schemas/app-context-schema.ts (1)

3-7: Tighten provider schema (non-empty, safe id)

Prevent empty strings and enforce a stable id format for mapping and icons.

-export const providerSchema = z.object({
-  id: z.string(),
-  name: z.string(),
-  oauth: z.boolean(),
-});
+export const providerSchema = z.object({
+  id: z.string().min(1, "id required").regex(/^[a-z0-9-]+$/i, "use alphanumerics and hyphens"),
+  name: z.string().min(1, "name required"),
+  oauth: z.boolean(),
+});
internal/config/config.go (1)

169-171: Add explicit tags for decoders to reduce coupling to field names

Makes flags/env decoders resilient and self-documenting.

 type Providers struct {
-	Providers map[string]OAuthServiceConfig
+	Providers map[string]OAuthServiceConfig `mapstructure:"providers" flag:"providers"`
 }
internal/service/oauth_broker_service.go (3)

47-54: Fix log formatting: %T prints 'string', not the provider name

Use %s to print the provider key.

-		if err != nil {
-			log.Error().Err(err).Msgf("Failed to initialize OAuth service: %T", name)
-			return err
-		}
-		log.Info().Msgf("Initialized OAuth service: %T", name)
+		if err != nil {
+			log.Error().Err(err).Msgf("Failed to initialize OAuth service: %s", name)
+			return err
+		}
+		log.Info().Msgf("Initialized OAuth service: %s", name)

3-9: Unify slices import to stdlib

You already use the stdlib "slices" elsewhere; prefer it here to drop x/exp.

-import (
+import (
 	"errors"
 	"tinyauth/internal/config"
 
 	"github.com/rs/zerolog/log"
-	"golang.org/x/exp/slices"
+	"slices"
 )

Also applies to: 64-65


25-30: Pre-size services map

Minor allocation win when many providers are configured.

 func NewOAuthBrokerService(configs map[string]config.OAuthServiceConfig) *OAuthBrokerService {
 	return &OAuthBrokerService{
-		services: make(map[string]OAuthService),
+		services: make(map[string]OAuthService, len(configs)),
 		configs:  configs,
 	}
 }
internal/utils/decoders/flags_decoder.go (1)

119-137: Clarify/validate provider id constraints

Flags like --providers-my-awesome-client-... aren’t supported; either validate and error early or document clearly.

Would you like me to add an explicit validation (e.g., provider id must match ^[a-z0-9-]+$ without extra dashes) and update the docs?

internal/controller/context_controller.go (1)

100-108: Preserve AppURL path; avoid rewriting to scheme://host

If AppURL includes a path (e.g., behind a reverse-proxy), dropping it can break clients. Use the parsed URL as-is.

-		AppURL:                fmt.Sprintf("%s://%s", appUrl.Scheme, appUrl.Host),
+		AppURL:                appUrl.String(),
internal/controller/context_controller_test.go (1)

35-45: Add an assertion for oauthName propagation

Since the API now exposes oauthName, add a case asserting it’s present when set in context.

 var userContext = config.UserContext{
   Username:    "testuser",
   Name:        "testuser",
   Email:       "test@example.com",
   IsLoggedIn:  true,
   OAuth:       false,
   Provider:    "username",
   TotpPending: false,
   OAuthGroups: "",
   TotpEnabled: false,
+  OAuthName:   "Local",
 }

 func TestUserContextHandler(t *testing.T) {
   expectedRes := controller.UserContextResponse{
     Status:      200,
     Message:     "Success",
     IsLoggedIn:  userContext.IsLoggedIn,
     Username:    userContext.Username,
     Name:        userContext.Name,
     Email:       userContext.Email,
     Provider:    userContext.Provider,
     OAuth:       userContext.OAuth,
     TotpPending: userContext.TotpPending,
+    OAuthName:   userContext.OAuthName,
   }

Also applies to: 95-105

frontend/src/pages/continue-page.tsx (1)

50-53: Guard against null redirectUriObj in handleRedirect

Non-null assertion can still crash at runtime in edge cases; add a quick guard.

 const handleRedirect = () => {
-  setLoading(true);
-  window.location.assign(redirectUriObj!.toString());
+  if (!redirectUriObj) return;
+  setLoading(true);
+  window.location.assign(redirectUriObj.toString());
 };
internal/controller/oauth_controller.go (1)

109-114: Improve CSRF mismatch logging

Avoid logging a nil error on pure mismatches; log clearer branches.

-	if err != nil || state != csrfCookie {
-		log.Warn().Err(err).Msg("CSRF token mismatch or cookie missing")
+	if err != nil || state != csrfCookie {
+		if err != nil {
+			log.Warn().Err(err).Msg("CSRF cookie missing")
+		} else {
+			log.Warn().Msg("CSRF token mismatch")
+		}
 		c.SetCookie(controller.config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
 		c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
 		return
 	}
internal/service/google_oauth_service.go (1)

31-32: Expose provider display name — consider a safe fallback.

If Name is unset in config, return a sensible default to keep UI stable.

 func (google *GoogleOAuthService) GetName() string {
-	return google.name
+	if google.name != "" {
+		return google.name
+	}
+	return "Google"
 }

Also applies to: 43-44, 117-119

internal/utils/decoders/flags_decoder_test.go (1)

11-60: Good positive test; add a precedence test (flags override env).

Add a subtest mixing env+flags to assert flag values win.

// outside current test
func TestProviders_FlagsOverrideEnv(t *testing.T) {
	env := []string{
		"TINYAUTH_PROVIDERS_CLIENT1_CLIENT_ID=env-id",
		"TINYAUTH_PROVIDERS_CLIENT1_CLIENT_SECRET=env-secret",
	}
	args := []string{
		"tinyauth",
		"--providers-client1-client-id=flag-id",
	}
	// use app_utils to exercise full path (env+flags merge)
	got, err := utils.GetOAuthProvidersConfig(env, args, "https://app.example.com")
	assert.NilError(t, err)
	assert.Equal(t, got["client1"].ClientID, "flag-id")
	assert.Equal(t, got["client1"].ClientSecret, "env-secret")
}
internal/utils/decoders/env_decoder_test.go (2)

37-54: Add cases for TokenURL and initialism/casing edge keys.

To harden decoder compatibility, also assert mapping for:

  • TOKEN_URL (expects TokenURL)
  • USERINFO_URL (single word) vs USER_INFO_URL (two words) both landing on UserinfoURL
  • INSECURE_SKIP_VERIFY=true

Apply this minimal augmentation:

   test := map[string]string{
@@
-    "PROVIDERS_CLIENT1_USER_INFO_URL":        "client1-user-info-url",
+    "PROVIDERS_CLIENT1_USER_INFO_URL":        "client1-user-info-url",
+    "PROVIDERS_CLIENT1_TOKEN_URL":            "client1-token-url",
@@
   expected := config.Providers{
     Providers: map[string]config.OAuthServiceConfig{
       "client1": {
@@
-        UserinfoURL:        "client1-user-info-url",
+        UserinfoURL:        "client1-user-info-url",
+        TokenURL:           "client1-token-url",

56-60: Consider adding a negative test for provider IDs with underscores.

Currently, providers like PROVIDERS_MY_AWESOME_CLIENT_* are likely to mis-decode (see decoder comment). Add one test asserting proper handling or explicit error until decoder is fixed.

cmd/root.go (2)

55-55: UnknownFlags=true can hide genuine typos in user flags.

This is useful to pass through dynamic --providers-* flags, but it will also swallow misspelled core flags. Consider validating and warning on unknown flags that don't start with the expected providers prefix.

Example approach: scan os.Args for --providers-* here and only tolerate those; fail others.


96-96: Help text still lists fixed providers.

With dynamic providers, prefer “Auto redirect to the specified configured provider id.” to avoid misleading users.

Apply this tweak:

-{"oauth-auto-redirect", "none", "Auto redirect to the specified OAuth provider if configured. (available providers: github, google, generic)"},
+{"oauth-auto-redirect", "none", "Auto redirect to the specified configured OAuth provider id (e.g. google, github, client1)."},
internal/utils/app_utils_test.go (1)

205-271: Solid happy-path tests; add a few more scenarios.

Suggestions:

  • Merge precedence: flags should override env for same provider key.
  • Scopes parsing from flags (e.g., --providers-client1-scopes=a,b).
  • Name and InsecureSkipVerify fields.

Apply a small extra case for precedence:

 env := []string{"PROVIDERS_CLIENT1_CLIENT_ID=env-id", "PROVIDERS_CLIENT1_CLIENT_SECRET=env-secret"}
 args := []string{"/tinyauth/tinyauth", "--providers-client1-client-id=flag-id", "--providers-client1-client-secret=flag-secret"}
@@
 expected := map[string]config.OAuthServiceConfig{
   "client1": {ClientID: "flag-id", ClientSecret: "flag-secret"},
 }
internal/utils/decoders/env_decoder.go (1)

119-137: Comment fix and initialism mapping caveat.

  • The comment says “client_clientId” but code produces “…_client1_clientId”. Update comment.
  • Be aware normalizeEnv yields userInfoUrl/tokenUrl; struct fields are UserinfoURL/TokenURL. Parser must be forgiving on URL initialism; otherwise consider a special-case to keep “URL” uppercased.

Suggested comment tweak:

-// normalizeEnv converts env vars from PROVIDERS_CLIENT1_CLIENT_ID to tinyauth_providers_client_clientId
+// normalizeEnv converts env vars from PROVIDERS_CLIENT1_CLIENT_ID to tinyauth_providers_client1_clientId

If needed, special-case URL:

-    fkb += strings.ToUpper(string([]rune(s)[0])) + string([]rune(s)[1:])
+    part := strings.ToUpper(string([]rune(s)[0])) + string([]rune(s)[1:])
+    if strings.EqualFold(s, "url") { part = "URL" }
+    fkb += part
frontend/src/pages/login-page.tsx (3)

75-81: Clear the handover timer and reset state on OAuth error.

Prevents stale timers and a latent “showRedirectButton=true” after failures.

Apply:

 onError: () => {
-  setOauthAutoRedirectHandover(false);
+  setOauthAutoRedirectHandover(false);
+  if (redirectButtonTimer.current) {
+    clearTimeout(redirectButtonTimer.current);
+    redirectButtonTimer.current = null;
+  }
+  setShowRedirectButton(false);
   toast.error(t("loginOauthFailTitle"), {
     description: t("loginOauthFailSubtitle"),
   });
 },

153-177: Expose manual redirect button state to a11y.

Optional: add aria-busy to the CardFooter while waiting, and disable the button if oauthMutation is pending.

-<CardFooter className="flex flex-col items-stretch">
+<CardFooter className="flex flex-col items-stretch" aria-busy={oauthMutation.isPending}>
   <Button
     onClick={() => {
       window.location.replace(oauthMutation.data?.data.url);
     }}
+    disabled={oauthMutation.isPending}
   >

218-221: Use strict equality.

Prefer === to avoid accidental coercion.

-{providers.length == 0 && (
+{providers.length === 0 && (
internal/bootstrap/app_bootstrap.go (4)

49-55: Add error context when loading OAuth providers.

Wrap the returned error for clearer diagnostics.

-	oauthProviders, err := utils.GetOAuthProvidersConfig(os.Environ(), os.Args, app.Config.AppURL)
-	if err != nil {
-		return err
-	}
+	oauthProviders, err := utils.GetOAuthProvidersConfig(os.Environ(), os.Args, app.Config.AppURL)
+	if err != nil {
+		return fmt.Errorf("failed to load OAuth providers config from env/flags: %w", err)
+	}

143-147: DRY known-provider friendly names.

This map duplicates knowledge also used in utils for “babysitting.” Consider centralizing friendly-name mapping (e.g., utils.FriendlyProviderName or a shared const map) to avoid divergence.


170-174: Username provider append looks good.

Optional: if only LDAP is enabled (no local users), consider labeling “LDAP” for clarity; keep ID “username” for compatibility.

Does the frontend icon set handle a non-”Username” label without breaking layout?


147-167: Make provider order deterministic and trim/display-safe.

Sort provider IDs case-insensitively, trim provider.Name, and preallocate the slice; no code was found that depends on map iteration order.

-	configuredProviders := make([]controller.Provider, 0)
-
-	for id, provider := range oauthProviders {
-		if id == "" {
-			continue
-		}
-
-		if provider.Name == "" {
-			if name, ok := babysit[id]; ok {
-				provider.Name = name
-			} else {
-				provider.Name = utils.Capitalize(id)
-			}
-		}
-
-		configuredProviders = append(configuredProviders, controller.Provider{
-			Name:  provider.Name,
-			ID:    id,
-			OAuth: true,
-		})
-	}
+	configuredProviders := make([]controller.Provider, 0, len(oauthProviders))
+
+	ids := make([]string, 0, len(oauthProviders))
+	for id := range oauthProviders {
+		if id != "" {
+			ids = append(ids, id)
+		}
+	}
+	sort.Slice(ids, func(i, j int) bool {
+		return strings.ToLower(ids[i]) < strings.ToLower(ids[j])
+	})
+
+	for _, id := range ids {
+		provider := oauthProviders[id]
+		name := strings.TrimSpace(provider.Name)
+		if name == "" {
+			if bName, ok := babysit[id]; ok {
+				name = bName
+			} else {
+				name = utils.Capitalize(id)
+			}
+		}
+		configuredProviders = append(configuredProviders, controller.Provider{
+			Name:  name,
+			ID:    id,
+			OAuth: true,
+		})
+	}

Add missing import:

 import (
 	"fmt"
 	"net/url"
 	"os"
+	"sort"
 	"strings"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2d78e6b and 060e20e.

📒 Files selected for processing (33)
  • cmd/root.go (2 hunks)
  • frontend/src/components/icons/microsoft.tsx (1 hunks)
  • frontend/src/components/icons/oauth.tsx (1 hunks)
  • frontend/src/components/icons/pocket-id.tsx (1 hunks)
  • frontend/src/components/icons/tailscale.tsx (1 hunks)
  • frontend/src/lib/i18n/locales/en-US.json (1 hunks)
  • frontend/src/lib/i18n/locales/en.json (1 hunks)
  • frontend/src/pages/continue-page.tsx (1 hunks)
  • frontend/src/pages/login-page.tsx (6 hunks)
  • frontend/src/pages/logout-page.tsx (2 hunks)
  • frontend/src/schemas/app-context-schema.ts (1 hunks)
  • frontend/src/schemas/user-context-schema.ts (1 hunks)
  • internal/assets/migrations/000002_oauth_name.down.sql (1 hunks)
  • internal/assets/migrations/000002_oauth_name.up.sql (1 hunks)
  • internal/bootstrap/app_bootstrap.go (5 hunks)
  • internal/config/config.go (5 hunks)
  • internal/controller/context_controller.go (3 hunks)
  • internal/controller/context_controller_test.go (2 hunks)
  • internal/controller/oauth_controller.go (1 hunks)
  • internal/middleware/context_middleware.go (1 hunks)
  • internal/model/session_model.go (1 hunks)
  • internal/service/auth_service.go (2 hunks)
  • internal/service/generic_oauth_service.go (3 hunks)
  • internal/service/github_oauth_service.go (3 hunks)
  • internal/service/google_oauth_service.go (3 hunks)
  • internal/service/oauth_broker_service.go (1 hunks)
  • internal/utils/app_utils.go (2 hunks)
  • internal/utils/app_utils_test.go (2 hunks)
  • internal/utils/decoders/env_decoder.go (1 hunks)
  • internal/utils/decoders/env_decoder_test.go (1 hunks)
  • internal/utils/decoders/flags_decoder.go (1 hunks)
  • internal/utils/decoders/flags_decoder_test.go (1 hunks)
  • internal/utils/decoders/label_decoder_test.go (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (17)
frontend/src/pages/continue-page.tsx (1)
frontend/src/pages/totp-page.tsx (2)
  • redirectTimer (53-55)
  • redirectTimer (53-55)
internal/utils/decoders/flags_decoder_test.go (2)
internal/config/config.go (2)
  • Providers (169-171)
  • OAuthServiceConfig (53-64)
internal/utils/decoders/flags_decoder.go (1)
  • DecodeFlags (15-41)
internal/utils/app_utils.go (4)
internal/config/config.go (2)
  • OAuthServiceConfig (53-64)
  • Providers (169-171)
internal/utils/decoders/env_decoder.go (1)
  • DecodeEnv (15-41)
internal/utils/decoders/flags_decoder.go (1)
  • DecodeFlags (15-41)
internal/utils/security_utils.go (1)
  • GetSecret (13-28)
frontend/src/components/icons/pocket-id.tsx (4)
frontend/src/components/icons/generic.tsx (1)
  • GenericIcon (3-24)
frontend/src/components/icons/github.tsx (1)
  • GithubIcon (3-18)
frontend/src/components/icons/google.tsx (1)
  • GoogleIcon (3-30)
frontend/src/components/ui/oauth-button.tsx (1)
  • Props (6-11)
internal/utils/decoders/env_decoder_test.go (2)
internal/config/config.go (2)
  • Providers (169-171)
  • OAuthServiceConfig (53-64)
internal/utils/decoders/env_decoder.go (1)
  • DecodeEnv (15-41)
frontend/src/components/icons/microsoft.tsx (3)
frontend/src/components/icons/google.tsx (1)
  • GoogleIcon (3-30)
frontend/src/components/icons/github.tsx (1)
  • GithubIcon (3-18)
frontend/src/components/icons/generic.tsx (1)
  • GenericIcon (3-24)
frontend/src/components/icons/oauth.tsx (2)
frontend/src/components/icons/generic.tsx (1)
  • GenericIcon (3-24)
frontend/src/components/ui/oauth-button.tsx (1)
  • Props (6-11)
internal/controller/context_controller.go (1)
internal/config/config.go (1)
  • Providers (169-171)
frontend/src/lib/i18n/locales/en.json (1)
frontend/src/components/ui/oauth-button.tsx (1)
  • Props (6-11)
frontend/src/pages/logout-page.tsx (1)
frontend/src/context/user-context.tsx (1)
  • useUserContext (38-48)
internal/utils/decoders/flags_decoder.go (1)
internal/config/config.go (1)
  • Providers (169-171)
internal/utils/app_utils_test.go (2)
internal/config/config.go (1)
  • OAuthServiceConfig (53-64)
internal/utils/app_utils.go (1)
  • GetOAuthProvidersConfig (137-198)
frontend/src/pages/login-page.tsx (9)
frontend/src/components/icons/tailscale.tsx (1)
  • TailscaleIcon (3-24)
frontend/src/components/icons/microsoft.tsx (1)
  • MicrosoftIcon (3-18)
frontend/src/components/icons/pocket-id.tsx (1)
  • PocketIDIcon (3-20)
frontend/src/context/user-context.tsx (1)
  • useUserContext (38-48)
frontend/src/context/app-context.tsx (1)
  • useAppContext (36-44)
frontend/src/components/ui/card.tsx (7)
  • Card (85-85)
  • CardHeader (86-86)
  • CardTitle (88-88)
  • CardDescription (90-90)
  • CardFooter (87-87)
  • CardContent (91-91)
  • CardFooter (74-82)
frontend/src/components/ui/oauth-button.tsx (2)
  • OAuthButton (13-33)
  • Props (6-11)
frontend/src/components/icons/oauth.tsx (1)
  • OAuthIcon (3-24)
frontend/src/pages/totp-page.tsx (1)
  • useUserContext (20-83)
internal/bootstrap/app_bootstrap.go (5)
internal/utils/app_utils.go (1)
  • GetOAuthProvidersConfig (137-198)
internal/config/config.go (2)
  • Config (17-42)
  • Providers (169-171)
internal/service/oauth_broker_service.go (1)
  • NewOAuthBrokerService (25-30)
internal/controller/context_controller.go (1)
  • Provider (37-41)
internal/utils/string_utils.go (1)
  • Capitalize (7-12)
internal/controller/context_controller_test.go (1)
internal/controller/context_controller.go (1)
  • Provider (37-41)
internal/utils/decoders/env_decoder.go (1)
internal/config/config.go (1)
  • Providers (169-171)
frontend/src/components/icons/tailscale.tsx (3)
frontend/src/components/icons/github.tsx (1)
  • GithubIcon (3-18)
frontend/src/components/icons/generic.tsx (1)
  • GenericIcon (3-24)
frontend/src/components/icons/google.tsx (1)
  • GoogleIcon (3-30)
🔇 Additional comments (26)
internal/utils/decoders/label_decoder_test.go (1)

8-8: Switch to gotest.tools/assert looks good.
Consistent with the rest of the suite and improves failure messages.

frontend/src/components/icons/oauth.tsx (1)

3-3: No action required — OAuthIcon rename verified.
oauth.tsx exports OAuthIcon and frontend/src/pages/login-page.tsx imports/uses OAuthIcon; ripgrep found no occurrences of GenericIcon, so no compatibility alias needed.

internal/assets/migrations/000002_oauth_name.down.sql (1)

1-1: Down migration looks good.
Symmetric with the up migration.

frontend/src/schemas/user-context-schema.ts (1)

11-11: Keep oauthName required — backend always returns oauthName

Verified: internal/controller/context_controller.go defines OAuthName string json:"oauthName" (no omitempty) and the handler returns that struct on both success and unauthorized paths, so the JSON will always include oauthName (empty string if unset). Locations: internal/controller/context_controller.go, frontend/src/schemas/user-context-schema.ts, frontend/src/context/user-context.tsx.

frontend/src/components/icons/pocket-id.tsx (1)

13-17: LGTM.
Black circle + white mark ensures visibility on light backgrounds; props are forwarded correctly.

internal/model/session_model.go (1)

12-13: LGTM: schema matches migration and propagation path

Field addition aligns with the migration and context exposure.

internal/controller/context_controller.go (1)

37-45: Provider type and config injection look good

The new Provider struct and propagation via controller config cleanly model multi-provider state. JSON shape is sane and backwards changes are intentional.

internal/controller/context_controller_test.go (1)

15-26: LGTM: Tests updated for typed providers

Switching to []controller.Provider in both config and expectations matches the new public API.

frontend/src/pages/continue-page.tsx (1)

70-74: Confirm 5s reveal UX

Timeout increased to 5s before showing the manual button. Confirm this aligns with the new auto-redirect handover spec; otherwise users may wait longer than necessary when redirects fail.

internal/controller/oauth_controller.go (1)

189-190: Good: Persisting OAuthName into the session cookie

Storing OAuthName from the service ensures the UI can display a friendly provider name consistently.

internal/service/github_oauth_service.go (1)

36-37: LGTM: Provider name plumbed and exposed via GetName()

The added field and accessor align with the interface and are used by the controller to set OAuthName.

Also applies to: 173-175

internal/service/auth_service.go (1)

213-214: Approve — OAuthName propagation verified; migration & model present.

Session persistence and cookie hydration include OAuthName; aligns with the new model and context usage. Found internal/model/session_model.go (OAuthName field) and internal/assets/migrations/000002_oauth_name.up.sql/.down.sql (adds/drops column and seeds existing rows). Ensure the migration has been applied in all target environments before rollout.

internal/service/generic_oauth_service.go (1)

25-26: LGTM on name wiring.

Getter and constructor plumbing are consistent with the interface and other providers.

Also applies to: 42-43, 121-123

frontend/src/lib/i18n/locales/en.json (1)

17-19: Copy and tone read well.

Keys are consistent with existing naming; punctuation and casing look good.

internal/middleware/context_middleware.go (1)

98-99: LGTM: OAuthName added to context.

Matches session cookie shape; downstream UI can render provider display name.

internal/utils/app_utils.go (1)

9-11: No changes required — go.mod sets Go 1.24.0, which covers maps.Copy (>=1.21) and fmt.Appendf.

go.mod: go 1.24.0; imports: internal/utils/app_utils.go ("maps"); fmt.Appendf usages: internal/service/google_oauth_service.go:62, internal/service/github_oauth_service.go:67, internal/service/generic_oauth_service.go:72.

frontend/src/lib/i18n/locales/en-US.json (1)

17-19: LGTM; matches base locale.

Strings mirror en.json and support the new auto-redirect UI.

internal/utils/decoders/env_decoder_test.go (1)

11-36: Good baseline coverage for multi-provider env decoding.

Covers two providers, scopes parsing, name, and booleans. Looks consistent with config.OAuthServiceConfig.

cmd/root.go (1)

96-96: Trusted proxies description improvement looks good.

internal/utils/app_utils_test.go (1)

4-4: Import addition is appropriate.

frontend/src/pages/login-page.tsx (2)

30-36: Icon map with sensible fallback.

Good UX: known icons per id with OAuthIcon fallback.


114-129: Auto-redirect guard conditions look right.

Checks: providers available, id exists, not logged in, and redirect_uri present. Nice.

internal/bootstrap/app_bootstrap.go (4)

6-6: LGTM on import.


123-123: Constructor change wired correctly.


177-177: Provider list debug log is helpful.

No secrets included; safe at debug level.


218-218: Context config migration to Providers looks correct.

Comment thread frontend/src/components/icons/tailscale.tsx
Comment thread frontend/src/schemas/app-context-schema.ts
Comment thread internal/assets/migrations/000002_oauth_name.up.sql Outdated
Comment thread internal/utils/app_utils.go
Comment thread internal/utils/decoders/env_decoder.go Outdated
Comment thread internal/utils/decoders/flags_decoder.go
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
internal/assets/migrations/000002_oauth_name.up.sql (1)

1-8: Wrap in a transaction if your migrator doesn't do it.

Prevents a half-applied state between ALTER and UPDATE.

If not auto-wrapped by your migration runner, enclose the statements in BEGIN/COMMIT.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 060e20e and 120ae2c.

📒 Files selected for processing (3)
  • frontend/src/components/icons/tailscale.tsx (1 hunks)
  • internal/assets/migrations/000002_oauth_name.up.sql (1 hunks)
  • internal/utils/app_utils.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/src/components/icons/tailscale.tsx
  • internal/utils/app_utils.go
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (go)

Comment thread internal/assets/migrations/000002_oauth_name.up.sql Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (7)
internal/utils/decoders/flags_decoder.go (1)

24-29: More robust flag trimming.

TrimPrefix("--") misses single-dash or accidental triple-dash variants. Prefer TrimLeft to strip any number of leading dashes.

- for k, v := range flags {
-   filtered[strings.TrimPrefix(k, "--")] = v
- }
+ for k, v := range flags {
+   filtered[strings.TrimLeft(k, "-")] = v
+ }
internal/utils/decoders/decoders.go (3)

34-45: Guard against empty provider IDs.

If the key is like PROVIDERS__CLIENT_ID, camelClientName ends up empty, producing tinyauth.providers..clientId.

   clientNameParts := strings.Split(strings.TrimPrefix(strings.TrimSuffix(cebabKey, sep+strings.ReplaceAll(suffix, "-", sep)), "providers"+sep), sep)

+  // Skip if no provider token
+  if len(clientNameParts) == 0 || clientNameParts[0] == "" {
+    continue
+  }

49-60: Nit: typos and readability.

filedParts → fieldParts; cebabKey → lowerKey (or kebabKey). No behavior change, but avoids confusion.

- filedParts := strings.Split(suffix, "-")
+ fieldParts := strings.Split(suffix, "-")
...
- cebabKey := strings.ToLower(k)
+ lowerKey := strings.ToLower(k)

And update references accordingly.


13-21: Micro: avoid repeated string concatenations.

Using strings.Builder (or pre-sizing slices and Join) will reduce allocations in tight loops.

internal/utils/decoders/decoders_test.go (1)

10-44: Solid coverage; add a couple of edge cases.

Consider tests for:

  • name and insecure-skip-verify keys.
  • a provider with consecutive separators (e.g., PROVIDERS__X__CLIENT_ID) to verify the empty‑provider guard.
internal/config/config.go (2)

90-101: UserContext OAuthName: API surface check.

If this is serialized to clients, confirm casing is acceptable (OAuthName vs oauthName) or add json tags for stability.


169-171: Providers wrapper type: optional mapstructure tag.

If any non-paerser decoding path exists, add mapstructure:"providers" for symmetry.

 type Providers struct {
-  Providers map[string]OAuthServiceConfig
+  Providers map[string]OAuthServiceConfig `mapstructure:"providers"`
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 120ae2c and cebce1a.

📒 Files selected for processing (5)
  • internal/config/config.go (5 hunks)
  • internal/utils/decoders/decoders.go (1 hunks)
  • internal/utils/decoders/decoders_test.go (1 hunks)
  • internal/utils/decoders/env_decoder.go (1 hunks)
  • internal/utils/decoders/flags_decoder.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
internal/utils/decoders/decoders_test.go (1)
internal/utils/decoders/decoders.go (1)
  • NormalizeKeys (9-67)
internal/utils/decoders/flags_decoder.go (2)
internal/config/config.go (1)
  • Providers (169-171)
internal/utils/decoders/decoders.go (1)
  • NormalizeKeys (9-67)
internal/utils/decoders/env_decoder.go (2)
internal/config/config.go (1)
  • Providers (169-171)
internal/utils/decoders/decoders.go (1)
  • NormalizeKeys (9-67)
internal/utils/decoders/decoders.go (1)
internal/config/config.go (1)
  • OAuthServiceConfig (53-64)
🔇 Additional comments (4)
internal/utils/decoders/env_decoder.go (1)

9-20: LGTM: env decoding pipeline is clean and composable.

NormalizeKeys + parser.Decode is the right shape for multi‑provider envs.

Please confirm we actually need both roots in Decode (tinyauth, tinyauth.providers). If only tinyauth is required, drop the second to reduce ambiguity.

internal/utils/decoders/flags_decoder.go (1)

10-22: LGTM: flags → normalized keys → parser.Decode flow.

Consistent with the env decoder; easy to maintain.

internal/config/config.go (2)

53-64: Key tags look consistent; confirm UserinfoURL tag.

user-info-url matches the tag schema; just confirm downstream expects “user-info-url”, not “userinfo-url”.


80-88: SessionCookie OAuthName addition: check persistence/migrations.

Assuming migrations wire this field, looks good. Verify JSON exposure isn’t required here (no json tags).

Comment thread internal/utils/decoders/decoders.go
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
internal/assets/migrations/000002_oauth_name.up.sql (1)

1-9: Wrap in a transaction if your migrator doesn’t.

The prior concern stands: ADD COLUMN + UPDATE should be atomic if the runner doesn’t auto-wrap.

BEGIN;
-- statements...
COMMIT;
🧹 Nitpick comments (4)
internal/assets/migrations/000002_oauth_name.up.sql (4)

7-7: Prefer INITCAP with normalization over manual SUBSTR/UPPER.

This yields better display names for values like 'GITHUB', 'azure_ad', or ' okta '. Example refactor:

-  ELSE UPPER(SUBSTR("provider", 1, 1)) || SUBSTR("provider", 2)
+  ELSE INITCAP(REGEXP_REPLACE(TRIM("provider"), '[_\\-\\s]+', ' ', 'g'))

3-9: Guard against empty/whitespace-only providers.

As written, provider='' would backfill oauth_name to ''. Trim and NULL-check to avoid junk values.

-WHERE "oauth_name" IS NULL AND "provider" IS NOT NULL;
+WHERE "oauth_name" IS NULL
+  AND NULLIF(TRIM("provider"), '') IS NOT NULL;

4-6: Trim before case-insensitive comparisons.

Covers stray whitespace from prior data.

-  WHEN LOWER("provider") = 'github' THEN 'GitHub'
-  WHEN LOWER("provider") = 'google' THEN 'Google'
+  WHEN LOWER(TRIM("provider")) = 'github' THEN 'GitHub'
+  WHEN LOWER(TRIM("provider")) = 'google' THEN 'Google'

7-7: Postgres function form consistency.

If you keep the manual capitalization path, prefer SUBSTRING(...) or verify SUBSTR two-arg support in your target PG version. Using INITCAP above sidesteps this.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cebce1a and 7795a98.

📒 Files selected for processing (1)
  • internal/assets/migrations/000002_oauth_name.up.sql (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (go)
🔇 Additional comments (1)
internal/assets/migrations/000002_oauth_name.up.sql (1)

3-9: Good preservation of historical provider names.

CASE mapping for GitHub/Google and leaving existing non-null oauth_name untouched looks correct.

@steveiliop56 steveiliop56 added the merge This pull request is ready to be merged into main. label Sep 15, 2025
@steveiliop56 steveiliop56 added this to the v4.0.0 milestone Sep 15, 2025
@steveiliop56 steveiliop56 merged commit 5c866ba into main Sep 16, 2025
8 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Dec 21, 2025
@coderabbitai coderabbitai Bot mentioned this pull request Mar 21, 2026
@Rycochet Rycochet deleted the feat/multi-oauth branch April 1, 2026 16:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge This pull request is ready to be merged into main.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Multiple OAuth providers

1 participant