Skip to content

fix: polishing auto signup via oidc functionality#1569

Merged
yottahmd merged 7 commits intomainfrom
auto-signup2
Jan 11, 2026
Merged

fix: polishing auto signup via oidc functionality#1569
yottahmd merged 7 commits intomainfrom
auto-signup2

Conversation

@yottahmd
Copy link
Copy Markdown
Collaborator

@yottahmd yottahmd commented Jan 11, 2026

Summary by CodeRabbit

  • New Features

    • Admins can enable/disable users from the UI; user list shows auth provider and account status.
    • Login now accepts an authentication token in the URL and redirects the user appropriately.
  • Improvements

    • OIDC configuration and default-role handling made more explicit; email whitelist/domain checks are stricter with clearer warnings.
  • Bug Fixes

    • Prevents users from disabling their own account via the admin UI.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 11, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

Adds user enable/disable support across API, service, and UI; restructures OIDC config to use a nested role-mapping and an IsConfigured() check; changes OIDC callback redirect to return token via URL; adds provider init retry and refines email allowlist logic; propagates remoteNode query param across frontend user endpoints.

Changes

Cohort / File(s) Summary
API schema / client
api/v2/api.yaml, api/v2/api.gen.go, ui/src/api/v2/schema.ts
Added optional isDisabled to UpdateUserRequest and exposed isDisabled + authProvider on User; added UserAuthProvider enum.
Auth service (user update)
internal/service/auth/service.go, internal/service/frontend/api/v2/users.go
Added IsDisabled *bool to UpdateUserInput; frontend handler maps request IsDisabled into input and prevents self-disable (Forbidden).
Frontend UI (users pages & modals)
ui/src/pages/users/index.tsx, ui/src/pages/users/UserFormModal.tsx, ui/src/pages/users/ResetPasswordModal.tsx
UI: table adds Auth/Status columns, per-user enable/disable actions; user create/update/reset calls include remoteNode query param (from AppBarContext); changed some request methods/URLs and added auth token guards.
Login / token handling
ui/src/pages/login.tsx, internal/service/frontend/auth/oidc.go
Login page reads token URL param and stores it to localStorage; OIDC callback now redirects to login with token=... (and &welcome=true for new users); provider init retried with delay on failures; error redirect trimmed to /login.
OIDC configuration & loader
internal/common/config/definition.go, internal/common/config/config.go, internal/common/config/loader.go, internal/common/config/config_test.go, internal/common/config/loader_test.go
Moved DefaultRole into nested OIDCRoleMapping (RoleMapping.DefaultRole); removed top-level Enabled/DefaultRole on AuthOIDC; added AuthOIDC.IsConfigured() and parseStringList loader helper; defaulted AutoSignup to true; updated tests and env bindings.
OIDC provisioning & validation
internal/service/frontend/server.go, internal/service/oidcprovision/service.go, internal/service/oidcprovision/service_test.go
Use IsConfigured() for builtin checks; isEmailAllowed now denies when any whitelist/allowed-domains restriction exists and none match; added tests for whitelist cases.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Browser
participant Frontend
participant OIDCProvider
participant DB
Browser->>OIDCProvider: Initiate auth (redirect)
OIDCProvider->>Browser: Redirect to Frontend callback (code)
Browser->>Frontend: GET /auth/oidc/callback?code=...
Frontend->>OIDCProvider: Exchange code for token
OIDCProvider-->>Frontend: ID/JWT token, userinfo
Frontend->>DB: Lookup or create user (provisioning)
DB-->>Frontend: User record (including created flag)
Frontend-->>Browser: Redirect to /login?token=[&welcome=true]

mermaid
sequenceDiagram
participant AdminUI
participant FrontendAPI
participant AuthService
participant DB
AdminUI->>FrontendAPI: PATCH /api/v2/users/{id} { isDisabled: true }
FrontendAPI->>AuthService: UpdateUserInput{ IsDisabled: true, ... }
AuthService->>DB: Update user.IsDisabled field
DB-->>AuthService: OK
AuthService-->>FrontendAPI: Updated user
FrontendAPI-->>AdminUI: 200 OK (updated user)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix: polishing auto signup via oidc functionality' accurately reflects the main objective of the changeset, which includes improvements to OIDC auto-signup flow including user disable functionality, OIDC configuration restructuring, retry logic, and related UI updates.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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.

Copy link
Copy Markdown

@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: 4

Caution

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

⚠️ Outside diff range comments (1)
internal/service/frontend/api/v2/users.go (1)

119-184: Consider forbidding self-disable (mirrors “cannot delete yourself”).
Right now an admin can PATCH themselves to isDisabled=true and lock themselves out (especially problematic if they’re the only admin). You already do a self-protection check for Delete; worth doing the same for disable.

Proposed fix (block self-disable in UpdateUser)
 func (a *API) UpdateUser(ctx context.Context, request api.UpdateUserRequestObject) (api.UpdateUserResponseObject, error) {
   if err := a.requireUserManagement(); err != nil {
     return nil, err
   }
   if err := a.requireAdmin(ctx); err != nil {
     return nil, err
   }

+  // Prevent self-disable (consistent with DeleteUser self-protection).
+  if currentUser, ok := auth.UserFromContext(ctx); ok && request.Body != nil && request.Body.IsDisabled != nil {
+    if currentUser.ID == request.UserId && *request.Body.IsDisabled {
+      return nil, &Error{
+        Code:       api.ErrorCodeForbidden,
+        Message:    "Cannot disable your own account",
+        HTTPStatus: http.StatusForbidden,
+      }
+    }
+  }
+
   if request.Body == nil {
     return nil, &Error{
       Code:       api.ErrorCodeBadRequest,
       Message:    "Invalid request body",
       HTTPStatus: http.StatusBadRequest,
     }
   }
🤖 Fix all issues with AI agents
In @internal/service/frontend/auth/oidc.go:
- Around line 504-514: The code currently appends the JWT to redirectURL and
sends it as a query param; instead set the JWT as a secure, HttpOnly cookie and
redirect without the token in the URL. In the handler that builds redirectURL
(variable redirectURL and the http.Redirect call), create and set an http.Cookie
(e.g. Name "auth_token") with Value tokenResult.Token, Secure: true, HttpOnly:
true, SameSite: http.SameSiteLaxMode (or Strict if appropriate), Path "/", and a
sensible Expires/MaxAge, then construct redirectURL without "?token=..." (only
include "&welcome=true" when isNewUser) and call http.Redirect(w, r,
redirectURL, http.StatusFound). Ensure you remove the token-related query
construction and replace it with the cookie set via http.SetCookie before
redirecting.

In @ui/src/pages/users/ResetPasswordModal.tsx:
- Around line 1-3: The fetch URL builder in ResetPasswordModal.tsx currently
interpolates remoteNode (and uses token) directly; update calls that construct
the fetch endpoint (the places around lines where remoteNode is used, e.g., the
initial GET/POST fetches and the reset submission block) to URL-encode
remoteNode with encodeURIComponent(remoteNode) and add a guard that token exists
before building the URL (return early or show an error if token is missing).
Ensure all occurrences (the spots near lines 38-39 and 76-85) use the encoded
value and perform a null/empty check on token before calling fetch.

In @ui/src/pages/users/UserFormModal.tsx:
- Around line 1-3: The fetch URL construction in UserFormModal.tsx uses raw
remoteNode (and token) values directly; URL-encode remoteNode (e.g., via
encodeURIComponent(remoteNode)) before interpolating into any fetch URL and
guard for a missing token (throw or early-return if token is falsy) so you never
build requests with undefined credentials; update all occurrences mentioned
(around lines 50-51, 85-96, 104-112) where fetch is called to use the
encodedRemoteNode and to check token presence before creating the URL or
headers.
🧹 Nitpick comments (4)
internal/service/frontend/server.go (1)

73-130: Good move to IsConfigured(), but validate RoleMapping.DefaultRole before casting.
If config parsing doesn’t already enforce valid roles, authmodel.Role(oidcCfg.RoleMapping.DefaultRole) can silently accept garbage and fail later in provisioning. Consider validating once (e.g., parse/valid check) and returning a clear startup error.

internal/service/frontend/auth/oidc.go (1)

517-523: Build the error redirect URL via url.URL/url.Values (avoid subtle path/query edge cases).
Current concat is mostly fine, but this keeps the code consistent with token/welcome handling above.

Proposed refactor
-	redirectURL := strings.TrimSuffix(basePath, "/") + "/login?error=" + url.QueryEscape(errMsg)
-	http.Redirect(w, r, redirectURL, http.StatusFound)
+	loginPath := strings.TrimSuffix(basePath, "/") + "/login"
+	u := &url.URL{Path: loginPath}
+	q := u.Query()
+	q.Set("error", errMsg)
+	u.RawQuery = q.Encode()
+	http.Redirect(w, r, u.String(), http.StatusFound)
internal/common/config/loader.go (1)

1250-1277: Consider trimming whitespace for []interface{} case.

The string case trims whitespace from each element, but the []interface{} case does not. This could lead to inconsistent behavior when the same config is provided via different formats.

♻️ Proposed fix for consistency
 	case []interface{}:
 		for _, item := range v {
-			if s, ok := item.(string); ok && s != "" {
-				result = append(result, s)
+			if s, ok := item.(string); ok {
+				if trimmed := strings.TrimSpace(s); trimmed != "" {
+					result = append(result, trimmed)
+				}
 			}
 		}
ui/src/pages/users/index.tsx (1)

231-245: Add ARIA label for accessibility.

The enable/disable button correctly prevents self-disabling and shows appropriate icons/labels. Consider adding an ARIA label to improve screen reader support.

♻️ Suggested accessibility improvement
 {isAdmin && user.id !== currentUser?.id && (
-  <DropdownMenuItem onClick={() => handleToggleDisabled(user)}>
+  <DropdownMenuItem 
+    onClick={() => handleToggleDisabled(user)}
+    aria-label={user.isDisabled ? `Enable user ${user.username}` : `Disable user ${user.username}`}
+  >
     {user.isDisabled ? (
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2da0188 and 019e943.

📒 Files selected for processing (18)
  • api/v2/api.gen.go
  • api/v2/api.yaml
  • internal/common/config/config.go
  • internal/common/config/config_test.go
  • internal/common/config/definition.go
  • internal/common/config/loader.go
  • internal/common/config/loader_test.go
  • internal/service/auth/service.go
  • internal/service/frontend/api/v2/users.go
  • internal/service/frontend/auth/oidc.go
  • internal/service/frontend/server.go
  • internal/service/oidcprovision/service.go
  • internal/service/oidcprovision/service_test.go
  • ui/src/api/v2/schema.ts
  • ui/src/pages/login.tsx
  • ui/src/pages/users/ResetPasswordModal.tsx
  • ui/src/pages/users/UserFormModal.tsx
  • ui/src/pages/users/index.tsx
🧰 Additional context used
📓 Path-based instructions (5)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Backend entrypoint in cmd/ orchestrates the scheduler and CLI; runtime, persistence, and service layers sit under internal/* (for example internal/runtime, internal/persistence)
Keep Go files gofmt/goimports clean; use tabs, PascalCase for exported symbols (SchedulerClient), lowerCamelCase for locals, and Err... names for package-level errors
Repository linting relies on golangci-lint; prefer idiomatic Go patterns, minimal global state, and structured logging helpers in internal/common

Files:

  • internal/common/config/definition.go
  • internal/service/oidcprovision/service_test.go
  • internal/service/frontend/auth/oidc.go
  • api/v2/api.gen.go
  • internal/service/frontend/server.go
  • internal/common/config/loader.go
  • internal/service/oidcprovision/service.go
  • internal/common/config/loader_test.go
  • internal/service/frontend/api/v2/users.go
  • internal/common/config/config_test.go
  • internal/service/auth/service.go
  • internal/common/config/config.go
**/*_test.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*_test.go: Co-locate Go tests as *_test.go; favour table-driven cases and cover failure paths
Use stretchr/testify/require and shared fixtures from internal/test instead of duplicating mocks

Files:

  • internal/service/oidcprovision/service_test.go
  • internal/common/config/loader_test.go
  • internal/common/config/config_test.go
ui/**/*.{ts,tsx,jsx,js}

📄 CodeRabbit inference engine (ui/CLAUDE.md)

ui/**/*.{ts,tsx,jsx,js}: Use developer-centric UI design with high information density, minimal whitespace, compact components, and no unnecessary decorations
Support both light and dark modes for all UI components using Tailwind CSS class pairs like dark:bg-slate-700
NEVER use full-page loading overlays or LoadingIndicator components that hide content - show stale data while fetching updates instead
Use compact modal design with small headers, minimal padding (p-2 or p-3), tight spacing, and support keyboard navigation (arrows, enter, escape)
Use small heights for form elements: select boxes h-7 or smaller, buttons h-7 or h-8, inputs with compact padding (py-0.5 or py-1)
Minimize row heights in tables and lists while maintaining readability, merge related columns, and always handle long text with whitespace-normal break-words
Use consistent metadata styling with bg-slate-200 dark:bg-slate-700 backgrounds and maintain text hierarchy with primary/secondary/muted text colors
Use flexbox-first layouts with min-h-0 and overflow-hidden to prevent layout breaks, account for fixed elements when setting heights
Maintain keyboard navigation support in all interactive components with appropriate focus indicators and ARIA labels

Files:

  • ui/src/api/v2/schema.ts
  • ui/src/pages/users/ResetPasswordModal.tsx
  • ui/src/pages/users/UserFormModal.tsx
  • ui/src/pages/users/index.tsx
  • ui/src/pages/login.tsx
ui/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

ui/**/*.{ts,tsx}: The React + TypeScript frontend resides in ui/, with production bundles copied to internal/service/frontend/assets by make ui
UI code follows ESLint + Prettier (2-space indent) and Tailwind utilities; name React components in PascalCase (JobList.tsx) and hooks with use* (useJobs.ts)

Files:

  • ui/src/api/v2/schema.ts
  • ui/src/pages/users/ResetPasswordModal.tsx
  • ui/src/pages/users/UserFormModal.tsx
  • ui/src/pages/users/index.tsx
  • ui/src/pages/login.tsx
{api/v1,api/v2}/**/*.{proto,yaml,yml,json}

📄 CodeRabbit inference engine (AGENTS.md)

API definitions live in api/v1 and api/v2; generated server stubs land in internal/service, while matching TypeScript clients flow into ui/src/api

Files:

  • api/v2/api.yaml
🧠 Learnings (3)
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to ui/**/*.{ts,tsx} : UI code follows ESLint + Prettier (2-space indent) and Tailwind utilities; name React components in PascalCase (`JobList.tsx`) and hooks with `use*` (`useJobs.ts`)

Applied to files:

  • ui/src/pages/users/UserFormModal.tsx
📚 Learning: 2025-12-04T10:34:01.045Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: ui/CLAUDE.md:0-0
Timestamp: 2025-12-04T10:34:01.045Z
Learning: Applies to ui/**/*.{ts,tsx,jsx,js} : Use compact modal design with small headers, minimal padding (p-2 or p-3), tight spacing, and support keyboard navigation (arrows, enter, escape)

Applied to files:

  • ui/src/pages/users/index.tsx
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to **/*_test.go : Use `stretchr/testify/require` and shared fixtures from `internal/test` instead of duplicating mocks

Applied to files:

  • internal/common/config/config_test.go
🧬 Code graph analysis (11)
ui/src/api/v2/schema.ts (1)
api/v2/api.gen.go (1)
  • UserAuthProvider (1210-1210)
ui/src/pages/users/ResetPasswordModal.tsx (1)
ui/src/contexts/AppBarContext.ts (1)
  • AppBarContext (11-21)
ui/src/pages/users/UserFormModal.tsx (1)
ui/src/contexts/AppBarContext.ts (1)
  • AppBarContext (11-21)
ui/src/pages/users/index.tsx (3)
ui/src/contexts/AuthContext.tsx (1)
  • TOKEN_KEY (24-24)
ui/src/components/ui/table.tsx (5)
  • TableHead (131-131)
  • TableRow (133-133)
  • TableHeader (132-132)
  • TableBody (127-127)
  • TableCell (129-129)
ui/src/components/ui/dropdown-menu.tsx (1)
  • DropdownMenuItem (185-185)
internal/service/frontend/server.go (2)
internal/service/oidcprovision/service.go (1)
  • Config (29-44)
internal/auth/role.go (1)
  • Role (13-13)
internal/common/config/loader.go (1)
internal/common/config/config.go (2)
  • Server (87-128)
  • Auth (161-167)
internal/service/oidcprovision/service.go (2)
internal/service/auth/service.go (1)
  • Service (78-83)
internal/common/stringutil/stringutil.go (1)
  • ExtractEmailDomain (390-396)
internal/common/config/loader_test.go (1)
internal/common/config/config.go (1)
  • OIDCRoleMapping (176-189)
ui/src/pages/login.tsx (1)
ui/src/contexts/AuthContext.tsx (1)
  • TOKEN_KEY (24-24)
internal/common/config/config_test.go (1)
internal/common/config/config.go (2)
  • OIDCRoleMapping (176-189)
  • Auth (161-167)
internal/service/auth/service.go (1)
internal/auth/role.go (1)
  • Role (13-13)
⏰ 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). (2)
  • GitHub Check: Test on ubuntu-latest
  • GitHub Check: Build
🔇 Additional comments (29)
internal/service/oidcprovision/service.go (1)

243-276: LGTM! Clear refactor that improves readability.

The introduction of hasWhitelist and hasAllowedDomains guard variables makes the access control logic more explicit and easier to follow. The logic correctly implements an OR relationship where either the whitelist or allowed domains can independently permit access, with explicit denial when restrictions exist but the email matches neither.

internal/service/oidcprovision/service_test.go (1)

483-494: LGTM! Good test coverage for whitelist-only scenarios.

These test cases properly validate the behavior when only a whitelist is configured (no allowed domains), covering both the allow and deny paths. The tests align with the refactored logic in the service code.

ui/src/pages/login.tsx (2)

3-3: Nice reuse of TOKEN_KEY instead of a duplicate constant.


84-84: Background tweak looks fine and keeps dark-mode compatibility.

api/v2/api.yaml (1)

4355-4369: UpdateUserRequest.isDisabled addition is consistent with PATCH semantics.
Minor: consider adjusting the description to explicitly say “If omitted, the disabled status is not changed.”

internal/service/auth/service.go (1)

286-291: IsDisabled plumbing in service is clean; consider a policy guard upstream.
This correctly applies the flag and existing auth checks already block disabled users. The remaining concern is policy (self-disable / last active admin) which likely belongs in the API layer (or a dedicated policy check).

Also applies to: 322-325

ui/src/api/v2/schema.ts (2)

1910-1916: Schema additions for isDisabled and authProvider are correctly sourced from the OpenAPI definition.

Verified that api/v2/api.yaml contains both UpdateUserRequest.isDisabled and User.authProvider (enum: builtin/oidc) and User.isDisabled definitions, which are properly reflected in the auto-generated schema.ts file. The file header confirms it is auto-generated by openapi-typescript and should not be manually edited.


5583-5586: UserAuthProvider enum is correctly configured with consistent string values across all layers.

The enum values (builtin, oidc) match in the OpenAPI spec (api/v2/api.yaml), backend constants (api/v2/api.gen.go), and the auto-generated frontend schema (ui/src/api/v2/schema.ts). No action required.

api/v2/api.gen.go (2)

1175-1183: IsDisabled *bool is the right PATCH shape; ensure semantics are documented/handled consistently.

Using *bool allows “no change” vs “explicitly enable/disable”. Just ensure callers understand: omit = unchanged, true = disable, false = enable.


11484-11622: Generated swaggerSpec blob regenerated correctly with pinned version v2.5.0.

Verification confirms api/v2/api.gen.go was regenerated using the pinned oapi-codegen v2.5.0. Both generated files (api/v1/api.gen.go and api/v2/api.gen.go) are in sync with the same version marker and show no staleness.

internal/common/config/loader.go (4)

437-447: LGTM! Clean parsing for flexible config formats.

The use of parseStringList enables both YAML lists and comma-separated environment variable strings, which improves configuration flexibility. The AutoSignup defaulting to true aligns with the expected OIDC user experience.


450-462: LGTM! Role mapping configuration properly loaded.

The RoleMapping configuration is cleanly loaded from the nested config structure, maintaining consistency with the new model where DefaultRole is part of RoleMapping.


511-513: LGTM! Sensible default for OIDC role.

Defaulting RoleMapping.DefaultRole to "viewer" follows the principle of least privilege for auto-provisioned OIDC users.


1125-1125: LGTM! Environment binding properly nested.

The environment binding AUTH_OIDC_DEFAULT_ROLE correctly maps to the nested auth.oidc.roleMapping.defaultRole config key.

internal/common/config/definition.go (2)

184-185: LGTM! DefaultRole field properly added to role mapping definition.

The field is correctly placed within OIDCRoleMappingDef with appropriate mapstructure tag, aligning with the PR's goal of moving default role handling into the RoleMapping structure.


228-230: LGTM! Documentation updated to reflect new behavior.

The docstrings correctly document that OIDC is automatically enabled when required fields are configured, and that AutoSignup defaults to true.

Also applies to: 240-240

ui/src/pages/users/index.tsx (4)

56-77: LGTM! Remote node support properly integrated.

The fetchUsers function correctly includes the remoteNode query parameter with a sensible default of 'local', and the dependency array properly includes appBarContext.selectedRemoteNode to trigger re-fetches when the selection changes.


109-132: LGTM! Toggle disabled implementation is clean.

The handleToggleDisabled function properly:

  • Uses PATCH method for partial updates
  • Toggles isDisabled based on current state
  • Handles errors gracefully
  • Refreshes the user list on success

159-166: LGTM! Table header properly expanded.

New columns for Auth, Status, Created, and Updated are appropriately sized and maintain consistency with the existing table design.


183-206: LGTM! User status and auth provider display is well implemented.

The implementation follows the coding guidelines:

  • Uses appropriate opacity (opacity-60) for disabled users
  • Applies proper dark mode support with text-red-600 dark:text-red-400
  • Maintains compact table design with text-sm sizing
internal/common/config/loader_test.go (2)

177-180: LGTM! Test expectations correctly updated.

The test expectations properly reflect the new OIDC configuration structure:

  • AutoSignup: true as the default
  • RoleMapping with DefaultRole: "viewer"

This aligns with the implementation changes in loader.go.


397-400: LGTM! YAML loading test updated consistently.

The YAML configuration test expectations mirror the environment variable test, ensuring consistent behavior across configuration sources.

internal/common/config/config_test.go (3)

384-417: LGTM! Test correctly validates incomplete OIDC handling.

The test properly verifies that:

  1. Incomplete OIDC configuration (missing clientId) does not cause validation errors
  2. IsConfigured() returns false for incomplete configurations
  3. The error message clarifies the expected behavior

This aligns with the design where OIDC is opt-in based on field presence rather than an explicit flag.


419-449: LGTM! Role validation test uses new structure.

The test correctly uses RoleMapping.DefaultRole and validates that invalid roles are rejected with an appropriate error message.


517-550: LGTM! Comprehensive role validation coverage.

The test validates all valid roles (admin, manager, operator, viewer) using the new RoleMapping.DefaultRole structure.

internal/common/config/config.go (4)

177-178: LGTM! DefaultRole properly moved into OIDCRoleMapping.

Moving DefaultRole into the OIDCRoleMapping struct provides a cleaner separation of concerns - role-related configuration is now consolidated in one place.


214-239: LGTM! Clean IsConfigured() implementation.

The IsConfigured() method provides a clear, single source of truth for determining if OIDC is properly configured. The check for all four required fields (ClientId, ClientSecret, ClientUrl, Issuer) ensures OIDC won't be enabled with partial configuration.

This is a good replacement for an explicit Enabled boolean flag as it derives the enabled state from the presence of required configuration.


416-421: LGTM! Validation gate uses IsConfigured().

Using IsConfigured() to gate OIDC validation ensures that incomplete configurations are silently ignored (OIDC just won't be available) rather than causing validation errors.


451-453: LGTM! Error message references correct config path.

The error message correctly references roleMapping.defaultRole which matches the YAML configuration path, making it easy for users to locate and fix the issue.

Comment thread internal/service/frontend/auth/oidc.go
Comment thread ui/src/pages/login.tsx
Comment thread ui/src/pages/users/ResetPasswordModal.tsx
Comment thread ui/src/pages/users/UserFormModal.tsx
@yottahmd
Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 11, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@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: 2

🤖 Fix all issues with AI agents
In @internal/common/config/loader.go:
- Around line 1250-1279: The []string branch in parseStringList currently
assigns the slice directly, bypassing trimming/filtering; update the case for
[]string to iterate over each element, apply strings.TrimSpace, and append only
non-empty results (same behavior as the string and []interface{} cases) so empty
or whitespace-only entries are filtered consistently; keep the function
signature and other branches unchanged and reuse the same trimming/append logic.

In @internal/service/frontend/auth/oidc.go:
- Around line 521-530: Update the misleading backend comment above the
redirectURL construction: explain that the frontend (login flow) extracts the
tokenResult.Token from the URL, stores it in localStorage, and then navigates
away using React Router's navigate(..., { replace: true }) which prevents the
token URL from entering browser history; reference redirectURL and
tokenResult.Token so maintainers know this is a one-time redirect and the
frontend (not history.replaceState) handles clearing the token from history.
🧹 Nitpick comments (3)
internal/service/frontend/auth/oidc.go (1)

86-101: Consider using contextual logger for consistency.

The retry loop uses slog.Warn directly, but the codebase pattern (shown in relevant snippets) uses logger.Warn(ctx, ...) for structured logging with context. Consider updating for consistency:

♻️ Proposed refactor
 		if attempt < oidcProviderMaxRetries {
-			slog.Warn("OIDC provider init failed, retrying",
+			logger.Warn(ctx, "OIDC provider init failed, retrying",
 				tag.Error(err),
 				slog.Int("attempt", attempt),
 				slog.Int("maxRetries", oidcProviderMaxRetries))
 			time.Sleep(oidcProviderRetryDelay)
 		}

Based on coding guidelines and relevant code snippets showing logger.Warn usage patterns.

ui/src/pages/users/index.tsx (2)

109-132: Consider optimistic updates for better UX.

The current implementation waits for the API response before updating the UI state. While this is safer, it could provide a snappier user experience with optimistic updates:

💡 Potential UX enhancement
const handleToggleDisabled = async (user: User) => {
  // Optimistically update UI
  setUsers(prevUsers => 
    prevUsers.map(u => 
      u.id === user.id ? { ...u, isDisabled: !u.isDisabled } : u
    )
  );
  
  try {
    const token = localStorage.getItem(TOKEN_KEY);
    const remoteNode = encodeURIComponent(appBarContext.selectedRemoteNode || 'local');
    const response = await fetch(`${config.apiURL}/users/${user.id}?remoteNode=${remoteNode}`, {
      method: 'PATCH',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${token}`,
      },
      body: JSON.stringify({ isDisabled: !user.isDisabled }),
    });

    if (!response.ok) {
      // Revert on error
      fetchUsers();
      const data = await response.json().catch(() => ({}));
      throw new Error(data.message || 'Failed to update user');
    }

    setError(null);
  } catch (err) {
    setError(err instanceof Error ? err.message : 'Failed to update user');
  }
};

Based on coding guidelines emphasizing developer-centric UI with high information density.


231-245: Consider adding confirmation for disable action.

The disable/enable toggle executes immediately without confirmation. While less destructive than deletion, disabling a user (especially by accident) could disrupt their workflow. Consider adding a confirmation modal similar to the delete action.

🛡️ Suggested confirmation flow

Add state for a confirmation modal:

const [togglingUser, setTogglingUser] = useState<User | null>(null);

Update the dropdown item:

<DropdownMenuItem onClick={() => setTogglingUser(user)}>
  {user.isDisabled ? (
    <>
      <UserCheck className="h-4 w-4 mr-2" />
      Enable
    </>
  ) : (
    <>
      <Ban className="h-4 w-4 mr-2" />
      Disable
    </>
  )}
</DropdownMenuItem>

Add a confirmation modal similar to the delete modal pattern.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 019e943 and 7d529cb.

📒 Files selected for processing (9)
  • internal/common/config/config.go
  • internal/common/config/config_test.go
  • internal/common/config/loader.go
  • internal/service/frontend/api/v2/users.go
  • internal/service/frontend/auth/oidc.go
  • ui/src/pages/login.tsx
  • ui/src/pages/users/ResetPasswordModal.tsx
  • ui/src/pages/users/UserFormModal.tsx
  • ui/src/pages/users/index.tsx
🧰 Additional context used
📓 Path-based instructions (4)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Backend entrypoint in cmd/ orchestrates the scheduler and CLI; runtime, persistence, and service layers sit under internal/* (for example internal/runtime, internal/persistence)
Keep Go files gofmt/goimports clean; use tabs, PascalCase for exported symbols (SchedulerClient), lowerCamelCase for locals, and Err... names for package-level errors
Repository linting relies on golangci-lint; prefer idiomatic Go patterns, minimal global state, and structured logging helpers in internal/common

Files:

  • internal/service/frontend/auth/oidc.go
  • internal/common/config/config.go
  • internal/service/frontend/api/v2/users.go
  • internal/common/config/loader.go
  • internal/common/config/config_test.go
ui/**/*.{ts,tsx,jsx,js}

📄 CodeRabbit inference engine (ui/CLAUDE.md)

ui/**/*.{ts,tsx,jsx,js}: Use developer-centric UI design with high information density, minimal whitespace, compact components, and no unnecessary decorations
Support both light and dark modes for all UI components using Tailwind CSS class pairs like dark:bg-slate-700
NEVER use full-page loading overlays or LoadingIndicator components that hide content - show stale data while fetching updates instead
Use compact modal design with small headers, minimal padding (p-2 or p-3), tight spacing, and support keyboard navigation (arrows, enter, escape)
Use small heights for form elements: select boxes h-7 or smaller, buttons h-7 or h-8, inputs with compact padding (py-0.5 or py-1)
Minimize row heights in tables and lists while maintaining readability, merge related columns, and always handle long text with whitespace-normal break-words
Use consistent metadata styling with bg-slate-200 dark:bg-slate-700 backgrounds and maintain text hierarchy with primary/secondary/muted text colors
Use flexbox-first layouts with min-h-0 and overflow-hidden to prevent layout breaks, account for fixed elements when setting heights
Maintain keyboard navigation support in all interactive components with appropriate focus indicators and ARIA labels

Files:

  • ui/src/pages/login.tsx
  • ui/src/pages/users/index.tsx
  • ui/src/pages/users/UserFormModal.tsx
  • ui/src/pages/users/ResetPasswordModal.tsx
ui/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

ui/**/*.{ts,tsx}: The React + TypeScript frontend resides in ui/, with production bundles copied to internal/service/frontend/assets by make ui
UI code follows ESLint + Prettier (2-space indent) and Tailwind utilities; name React components in PascalCase (JobList.tsx) and hooks with use* (useJobs.ts)

Files:

  • ui/src/pages/login.tsx
  • ui/src/pages/users/index.tsx
  • ui/src/pages/users/UserFormModal.tsx
  • ui/src/pages/users/ResetPasswordModal.tsx
**/*_test.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*_test.go: Co-locate Go tests as *_test.go; favour table-driven cases and cover failure paths
Use stretchr/testify/require and shared fixtures from internal/test instead of duplicating mocks

Files:

  • internal/common/config/config_test.go
🧠 Learnings (2)
📚 Learning: 2025-12-04T10:34:01.045Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: ui/CLAUDE.md:0-0
Timestamp: 2025-12-04T10:34:01.045Z
Learning: Applies to ui/**/*.{ts,tsx,jsx,js} : Use compact modal design with small headers, minimal padding (p-2 or p-3), tight spacing, and support keyboard navigation (arrows, enter, escape)

Applied to files:

  • ui/src/pages/users/index.tsx
  • ui/src/pages/users/UserFormModal.tsx
  • ui/src/pages/users/ResetPasswordModal.tsx
📚 Learning: 2025-12-04T10:34:17.062Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.062Z
Learning: Applies to ui/**/*.{ts,tsx} : UI code follows ESLint + Prettier (2-space indent) and Tailwind utilities; name React components in PascalCase (`JobList.tsx`) and hooks with `use*` (`useJobs.ts`)

Applied to files:

  • ui/src/pages/users/UserFormModal.tsx
🧬 Code graph analysis (8)
internal/service/frontend/auth/oidc.go (2)
internal/common/logger/context.go (2)
  • Warn (45-47)
  • Error (50-52)
internal/common/logger/tag/tag.go (2)
  • Error (20-22)
  • Token (386-388)
ui/src/pages/login.tsx (1)
ui/src/contexts/AuthContext.tsx (1)
  • TOKEN_KEY (24-24)
ui/src/pages/users/index.tsx (5)
internal/auth/user.go (1)
  • User (10-35)
api/v2/api.gen.go (1)
  • User (1186-1207)
ui/src/contexts/AuthContext.tsx (1)
  • TOKEN_KEY (24-24)
ui/src/components/ui/table.tsx (5)
  • TableHead (131-131)
  • TableRow (133-133)
  • TableHeader (132-132)
  • TableBody (127-127)
  • TableCell (129-129)
ui/src/components/ui/dropdown-menu.tsx (1)
  • DropdownMenuItem (185-185)
ui/src/pages/users/UserFormModal.tsx (1)
ui/src/contexts/AppBarContext.ts (1)
  • AppBarContext (11-21)
internal/service/frontend/api/v2/users.go (2)
internal/auth/context.go (1)
  • UserFromContext (20-23)
api/v2/api.gen.go (3)
  • UserId (1366-1366)
  • Error (580-589)
  • ErrorCodeForbidden (46-46)
internal/common/config/loader.go (1)
internal/common/config/config.go (1)
  • Auth (162-168)
ui/src/pages/users/ResetPasswordModal.tsx (2)
ui/src/contexts/AppBarContext.ts (1)
  • AppBarContext (11-21)
api/v2/api.gen.go (1)
  • Error (580-589)
internal/common/config/config_test.go (1)
internal/common/config/config.go (2)
  • OIDCRoleMapping (177-190)
  • Auth (162-168)
⏰ 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). (2)
  • GitHub Check: Test on ubuntu-latest
  • GitHub Check: Build
🔇 Additional comments (32)
internal/common/config/loader.go (5)

437-439: LGTM: Flexible configuration parsing.

The use of parseStringList for Scopes and Whitelist enables both YAML list and comma-separated string formats, improving configuration flexibility.


447-447: LGTM: Consistent domain parsing.

Using parseStringList for AllowedDomains maintains consistency with the other OIDC configuration fields.


450-462: LGTM: Role mapping configuration.

The role mapping configuration is correctly loaded from the definition, including the DefaultRole field. The nil checks ensure safe access.


511-513: LGTM: Secure default role.

Defaulting to "viewer" is a secure choice, providing read-only access by default for auto-signed-up users.


443-446: The AutoSignup default to true is intentional and already documented in the code comments (config.go line 230: "Auto-create users on first login (default: true)"). This is not a breaking change—it is the designed behavior for OIDC-based authentication in builtin mode. No additional documentation action is required.

internal/service/frontend/api/v2/users.go (1)

151-164: LGTM: Self-disable protection implemented correctly.

The self-disable protection is well-implemented:

  • Checks only when attempting to disable (*request.Body.IsDisabled == true)
  • Compares current user ID with target user ID
  • Returns appropriate Forbidden error

The check gracefully handles missing user context (when ok is false), though this scenario is unlikely since requireAdmin already validates authentication.

ui/src/pages/users/ResetPasswordModal.tsx (3)

1-3: LGTM: Standard context usage.

The AppBarContext is correctly imported and consumed to access the selected remote node.

Also applies to: 38-38


77-79: LGTM: Defensive authentication check.

The early token validation prevents unnecessary API calls and provides clear error messaging when not authenticated.


80-81: LGTM: Proper remoteNode handling.

The remoteNode parameter is correctly:

  • Retrieved from context with a sensible 'local' fallback
  • URL-encoded to handle special characters
  • Appended as a query parameter
ui/src/pages/users/UserFormModal.tsx (4)

1-3: LGTM: Consistent context usage.

The AppBarContext usage is consistent with other user management components.

Also applies to: 50-50


85-88: LGTM: Consistent authentication and remoteNode handling.

The token validation and remoteNode extraction match the pattern used in other user management modals.


92-93: LGTM: Correct HTTP method and remoteNode integration.

Using PATCH for partial updates is semantically correct, and the remoteNode query parameter is properly integrated.


107-107: LGTM: remoteNode added to create endpoint.

The remoteNode parameter is consistently added to the create user endpoint.

ui/src/pages/login.tsx (5)

3-3: LGTM: Centralized token key constant.

Using TOKEN_KEY from AuthContext ensures consistent token storage key usage across the application.


34-46: LGTM: Clean OIDC callback flow.

The token callback handling is well-implemented:

  • Stores token in localStorage using the centralized key
  • Navigates immediately with replace: true to prevent back-button issues
  • Early return prevents interference with error/welcome message logic
  • Clear comment explains the flow

49-50: LGTM: Fixed double-decoding bug.

Removing decodeURIComponent is correct since searchParams.get() already decodes URL parameters. The previous code would have double-decoded, potentially causing issues with certain error messages.


55-55: LGTM: Complete effect dependencies.

Adding navigate and from to the dependency array follows React best practices, even though these values are typically stable.


85-85: LGTM: Refined background styling.

The change to bg-muted/50 provides a softer, more visually refined login page appearance while maintaining light/dark mode compatibility.

internal/service/frontend/auth/oidc.go (2)

35-39: LGTM! Sensible retry configuration.

The retry constants (3 attempts with 500ms delays) provide reasonable resilience against transient network issues during OIDC provider initialization without excessive delays.


538-539: LGTM! Consistent URL path normalization.

The trailing slash trimming ensures clean URL construction and matches the pattern used in the callback handler (line 526).

ui/src/pages/users/index.tsx (4)

59-77: LGTM! Proper remoteNode parameter handling.

The implementation correctly:

  • Defaults to 'local' when no remote node is selected
  • URL-encodes the parameter value
  • Updates the useCallback dependency array to include selectedRemoteNode

160-164: LGTM! Compact table column layout.

The updated header widths accommodate the new Auth and Status columns while maintaining the compact, high-information-density design specified in the coding guidelines.


171-183: LGTM! Correct colspan updates and disabled user styling.

The colspan values correctly reflect the new column count (7), and the opacity-60 class provides a subtle visual indicator for disabled users without cluttering the interface.


197-206: LGTM! Clear status indicators with dark mode support.

The Auth and Status columns provide clear visual indicators:

  • Authentication provider (SSO vs Local) is immediately visible
  • Disabled status uses red color coding with proper dark mode variants
  • Text hierarchy maintains readability

Based on coding guidelines requiring dark mode support for all UI components.

internal/common/config/config.go (4)

178-190: LGTM! Improved configuration structure.

Moving DefaultRole into the OIDCRoleMapping struct improves logical grouping of role-related configuration. The documentation clearly indicates the default value.


418-454: LGTM! Validation correctly updated for new structure.

The validation logic properly uses IsConfigured() for gating and accesses the nested RoleMapping.DefaultRole field. The error message accurately reflects the configuration path.


456-467: LGTM! Improved email scope validation.

The refactored validation:

  • Uses idiomatic slices.Contains for cleaner code
  • Clearly errors when email scope is required but missing (whitelist/allowedDomains configured)
  • Provides helpful warning for potential future issues

This improves the developer experience by catching configuration issues early.


217-240: LGTM! Clean auto-detection pattern.

The IsConfigured() method provides a clean API for determining OIDC availability based on required fields, eliminating the need for an explicit Enabled flag. This is more intuitive and reduces configuration errors.

The AutoSignup default is properly handled: the loader uses a nullable *bool input, applies a true default when not specified, then assigns it to the non-nullable bool field in the final config struct.

internal/common/config/config_test.go (4)

384-417: LGTM! Test correctly validates auto-detection behavior.

The renamed test clearly documents the new behavior: incomplete OIDC configuration doesn't error (OIDC simply isn't enabled). The assertion using IsConfigured() properly validates this.


419-449: LGTM! Test validates nested role mapping correctly.

The test properly uses the nested RoleMapping.DefaultRole structure and verifies the validation error message matches the implementation.


451-481: LGTM! Valid configuration test updated correctly.

The test uses the new nested RoleMapping structure and includes the email scope to ensure clean validation without warnings.


483-549: LGTM! Comprehensive email scope validation tests.

The tests correctly cover both scenarios:

  1. Missing email scope without access control → warning
  2. Missing email scope with whitelist configured → error

This validates the improved validation logic from config.go.

Comment thread internal/common/config/loader.go
Comment thread internal/service/frontend/auth/oidc.go
@yottahmd yottahmd merged commit 7fca188 into main Jan 11, 2026
6 checks passed
@yottahmd yottahmd deleted the auto-signup2 branch January 11, 2026 12:17
@codecov
Copy link
Copy Markdown

codecov Bot commented Jan 11, 2026

Codecov Report

❌ Patch coverage is 80.95238% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.58%. Comparing base (2da0188) to head (111eff5).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
internal/common/config/loader.go 76.92% 5 Missing and 1 partial ⚠️
internal/service/auth/service.go 0.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1569      +/-   ##
==========================================
- Coverage   66.61%   66.58%   -0.04%     
==========================================
  Files         247      247              
  Lines       27504    27527      +23     
==========================================
+ Hits        18321    18328       +7     
- Misses       7568     7580      +12     
- Partials     1615     1619       +4     
Files with missing lines Coverage Δ
internal/common/config/config.go 85.71% <100.00%> (-14.29%) ⬇️
internal/service/oidcprovision/service.go 76.54% <100.00%> (+0.44%) ⬆️
internal/service/auth/service.go 76.03% <0.00%> (-0.43%) ⬇️
internal/common/config/loader.go 79.17% <76.92%> (-0.05%) ⬇️

... and 2 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2da0188...111eff5. Read the comment docs.

🚀 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant