Skip to content

Conversation

@elibosley
Copy link
Member

@elibosley elibosley commented Aug 4, 2025

This will require an Account app update, as the account app is not fully OIDC compliant at this point.

  • Added new types and resolvers for managing OIDC providers, including creation, update, and deletion functionalities.
  • Introduced SSO settings in the API schema, allowing for configuration of multiple OIDC providers.
  • Enhanced the GraphQL API with queries to retrieve public and configured OIDC providers.
  • Updated the pnpm lock file to include the class-transformer package.
  • Refactored settings management to incorporate SSO-related configurations.

Summary by CodeRabbit

  • New Features

    • Full OIDC SSO: admin provider management, public provider buttons, end-to-end login flow, ephemeral session tokens and CLI token validation.
    • UPS support: configuration and device/status surfaces in UI and API.
  • UI / Settings

    • Settings UI: tabbed provider editor, accordion sections, improved form validation (AJV) and richer form controls for OIDC.
  • Documentation

    • Detailed OIDC provider setup and examples added.
  • Chores

    • Dev proxy/port and dependency updates; local config handling and gitignore improvements.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 4, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds end-to-end OIDC SSO: file-backed provider persistence and migration, discovery/validation, auth URL and callback handling, ephemeral one-time session tokens, GraphQL schema/resolvers/queries, REST/CLI integration, frontend SSO components and JSONForms enhancements, docs, and tests.

Changes

Cohort / File(s) Summary
Env & Gitignore
api/.env.development, api/.gitignore, .gitignore
Add PATHS_OIDC_JSON env var; restructure ignores to track dev/connectStatus.json, ignore dev/configs/oidc.local.json, and ignore api/dev/configs/api.json.backup.
Dev configs & docs
api/dev/configs/*
api/dev/configs/README.md, api/dev/configs/oidc.json, api/dev/configs/api.json, api/dev/configs/connect.json
Add sample oidc.json and README explaining PATHS_OIDC_JSON; minor formatting change to api.json; clear connect.json apikey and set dynamicRemoteAccessType to STATIC.
GraphQL schema & models
api/generated-schema.graphql, api/src/.../sso/*.model.ts, api/src/.../settings/*
Add SSO schema types (SsoSettings, OidcProvider, OidcAuthorizationRule, PublicOidcProvider, OidcSessionValidation), enums and inputs; add warnings to UpdateSettingsResponse; UPS-related schema additions.
SSO services & persistence
api/src/.../sso/oidc-auth.service.ts, oidc-config.service.ts, oidc-session.service.ts, oidc-validation.service.ts
New services: OidcAuthService (discovery, auth URL, callback/token exchange), OidcConfigPersistence (file-backed provider CRUD + migration), OidcSessionService (ephemeral one-time tokens), OidcValidationService (discovery validation).
SSO resolver & modules
api/src/.../sso/sso.resolver.ts, sso.module.ts, graph/resolvers/resolvers.module.ts, settings.module.ts
Add SsoResolver (public/admin queries, validateOidcSession), SsoModule; wire SsoModule into ResolversModule and SettingsModule.
Settings flow & user-settings API
packages/unraid-shared/src/services/user-settings.ts, api/src/.../settings/*
Aggregate per-fragment warnings through user-settings update flows; ApiSettings conditionally shows SSO users slice based on OIDC migration; add SsoSettings model and resolve wiring.
CLI
api/src/unraid-api/cli/queries/validate-oidc-session.query.ts, api/src/unraid-api/cli/sso/validate-token.command.ts
Add VALIDATE_OIDC_SESSION_QUERY; replace local JWT/JWKS CLI validation with GraphQL-based validateOidcSession; standardize JSON CLI outputs and exit codes.
REST controller & app module
api/src/.../rest/rest.controller.ts, rest.module.ts, api/src/unraid-api/app/app.module.ts
Inject OidcAuthService; add authorize and callback endpoints (dev and prod paths); import SsoModule into RestModule; change logger request/response serializers to return undefined.
Resolver wiring & settings UI
api/src/unraid-api/graph/resolvers/settings/*
Add SsoSettingsResolver; SettingsResolver returns sso node; include 'sso' namespace in unified settings schema calls; update updateSettings typing and surface warnings field.
Tests & test helpers
api/src/__test__/setup/*, api/src/.../sso/*.test.ts, api/src/.../sso/oidc-session.service.spec.ts, web/__test__/components/SsoButton.test.ts
Add comprehensive OIDC unit tests and session tests, API JSON backup test helper, adapt SSO UI tests to SsoButtons flow, adjust rclone-related tests.
Web UI: SSO components & composables
web/components/sso/*, web/components/SsoButton.ce.vue, web/components/queries/*, web/composables/gql/*, web/pages/login.vue, web/server/api/debug/validate-token.post.ts
Replace SsoButton with SsoButtons + SsoProviderButton; add useSsoProviders/useSsoAuth hooks; add public/admin GraphQL queries; add login debug UI and server endpoint to run CLI token validation.
Forms / JSONForms infra
unraid-ui/src/forms/*, unraid-ui/src/index.ts, usages in web components
Add AccordionLayout and ObjectArrayField, form-utils helpers, AJV config (jsonFormsAjv) and wire ajv into JsonForms usages; update renderers and re-export forms config.
Client GraphQL typings
web/composables/gql/gql.ts, web/composables/gql/graphql.ts
Add typed Documents and client types for OidcProviders/PublicOidcProviders and extend client schema/types for OIDC and UPS.
Package deps & scripts
api/package.json, web/package.json, unraid-ui/package.json, packages/unraid-shared/package.json
Add openid-client to API deps and change dev script; add ajv to web/unraid-ui deps/peer/dev; add class-transformer to shared devDeps.
Docs & public pages
api/docs/public/index.md, api/docs/public/oidc-provider-setup.md
Add OIDC Provider Setup entry in docs index and full OIDC provider setup documentation.
Misc / infra
api/src/__test__/setup/api-json-backup.ts, api/src/unraid-api/graph/resolvers/rclone/rclone-api.service.ts, api/src/unraid-api/graph/introspection-plugin.ts, api/src/unraid-api/graph/sandbox-plugin.ts, api/src/unraid-api/graph/graph.module.ts, api/src/unraid-api/main.ts
Add API JSON backup test helper; rclone URL normalization and production startup guard; add dynamic introspection plugin and sandbox plugin signature change; add pre-handler blocking GraphQL sandbox when disabled.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Web as Web (Login UI)
  participant API as API REST
  participant OIDC as OIDC Provider
  participant Auth as OidcAuthService
  participant Sess as OidcSessionService

  User->>Web: Click provider button
  Web->>API: GET /graphql/api/auth/oidc/authorize/:providerId?state=...
  API->>Auth: getAuthorizationUrl(providerId, state, host)
  Auth-->>API: authorization URL
  API-->>Web: 302 redirect to OIDC
  Web->>OIDC: Authorization request
  OIDC-->>API: Redirect /graphql/api/auth/oidc/callback?code&state
  API->>Auth: handleCallback(providerId, code, state, host, fullUrl)
  Auth->>Sess: createSession(providerId, subject)
  Sess-->>Auth: one-time token
  API-->>Web: 302 redirect to /login#token=<token>
  Web->>Web: Submit login form with token (useSsoAuth)
Loading
sequenceDiagram
  autonumber
  participant CLI as CLI (validate-token)
  participant API as API GraphQL
  participant Res as SsoResolver
  participant Sess as OidcSessionService

  CLI->>API: validateOidcSession(token)
  API->>Res: resolve validateOidcSession
  Res->>Sess: validateSession(token)
  Sess-->>Res: { valid, username? }
  Res-->>API: OidcSessionValidation
  API-->>CLI: { valid, username }
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~90 minutes

Possibly related PRs

Suggested reviewers

  • zackspear
  • mdatelle

Poem

"I hop through keys, a tiny scribe,
I stitch the buttons, mend the tribe.
Tokens puff and rules take flight,
Providers gleam in login light.
A rabbit's patch of code — SSO delight!" 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch api-feat-oidc-sso

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@claude
Copy link

claude bot commented Aug 4, 2025

Claude encountered an error —— View job


I'll analyze this and get back to you.

@coderabbitai coderabbitai bot requested review from pujitm and zackspear August 4, 2025 16:49
@github-actions
Copy link
Contributor

github-actions bot commented Aug 4, 2025

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

Copy link
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: 25

🔭 Outside diff range comments (1)
packages/unraid-shared/package.json (1)

29-38: class-transformer must be a runtime / peer dependency, not merely a dev dependency

class-transformer is required at runtime whenever decorated DTOs/entities from @unraid/shared are instantiated, but it is declared here only under devDependencies.
Down-stream packages that install @unraid/shared in production will therefore miss the module and crash with ERR_MODULE_NOT_FOUND.

Action:

-  "devDependencies": {
-
-      "class-transformer": "0.5.1",
-
-  },
+  "dependencies": {
+      "class-transformer": "0.5.1"
+  },
+  "peerDependencies": {
+      …,
+      "class-transformer": "0.5.1",
+
+  },

Move it to dependencies (or at minimum peerDependencies) and bump the lock-file accordingly.

🧹 Nitpick comments (17)
api/src/unraid-api/graph/resolvers/resolvers.module.ts (1)

30-52: Importing SsoModule looks good, check circular-dependency risk

SsoModule is injected into the main resolver tree. As it depends on AuthModule internally, make sure there is no circular import between the two (Nest will warn at runtime). If none, this addition is fine.

api/.gitignore (1)

86-88: Redundant ignore rule – already covered by dev/configs/*

dev/configs/* (Line 90) already excludes every file in that folder, so explicitly listing dev/configs/oidc.local.json is unnecessary noise.

Apply:

-# local OIDC config for testing - contains secrets
-dev/configs/oidc.local.json
api/.env.development (1)

20-20: Keep .env keys sorted to silence dotenv-linter

PATHS_OIDC_JSON is out of alphabetical order, triggering the linter warning. Move it above PATHS_PARITY_CHECKS to stay consistent with the rest of the file.

web/components/queries/oidc-providers.query.ts (1)

3-22: Consider adding __typename for better Apollo cache normalization

Including __typename in the selection set helps Apollo identify the object type and avoids cache misses when the same query is executed elsewhere with __typename present.

           buttonIcon
+          __typename
api/src/unraid-api/graph/resolvers/sso/oidc-session-validation.model.ts (1)

3-10: Optional: add class-level description for schema clarity

A short description via @ObjectType({ description: '...' }) makes the generated GraphQL schema self-documenting and improves SDL readability.

-@ObjectType()
+@ObjectType({ description: 'Result of validating an OIDC session token' })
api/src/unraid-api/cli/queries/validate-oidc-session.query.ts (1)

3-10: Add __typename to avoid Apollo cache collisions

For consistency with other queries and safer cache behaviour, include __typename:

             username
+            __typename
web/server/api/debug/validate-token.post.ts (1)

31-41: Improve JSON parsing robustness.

The current JSON parsing logic could be more robust and easier to maintain.

- // Look for JSON output from the end
- for (let i = lines.length - 1; i >= 0; i--) {
-   const line = lines[i].trim();
-   if (line.startsWith('{') && line.endsWith('}')) {
-     try {
-       parsedOutput = JSON.parse(line);
-       break;
-     } catch {
-       // Continue looking
-     }
-   }
- }
+ // Look for JSON output from the end
+ parsedOutput = lines
+   .reverse()
+   .find(line => {
+     const trimmed = line.trim();
+     if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
+       try {
+         return JSON.parse(trimmed);
+       } catch {
+         return false;
+       }
+     }
+     return false;
+   });
web/components/sso/SsoProviderButton.vue (2)

34-41: Improve accessibility for provider icons.

The alt text should be more descriptive for screen readers.

    <img 
      v-if="provider.buttonIcon" 
      :src="provider.buttonIcon" 
      class="w-5 h-5 mr-2" 
-     :alt="provider.name"
+     :alt="`${provider.name} logo`"
    >

44-57: Consider reducing !important declarations in CSS.

Excessive use of !important can indicate styling conflicts and make the component harder to customize.

Consider using more specific CSS selectors or CSS modules to avoid the need for !important declarations. This would make the component more maintainable and easier to customize.

web/components/sso/useSsoProviders.ts (2)

54-62: Consider implementing exponential backoff for polling.

Polling every 2 seconds indefinitely could be resource-intensive, especially if the API takes longer to become available.

+ const pollDelay = ref(2000);
+ const maxPollDelay = 30000; // 30 seconds max
+
  onMounted(() => {
    checkApiAvailability();
-   // Poll every 2 seconds if API is not available
+   // Poll with exponential backoff if API is not available
    pollInterval.value = setInterval(() => {
      if (!apiAvailable.value) {
        checkApiAvailability();
+       // Increase delay up to maximum
+       pollDelay.value = Math.min(pollDelay.value * 1.5, maxPollDelay);
      }
-   }, 2000);
+   }, pollDelay.value);
  });

20-27: Consider using cache-first fetch policy.

The network-only fetch policy might be too aggressive and could impact performance.

  const { result: providersResult, refetch: refetchProviders } = useQuery(
    PUBLIC_OIDC_PROVIDERS, 
    null, 
    {
-     fetchPolicy: 'network-only',
+     fetchPolicy: 'cache-first',
      errorPolicy: 'all',
    }
  );

This would allow the component to use cached data when available while still allowing manual refetches during polling.

api/src/unraid-api/graph/resolvers/settings/settings.service.ts (1)

81-83: Clean up commented code

The commented extraOriginsSlice code should either be removed or properly documented with a TODO/FIXME comment explaining the retention reason.

-    // Because CORS is effectively disabled, this setting is no longer necessary
-    // keeping it here for in case it needs to be re-enabled
-    // slices.push(this.extraOriginsSlice());
+    // TODO: CORS settings temporarily disabled - may need to re-enable if CORS restrictions are reinstated
+    // slices.push(this.extraOriginsSlice());

Or remove entirely if the feature is permanently deprecated.

web/components/sso/SsoButtons.vue (1)

43-45: Remove empty style section

The empty style section can be removed if no component-specific styles are needed.

Remove lines 43-45 entirely if no styles are planned.

api/src/unraid-api/graph/resolvers/sso/oidc-session.service.ts (1)

83-100: Consider more flexible UUID extraction

The regex pattern ^OIDC-SESSION-([a-f0-9-]+)- only matches lowercase hex characters, but UUIDs can contain uppercase letters when using certain UUID versions or formats.

Consider using a more flexible pattern:

-            const match = signature.match(/^OIDC-SESSION-([a-f0-9-]+)-/);
+            const match = signature.match(/^OIDC-SESSION-([a-fA-F0-9-]+)-/);
web/pages/login.vue (1)

130-172: Consider moving debug tools to a separate development-only page

While the debug modal is useful for development, having it on the production login page could confuse users or expose implementation details.

Consider:

  1. Adding an environment check to only show the debug functionality in development
  2. Moving this to a separate /debug/sso route
  3. Adding a feature flag to control visibility

Would you like me to help implement a development-only flag for this feature?

api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts (1)

169-177: Add comment clarifying JWT parsing is for claims extraction only

The manual JWT parsing without verification could be misunderstood as a security issue. Add a comment to clarify that signature verification is handled by openid-client.

Add clarifying comment:

             // Parse ID token to get user info
             let claims: { sub?: string } | null = null;
             if (tokens.id_token) {
                 try {
-                    // Decode the JWT manually
+                    // Decode the JWT manually for claims extraction only
+                    // Note: Signature verification is already done by openid-client
                     const payload = tokens.id_token.split('.')[1];
                     claims = JSON.parse(Buffer.from(payload, 'base64').toString());
                     this.logger.debug(`ID token claims: ${JSON.stringify(claims)}`);
api/generated-schema.graphql (1)

1403-1409: Consider whether SsoSettings really needs to be a top-level Node.

​SsoSettings is only ever accessed through the Settings root-object. Exposing it as a first-class Node (with its own globally unique ID) increases API surface and resolver work for no practical gain. If there is no use-case for fetching an SsoSettings instance by ID, dropping implements Node here will keep the schema smaller and simpler.

@coderabbitai coderabbitai bot requested a review from mdatelle August 9, 2025 02:06
@github-actions
Copy link
Contributor

github-actions bot commented Aug 9, 2025

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

Copy link
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: 3

♻️ Duplicate comments (12)
api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts (1)

101-110: cleanProvider still omits several optional fields

jwksUri, buttonText, and buttonVariant are not normalised, so empty-string values survive persistence and re-appear in the UI. Please trim/undefine them exactly like the other optional strings.

-            buttonIcon: provider.buttonIcon?.trim() || undefined,
-            buttonStyle: provider.buttonStyle?.trim() || undefined,
+            jwksUri: provider.jwksUri?.trim() || undefined,
+            buttonIcon: provider.buttonIcon?.trim() || undefined,
+            buttonText: provider.buttonText?.trim() || undefined,
+            buttonVariant: provider.buttonVariant?.trim() || undefined,
+            buttonStyle: provider.buttonStyle?.trim() || undefined,
api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts (1)

339-348: Refactor hard-coded redirect URI logic

Previous feedback about configurability remains unaddressed – hard-coded localhost detection and default tower.local will break behind reverse proxies or non-standard ports. Delegate to config values (OIDC_CALLBACK_URL, etc.) for flexibility.

web/pages/login.vue (2)

38-42: Do not echo plaintext passwords in debug modal

Showing the submitted password jeopardises users’ credentials. Mask or omit it as previously suggested.


50-70: Fix TS2339 by typing $fetch response

The pipeline error persists. Define ValidateTokenResponse (with optional stdout, error, etc.) and use $fetch<ValidateTokenResponse>() or assert the type before accessing stdout.

api/src/unraid-api/rest/rest.controller.ts (2)

69-88: Validate providerId input (defense-in-depth).

Add a simple allowlist check for providerId before passing to the service to reduce attack surface.

     ) {
       try {
         if (!state) {
           return res.status(400).send('State parameter is required');
         }
+
+        // Validate providerId format (alphanumeric, dot, underscore, dash, max 64)
+        if (!/^[A-Za-z0-9._-]{1,64}$/.test(providerId)) {
+          return res.status(400).send('Invalid provider ID format');
+        }

133-159: Simplify and sanitize error handling to avoid leaking details.

The deep inspection/logging of UnauthorizedException response increases exposure risk. Keep user-facing message minimal and log only safe info.

-            if (error instanceof UnauthorizedException) {
-                // NestJS exceptions store the message in the response property
-                const response = error.getResponse();
-                this.logger.debug(`UnauthorizedException response: ${JSON.stringify(response)}`);
-                this.logger.debug(`UnauthorizedException message: ${error.message}`);
-
-                // Try to get the message from various places
-                if (typeof response === 'string') {
-                    errorMessage = response;
-                } else if (typeof response === 'object' && response !== null) {
-                    // Check for message in response object
-                    if ('message' in response && response.message) {
-                        errorMessage = Array.isArray(response.message)
-                            ? response.message[0]
-                            : String(response.message);
-                    } else if ('error' in response && response.error) {
-                        errorMessage = String(response.error);
-                    }
-                }
-
-                // If we still don't have a message, use the error's message property
-                if (errorMessage === 'Authentication failed' && error.message) {
-                    errorMessage = error.message;
-                }
-            } else if (error instanceof Error) {
-                errorMessage = error.message;
-            }
+            if (error instanceof UnauthorizedException) {
+                // Map to safe, user-friendly messages only
+                const resp = error.getResponse() as any;
+                const msg = typeof resp === 'string' ? resp : resp?.message;
+                if (typeof msg === 'string') {
+                  if (msg.includes('authorization requirements')) {
+                    errorMessage = 'Access denied: Authorization requirements not met';
+                  } else if (msg.includes('Provider') && msg.includes('not found')) {
+                    errorMessage = 'Invalid authentication provider';
+                  }
+                }
+            } else if (error instanceof Error && error.message) {
+                // Safe generic error text
+                errorMessage = 'Authentication failed';
+            }
api/docs/public/oidc-provider-examples.md (1)

17-20: Emphasize HTTPS requirement for production redirect URIs.

Reinforce secure redirects immediately after examples.

 - Local development: `http://localhost:3001/graphql/api/auth/oidc/callback`
 - Production: `http://192.168.1.100:3001/graphql/api/auth/oidc/callback`
 - With custom domain: `https://unraid.example.com/graphql/api/auth/oidc/callback`
+
+⚠️ Security Warning: Always use HTTPS for production redirect URIs to prevent token interception. HTTP should only be used for local development.
api/generated-schema.graphql (4)

1463-1522: Do not expose clientSecret and implement Node on OidcProvider.

Prevent accidental secret leakage and align with Node-based resources.

-type OidcProvider {
+type OidcProvider implements Node {
   """The unique identifier for the OIDC provider"""
   id: PrefixedID!
@@
-  """OAuth2 client secret (if required by provider)"""
-  clientSecret: String
+  # clientSecret intentionally omitted from API output

1942-1953: Add auth directives to admin/internal queries and fix id type.

Secure provider/config/session endpoints and use consistent ID typing.

   """Get public OIDC provider information for login buttons"""
   publicOidcProviders: [PublicOidcProvider!]!
 
   """Get all configured OIDC providers (admin only)"""
-  oidcProviders: [OidcProvider!]!
+  oidcProviders: [OidcProvider!]! @usePermissions(action: READ, resource: "SSO", possession: ANY)
 
   """Get a specific OIDC provider by ID"""
-  oidcProvider(id: String!): OidcProvider
+  oidcProvider(id: PrefixedID!): OidcProvider @usePermissions(action: READ, resource: "SSO", possession: ANY)
 
   """Validate an OIDC session token (internal use for CLI validation)"""
-  validateOidcSession(token: String!): OidcSessionValidation!
+  validateOidcSession(token: String!): OidcSessionValidation! @usePermissions(action: READ, resource: "SSO", possession: OWN)

1999-2004: Enforce permissions on mutations and use PrefixedID for delete.

Match API conventions and protect update/delete.

   """Create or update an OIDC provider"""
-  upsertOidcProvider(provider: OidcProviderInput!): OidcProvider!
+  upsertOidcProvider(provider: OidcProviderInput!): OidcProvider! @usePermissions(action: UPDATE, resource: "SSO", possession: ANY)
 
   """Delete an OIDC provider"""
-  deleteOidcProvider(id: String!): Boolean!
+  deleteOidcProvider(id: PrefixedID!): Boolean! @usePermissions(action: DELETE, resource: "SSO", possession: ANY)

2046-2105: Make id optional in OidcProviderInput to support create vs. update.

Standard upsert pattern: absent id → create; present id → update.

 input OidcProviderInput {
   """The unique identifier for the OIDC provider"""
-  id: String!
+  id: String
web/composables/gql/graphql.ts (1)

1145-1175: Avoid querying clientSecret and sanitize buttonStyle downstream.

The generated type includes clientSecret, but your queries wisely don’t select it. Keep it that way; after server-side removal/masking, this type will update. Also, ensure any use of buttonStyle in the UI strips dangerous tokens like url(, @import, expression( to mitigate CSS injection.

🧹 Nitpick comments (7)
api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts (1)

149-160: Normalise before persisting

cleanedProvider is built manually and may still carry stray empty strings. Re-use cleanProvider (or extract a shared helper) before writing to disk to guarantee consistency between read & write paths.

api/.env.development (1)

20-20: Keep .env keys ordered to satisfy dotenv-linter

Move PATHS_OIDC_JSON above PATHS_PARITY_CHECKS (Line 12) to maintain alphabetical grouping and silence the linter warning.

api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts (1)

90-91: Replace hard-coded JWT with obvious dummy token

The long JWT-looking string triggers secret-scanners. Shorten it (e.g. DUMMY.JWT.TOKEN) – functionality of the test doesn’t depend on token contents.

api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts (1)

170-176: Avoid logging raw ID-token claims

this.logger.debug prints the full JWT claims, potentially leaking PII in logs. Mask or omit sensitive fields before logging.

api/docs/public/oidc-provider-examples.md (1)

297-306: Align documented operators with schema support.

Docs list regex/in operators that are not in the current schema. Clarify supported operators or add a note.

-  - `regex`: Regular expression match
-  - `in`: Claim value must be in the provided list
+  (Currently supported: `equals`, `contains`, `endsWith`, `startsWith`)

If regex/in are intended, extend the schema’s AuthorizationOperator accordingly.

api/generated-schema.graphql (1)

1456-1461: Optionally extend AuthorizationOperator if docs require it.

If you want regex/in semantics, add them here; otherwise keep docs limited.

 enum AuthorizationOperator {
   EQUALS
   CONTAINS
   ENDS_WITH
   STARTS_WITH
+  # REGEX
+  # IN
 }
web/composables/gql/graphql.ts (1)

1013-1016: Re-run GraphQL codegen after installing dependencies

The deleteOidcProvider input type should switch to PrefixedID once the server schema is updated. Currently in web/composables/gql/graphql.ts (lines 1013–1016):

export type MutationDeleteOidcProviderArgs = {
  id: Scalars['String']['input'];
};

Please verify the following after your schema fixes:

  • Ensure dependencies are installed (so graphql-codegen is available):
    pnpm install
  • Regenerate types/docs in the web package:
    pnpm --filter web codegen
  • Confirm that MutationDeleteOidcProviderArgs.id now uses PrefixedID instead of String.
  • Repeat verification for lines 1063–1066 and 1415–1417 in the same file.
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5c9b15b and 67b4a0c.

⛔ Files ignored due to path filters (3)
  • api/src/unraid-api/cli/generated/gql.ts is excluded by !**/generated/**
  • api/src/unraid-api/cli/generated/graphql.ts is excluded by !**/generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (48)
  • api/.env.development (1 hunks)
  • api/.gitignore (1 hunks)
  • api/dev/configs/README.md (1 hunks)
  • api/dev/configs/api.json (1 hunks)
  • api/dev/configs/connect.json (1 hunks)
  • api/dev/configs/oidc.json (1 hunks)
  • api/docs/public/index.md (1 hunks)
  • api/docs/public/oidc-provider-examples.md (1 hunks)
  • api/generated-schema.graphql (5 hunks)
  • api/package.json (1 hunks)
  • api/src/unraid-api/app/app.module.ts (1 hunks)
  • api/src/unraid-api/cli/queries/validate-oidc-session.query.ts (1 hunks)
  • api/src/unraid-api/cli/sso/validate-token.command.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/settings/settings.model.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/settings/settings.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/settings/settings.resolver.ts (5 hunks)
  • api/src/unraid-api/graph/resolvers/settings/settings.service.ts (4 hunks)
  • api/src/unraid-api/graph/resolvers/settings/sso-settings.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.test.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/oidc-provider.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/oidc-session-validation.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/public-oidc-provider.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/sso-settings.types.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/sso.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/utils/form-utils.ts (1 hunks)
  • api/src/unraid-api/rest/rest.controller.ts (2 hunks)
  • api/src/unraid-api/rest/rest.module.ts (1 hunks)
  • packages/unraid-shared/package.json (1 hunks)
  • unraid-ui/src/forms/ObjectArrayField.vue (1 hunks)
  • unraid-ui/src/forms/renderers.ts (4 hunks)
  • web/components/SsoButton.ce.vue (1 hunks)
  • web/components/queries/oidc-providers.query.ts (1 hunks)
  • web/components/queries/public-oidc-providers.query.ts (1 hunks)
  • web/components/sso/SsoButtons.vue (1 hunks)
  • web/components/sso/SsoProviderButton.vue (1 hunks)
  • web/components/sso/useSsoAuth.ts (1 hunks)
  • web/components/sso/useSsoProviders.ts (1 hunks)
  • web/composables/gql/gql.ts (3 hunks)
  • web/composables/gql/graphql.ts (16 hunks)
  • web/nuxt.config.ts (1 hunks)
  • web/pages/login.vue (9 hunks)
  • web/server/api/debug/validate-token.post.ts (1 hunks)
✅ Files skipped from review due to trivial changes (7)
  • api/package.json
  • api/src/unraid-api/rest/rest.module.ts
  • web/components/SsoButton.ce.vue
  • api/dev/configs/connect.json
  • api/.gitignore
  • api/src/unraid-api/graph/resolvers/sso/oidc-session-validation.model.ts
  • api/src/unraid-api/graph/resolvers/sso/public-oidc-provider.model.ts
🚧 Files skipped from review as they are similar to previous changes (32)
  • packages/unraid-shared/package.json
  • api/docs/public/index.md
  • api/src/unraid-api/app/app.module.ts
  • api/dev/configs/api.json
  • api/src/unraid-api/graph/resolvers/settings/settings.module.ts
  • web/nuxt.config.ts
  • api/src/unraid-api/cli/queries/validate-oidc-session.query.ts
  • web/components/sso/useSsoProviders.ts
  • api/src/unraid-api/graph/resolvers/settings/sso-settings.model.ts
  • web/components/queries/oidc-providers.query.ts
  • api/src/unraid-api/graph/utils/form-utils.ts
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts
  • web/components/sso/SsoProviderButton.vue
  • web/server/api/debug/validate-token.post.ts
  • api/src/unraid-api/cli/sso/validate-token.command.ts
  • api/dev/configs/oidc.json
  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.ts
  • api/src/unraid-api/graph/resolvers/settings/settings.model.ts
  • api/src/unraid-api/graph/resolvers/sso/sso-settings.types.ts
  • web/components/queries/public-oidc-providers.query.ts
  • unraid-ui/src/forms/ObjectArrayField.vue
  • api/src/unraid-api/graph/resolvers/sso/sso.module.ts
  • api/dev/configs/README.md
  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.test.ts
  • web/composables/gql/gql.ts
  • api/src/unraid-api/graph/resolvers/settings/settings.resolver.ts
  • unraid-ui/src/forms/renderers.ts
  • web/components/sso/useSsoAuth.ts
  • web/components/sso/SsoButtons.vue
  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts
  • api/src/unraid-api/graph/resolvers/settings/settings.service.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-provider.model.ts
🧰 Additional context used
📓 Path-based instructions (4)
api/**/*

📄 CodeRabbit Inference Engine (.cursor/rules/api-rules.mdc)

api/**/*: Use pnpm as the only package manager
Always run scripts from api/package.json unless specifically requested otherwise

Files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
  • api/docs/public/oidc-provider-examples.md
  • api/src/unraid-api/rest/rest.controller.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
  • api/generated-schema.graphql
  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts
api/src/unraid-api/**/*

📄 CodeRabbit Inference Engine (.cursor/rules/api-rules.mdc)

Prefer adding new files to the NestJS repository located at api/src/unraid-api/ instead of the legacy code

Prefer adding new files to the NestJS repo located at api/src/unraid-api/ instead of the legacy code

Files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
  • api/src/unraid-api/rest/rest.controller.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts
api/**/*.{test,spec}.{js,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/api-rules.mdc)

api/**/*.{test,spec}.{js,ts,tsx}: Use Vitest as the test suite; do not use Jest
Prefer not to mock simple dependencies in tests

Files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts
api/**/*.{test,spec}.{ts,js}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Test suite is VITEST, do not use jest in the API package

Files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts
🧠 Learnings (74)
📓 Common learnings
Learnt from: mdatelle
PR: unraid/api#942
File: api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts:111-113
Timestamp: 2024-11-06T20:59:25.809Z
Learning: In the Unraid API project, error handling for mutations is handled at the service level rather than in the GraphQL resolvers. Specifically, in `api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts`, methods like `removeRoleFromApiKey` rely on service-level error handling.
📚 Learning: 2024-11-05T14:49:07.308Z
Learnt from: elibosley
PR: unraid/api#942
File: api/src/unraid-api/auth/api-key.service.ts:62-70
Timestamp: 2024-11-05T14:49:07.308Z
Learning: In `api/src/unraid-api/auth/api-key.service.ts`, when handling read errors in the `findById` method, throw a `GraphQLError` instead of an `InternalServerErrorException`.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
  • web/pages/login.vue
  • api/src/unraid-api/rest/rest.controller.ts
📚 Learning: 2024-11-06T20:59:25.809Z
Learnt from: mdatelle
PR: unraid/api#942
File: api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts:111-113
Timestamp: 2024-11-06T20:59:25.809Z
Learning: In the Unraid API project, error handling for mutations is handled at the service level rather than in the GraphQL resolvers. Specifically, in `api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts`, methods like `removeRoleFromApiKey` rely on service-level error handling.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
  • web/pages/login.vue
  • api/src/unraid-api/rest/rest.controller.ts
  • api/generated-schema.graphql
📚 Learning: 2024-11-04T22:00:41.946Z
Learnt from: mdatelle
PR: unraid/api#942
File: api/src/unraid-api/auth/auth.service.ts:42-105
Timestamp: 2024-11-04T22:00:41.946Z
Learning: In the 'AuthService' within `api/src/unraid-api/auth/auth.service.ts`, rate limiting is not required because the API is not public facing.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
  • api/src/unraid-api/rest/rest.controller.ts
📚 Learning: 2025-04-23T20:19:42.542Z
Learnt from: pujitm
PR: unraid/api#1367
File: packages/unraid-api-plugin-connect/src/pubsub/user.service.ts:44-52
Timestamp: 2025-04-23T20:19:42.542Z
Learning: The project uses a custom or extended implementation of NestJS ConfigService that includes a `set()` method for runtime configuration mutation, unlike the standard nestjs/config package which only provides getter methods.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
📚 Learning: 2024-11-15T16:22:12.460Z
Learnt from: elibosley
PR: unraid/api#942
File: api/src/unraid-api/auth/auth.module.ts:94-96
Timestamp: 2024-11-15T16:22:12.460Z
Learning: In `CookieService.defaultOpts()` in `api/src/unraid-api/auth/cookie.service.ts`, use `sameSite: 'lax'` for session cookie configuration.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
📚 Learning: 2025-03-04T14:55:00.903Z
Learnt from: elibosley
PR: unraid/api#1151
File: plugin/builder/utils/consts.ts:6-6
Timestamp: 2025-03-04T14:55:00.903Z
Learning: The startingDir constant in plugin/builder/utils/consts.ts is defined using process.cwd(), which can cause issues if directory changes occur after importing this constant. Using __dirname.split('/builder')[0] would make it more reliable by making it relative to the file location rather than the current working directory.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
📚 Learning: 2025-07-24T18:48:44.035Z
Learnt from: mdatelle
PR: unraid/api#1533
File: web/components/Docker/Edit.vue:16-32
Timestamp: 2025-07-24T18:48:44.035Z
Learning: In web/components/Docker/Edit.vue, the hardcoded configuration values in the config ref are intentional temporary/dummy data used during initial UI development phase while building out the real Docker components, as clarified by mdatelle in PR #1533.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
  • web/pages/login.vue
📚 Learning: 2025-05-08T19:28:37.034Z
Learnt from: elibosley
PR: unraid/api#1381
File: plugin/builder/cli/common-environment.ts:18-22
Timestamp: 2025-05-08T19:28:37.034Z
Learning: In plugin/builder/cli/common-environment.ts, the getDefaultBaseUrl() function intentionally returns an invalid URL when running in CI environments to force explicit specification of the base URL parameter rather than relying on defaults.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
📚 Learning: 2025-01-27T14:57:46.617Z
Learnt from: elibosley
PR: unraid/api#1063
File: web/_data/serverState.ts:137-147
Timestamp: 2025-01-27T14:57:46.617Z
Learning: The values in `web/_data/serverState.ts` are used for testing purposes and should remain as hardcoded mock data to facilitate testing different scenarios.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
📚 Learning: 2025-03-10T18:43:13.016Z
Learnt from: mdatelle
PR: unraid/api#1219
File: api/src/unraid-api/types/fastify.ts:25-30
Timestamp: 2025-03-10T18:43:13.016Z
Learning: In the Unraid API, cookies should be marked as optional (with `?`) in the FastifyRequest type definition since the cookies may not always be present, which is consistent with the authentication flow throughout the codebase.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
  • api/docs/public/oidc-provider-examples.md
  • api/src/unraid-api/rest/rest.controller.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
  • api/generated-schema.graphql
📚 Learning: 2024-11-18T20:52:47.800Z
Learnt from: elibosley
PR: unraid/api#963
File: web/helpers/markdown.ts:1-13
Timestamp: 2024-11-18T20:52:47.800Z
Learning: All configurations and direct usage of `marked` should be centralized in the `safeParseMarkdown` function in `web/helpers/markdown.ts` to maintain security and consistency.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
📚 Learning: 2025-04-25T13:29:07.868Z
Learnt from: elibosley
PR: unraid/api#1369
File: web/components/Activation/WelcomeModal.ce.vue:40-40
Timestamp: 2025-04-25T13:29:07.868Z
Learning: The `#confirmPassword` selector in the WelcomeModal.ce.vue component is intentionally looking for an element that exists in the Unraid OS login page (.login.php), not within the component itself. This is used as part of a font-size adjustment workaround to handle different CSS between login and authenticated pages.

Applied to files:

  • web/pages/login.vue
📚 Learning: 2025-02-18T17:36:22.670Z
Learnt from: pujitm
PR: unraid/api#1143
File: web/components/DummyServerSwitcher.vue:16-19
Timestamp: 2025-02-18T17:36:22.670Z
Learning: The DummyServerSwitcher component in web/components/DummyServerSwitcher.vue is a development tool for testing different server states, and includes intentional debug elements to aid development.

Applied to files:

  • web/pages/login.vue
📚 Learning: 2025-07-24T18:48:43.590Z
Learnt from: mdatelle
PR: unraid/api#1533
File: web/components/Docker/Console.vue:22-29
Timestamp: 2025-07-24T18:48:43.590Z
Learning: The Docker Console component in web/components/Docker/Console.vue is temporary/dummy code with placeholder terminal responses, used for building out the UI layout while developing the real components in PR #1533.

Applied to files:

  • web/pages/login.vue
📚 Learning: 2025-01-27T14:31:42.305Z
Learnt from: elibosley
PR: unraid/api#1063
File: web/components/SsoButton.ce.vue:5-8
Timestamp: 2025-01-27T14:31:42.305Z
Learning: In the Unraid API web components, SSO-related props are intentionally provided in both camelCase (`ssoEnabled`) and lowercase (`ssoenabled`) variants to support interchangeable usage across different contexts (e.g., HTML attributes vs Vue props).

Applied to files:

  • web/pages/login.vue
  • api/docs/public/oidc-provider-examples.md
  • web/composables/gql/graphql.ts
📚 Learning: 2025-07-21T14:01:04.139Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-07-21T14:01:04.139Z
Learning: Applies to web/__test__/components/**/*.ts : Use `mount` from Vue Test Utils for component testing

Applied to files:

  • web/pages/login.vue
📚 Learning: 2025-02-06T17:24:58.784Z
Learnt from: mdatelle
PR: unraid/api#1122
File: web/components/UserProfile/DropdownLaunchpad.vue:38-42
Timestamp: 2025-02-06T17:24:58.784Z
Learning: In the DropdownLaunchpad.vue component, the current implementation of conditional icon rendering using `h(BrandLoading, { variant: 'white' })` is a temporary fix to maintain build stability, with a proper refactoring being handled in a separate branch.

Applied to files:

  • web/pages/login.vue
📚 Learning: 2025-05-23T20:11:08.857Z
Learnt from: mdatelle
PR: unraid/api#1362
File: api/src/unraid-api/graph/resolvers/rclone/rclone-api.service.ts:421-424
Timestamp: 2025-05-23T20:11:08.857Z
Learning: When logging parameters or request data in error messages, always sanitize sensitive information by checking for common sensitive parameter names (pass, secret, token, key) and replacing their values with '***REDACTED***' or similar placeholders to prevent credential exposure in logs.

Applied to files:

  • web/pages/login.vue
  • api/docs/public/oidc-provider-examples.md
  • api/src/unraid-api/rest/rest.controller.ts
📚 Learning: 2025-01-15T21:34:00.006Z
Learnt from: pujitm
PR: unraid/api#1047
File: api/src/unraid-api/graph/sandbox-plugin.ts:57-57
Timestamp: 2025-01-15T21:34:00.006Z
Learning: In the GraphQL sandbox (api/src/unraid-api/graph/sandbox-plugin.ts), CSRF token validation should fail silently with a fallback value to maintain sandbox accessibility, as it's a development tool where strict security measures aren't required.

Applied to files:

  • web/pages/login.vue
  • api/docs/public/oidc-provider-examples.md
  • web/composables/gql/graphql.ts
  • api/generated-schema.graphql
📚 Learning: 2025-03-07T19:29:12.540Z
Learnt from: pujitm
PR: unraid/api#1219
File: api/src/unraid-api/main.ts:18-25
Timestamp: 2025-03-07T19:29:12.540Z
Learning: When handling type mismatches between dependencies in TypeScript (such as between NestJS and Fastify), prefer using `ts-expect-error` with a detailed explanation comment instead of `as any` type assertions. This better documents the reason for the type bypass and makes the issue more visible for future maintenance.

Applied to files:

  • web/pages/login.vue
  • api/src/unraid-api/rest/rest.controller.ts
📚 Learning: 2025-02-06T17:25:45.397Z
Learnt from: mdatelle
PR: unraid/api#1122
File: web/package.json:16-16
Timestamp: 2025-02-06T17:25:45.397Z
Learning: The build script in web/package.json should include type-check command (`npm run type-check`) even when NODE_ENV is production, as it provides value for local builds by catching type errors before pushing to CI/CD.

Applied to files:

  • web/pages/login.vue
📚 Learning: 2025-07-21T14:01:04.139Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-07-21T14:01:04.139Z
Learning: Applies to web/__test__/store/**/*.ts : Verify proper error handling in store tests

Applied to files:

  • web/pages/login.vue
  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts
📚 Learning: 2025-01-28T16:39:50.278Z
Learnt from: elibosley
PR: unraid/api#1078
File: api/src/unraid-api/unraid-file-modifier/modifications/sso.modification.ts:35-36
Timestamp: 2025-01-28T16:39:50.278Z
Learning: When reviewing TypeScript files containing PHP code snippets intended for file modifications (like in `SSOFileModification`), PHP functions that appear undefined should not be flagged as issues since they will be available in the target PHP environment where the code is injected.

Applied to files:

  • web/pages/login.vue
  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
📚 Learning: 2025-07-21T14:01:04.139Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-07-21T14:01:04.139Z
Learning: Applies to web/__test__/components/**/*.ts : Use `flushPromises()` for more complex promise chains in async tests

Applied to files:

  • web/pages/login.vue
📚 Learning: 2025-07-21T14:01:04.139Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-07-21T14:01:04.139Z
Learning: Applies to web/__test__/components/**/*.ts : Check for expected prop handling and event emissions in component tests

Applied to files:

  • web/pages/login.vue
📚 Learning: 2024-12-11T17:20:09.604Z
Learnt from: pujitm
PR: unraid/api#978
File: web/helpers/apollo-cache/index.ts:133-157
Timestamp: 2024-12-11T17:20:09.604Z
Learning: In `web/helpers/apollo-cache/index.ts`, when updating the cache in the `cache.updateQuery` function, it's sufficient to check if `data` is present, as type checking inside `data` is handled at the GraphQL layer.

Applied to files:

  • web/pages/login.vue
📚 Learning: 2024-12-09T15:45:46.492Z
Learnt from: pujitm
PR: unraid/api#975
File: web/components/Notifications/TabList.vue:1-4
Timestamp: 2024-12-09T15:45:46.492Z
Learning: In our Nuxt.js setup for the `web` project, it's not necessary to explicitly import `computed` from `vue` in Vue components, as it's globally available.

Applied to files:

  • web/pages/login.vue
📚 Learning: 2024-12-09T15:47:29.325Z
Learnt from: pujitm
PR: unraid/api#975
File: web/components/Notifications/TabList.vue:1-24
Timestamp: 2024-12-09T15:47:29.325Z
Learning: In our Nuxt setup using Vue.js, components defined within the codebase are autoloaded and do not require explicit import statements in the script section. For example, components like `TabsList` and `TabsTrigger` used in `web/components/Notifications/TabList.vue` are automatically available without imports.

Applied to files:

  • web/pages/login.vue
📚 Learning: 2024-12-06T17:34:16.133Z
Learnt from: pujitm
PR: unraid/api#974
File: web/components/Loading/Error.vue:50-50
Timestamp: 2024-12-06T17:34:16.133Z
Learning: In this project, the `Button` component from `~/components/shadcn/Button.vue` is autoloaded and does not need to be imported manually in components like `web/components/Loading/Error.vue`.

Applied to files:

  • web/pages/login.vue
📚 Learning: 2024-12-17T13:55:42.068Z
Learnt from: elibosley
PR: unraid/api#972
File: web/components/ColorSwitcher.ce.vue:1-2
Timestamp: 2024-12-17T13:55:42.068Z
Learning: In this Nuxt.js project, components used in templates are automatically imported by Nuxt, so explicit import statements for components are unnecessary.

Applied to files:

  • web/pages/login.vue
📚 Learning: 2025-07-28T15:34:24.715Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-07-28T15:34:24.715Z
Learning: Applies to web/**/store/**/*.{ts,js} : Ensure Vue reactivity imports are added to store files (computed, ref, watchEffect)

Applied to files:

  • web/pages/login.vue
📚 Learning: 2025-03-27T23:52:57.888Z
Learnt from: zackspear
PR: unraid/api#0
File: :0-0
Timestamp: 2025-03-27T23:52:57.888Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files ensures that all web components share a single Pinia store instance, which is the desired behavior. Without this initialization, each web component would have its own isolated store, breaking the intended architecture.

Applied to files:

  • web/pages/login.vue
📚 Learning: 2025-03-27T23:52:57.888Z
Learnt from: zackspear
PR: unraid/api#0
File: :0-0
Timestamp: 2025-03-27T23:52:57.888Z
Learning: In the unraid/api project, Vue components are compiled into web components. The `setActivePinia(createPinia())` call at the module level in store files is intentional and ensures all web components share a single Pinia store instance, which is the desired behavior. This shared state approach is critical for the application's architecture to function correctly.

Applied to files:

  • web/pages/login.vue
📚 Learning: 2024-12-06T17:38:40.999Z
Learnt from: elibosley
PR: unraid/api#974
File: web/components/Loading/Error.vue:1-3
Timestamp: 2024-12-06T17:38:40.999Z
Learning: In Nuxt.js projects, components are automatically imported, so explicit import statements for components like `LoadingSpinner` in `web/components/Loading/Error.vue` are not necessary.

Applied to files:

  • web/pages/login.vue
📚 Learning: 2024-11-01T17:52:55.311Z
Learnt from: pujitm
PR: unraid/api#941
File: web/components/Notifications/Item.vue:0-0
Timestamp: 2024-11-01T17:52:55.311Z
Learning: In the `web/components/Notifications/Item.vue` component, ensure that when delete or archive mutations fail, the notification remains displayed, and an error message is shown below the action buttons to inform the user.

Applied to files:

  • web/pages/login.vue
  • web/composables/gql/graphql.ts
📚 Learning: 2024-12-17T14:59:32.458Z
Learnt from: elibosley
PR: unraid/api#972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.

Applied to files:

  • web/pages/login.vue
📚 Learning: 2025-03-07T17:35:50.406Z
Learnt from: mdatelle
PR: unraid/api#1219
File: api/src/unraid-api/main.ts:32-55
Timestamp: 2025-03-07T17:35:50.406Z
Learning: Helmet security configuration in the Unraid API is intentionally relaxed (with disabled CSP, CORS policies, and HSTS) to maintain compatibility with existing Unraid plugins. Stricter security settings might break current plugin functionality.

Applied to files:

  • api/docs/public/oidc-provider-examples.md
📚 Learning: 2025-04-28T20:35:32.980Z
Learnt from: elibosley
PR: unraid/api#1368
File: api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch:0-0
Timestamp: 2025-04-28T20:35:32.980Z
Learning: For Unraid GUI mode, automatic session creation with root privileges is an acceptable pattern since these pages are already protected and only visible to authenticated users.

Applied to files:

  • api/docs/public/oidc-provider-examples.md
📚 Learning: 2024-11-06T20:59:06.096Z
Learnt from: mdatelle
PR: unraid/api#942
File: api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts:87-89
Timestamp: 2024-11-06T20:59:06.096Z
Learning: Error handling in mutations like `addRoleForUser` in `auth.resolver.ts` is performed at the service level, so additional error handling in the resolver is unnecessary.

Applied to files:

  • api/src/unraid-api/rest/rest.controller.ts
📚 Learning: 2025-01-23T21:26:17.766Z
Learnt from: elibosley
PR: unraid/api#1063
File: api/src/unraid-api/cli/sso/validate-token.command.ts:76-81
Timestamp: 2025-01-23T21:26:17.766Z
Learning: In the unraid/api codebase, store dispatches using Redux Toolkit's `unwrap()` method already handle errors appropriately and don't require additional try-catch blocks.

Applied to files:

  • api/src/unraid-api/rest/rest.controller.ts
📚 Learning: 2025-03-13T16:17:55.820Z
Learnt from: elibosley
PR: unraid/api#1211
File: api/src/unraid-api/graph/connect/connect-settings.service.ts:48-53
Timestamp: 2025-03-13T16:17:55.820Z
Learning: In NestJS services, dynamic imports should be wrapped in try/catch blocks with proper error logging and fallback handling to prevent unhandled rejections from crashing the application.

Applied to files:

  • api/src/unraid-api/rest/rest.controller.ts
📚 Learning: 2024-12-20T14:56:29.464Z
Learnt from: elibosley
PR: unraid/api#992
File: api/src/mothership/utils/create-local-connect-api-key.ts:23-29
Timestamp: 2024-12-20T14:56:29.464Z
Learning: The user prefers not to add error handling for the Redux dispatch in create-local-connect-api-key.ts.

Applied to files:

  • api/src/unraid-api/rest/rest.controller.ts
📚 Learning: 2025-01-31T14:42:06.752Z
Learnt from: pujitm
PR: unraid/api#1075
File: web/components/Notifications/Sidebar.vue:41-58
Timestamp: 2025-01-31T14:42:06.752Z
Learning: When handling errors in code that uses toast notifications, avoid using toast in the error handler itself since toast functionality might be the source of the error. Use console.error or other reliable logging mechanisms instead.

Applied to files:

  • api/src/unraid-api/rest/rest.controller.ts
📚 Learning: 2025-07-21T14:00:06.258Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/api-rules.mdc:0-0
Timestamp: 2025-07-21T14:00:06.258Z
Learning: Applies to api/src/unraid-api/**/* : Prefer adding new files to the NestJS repository located at api/src/unraid-api/ instead of the legacy code

Applied to files:

  • api/src/unraid-api/rest/rest.controller.ts
📚 Learning: 2025-07-28T15:34:24.715Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-07-28T15:34:24.715Z
Learning: Applies to api/src/unraid-api/**/* : Prefer adding new files to the NestJS repo located at api/src/unraid-api/ instead of the legacy code

Applied to files:

  • api/src/unraid-api/rest/rest.controller.ts
📚 Learning: 2025-04-23T20:19:42.542Z
Learnt from: pujitm
PR: unraid/api#1367
File: packages/unraid-api-plugin-connect/src/pubsub/user.service.ts:44-52
Timestamp: 2025-04-23T20:19:42.542Z
Learning: The project uses a custom ConfigService implementation that includes a `set()` method for runtime configuration mutation, unlike the standard nestjs/config package which only provides getter methods.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
📚 Learning: 2025-03-12T13:35:43.900Z
Learnt from: pujitm
PR: unraid/api#1211
File: web/codegen.ts:14-14
Timestamp: 2025-03-12T13:35:43.900Z
Learning: The JSON scalar type in web/codegen.ts was temporarily changed from 'string' to 'any' for compatibility with JsonForms integration. This change facilitates the implementation of the Connect settings web component.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
  • web/composables/gql/graphql.ts
📚 Learning: 2024-11-05T14:48:56.949Z
Learnt from: elibosley
PR: unraid/api#942
File: api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts:41-47
Timestamp: 2024-11-05T14:48:56.949Z
Learning: Input types for GraphQL mutations can be pulled from the GraphQL codegen types, as they are generated into the API files. Therefore, explicit typing in the resolver files may be unnecessary.

Applied to files:

  • web/composables/gql/graphql.ts
  • api/generated-schema.graphql
📚 Learning: 2025-03-12T13:48:14.850Z
Learnt from: pujitm
PR: unraid/api#1211
File: web/composables/gql/gql.ts:17-18
Timestamp: 2025-03-12T13:48:14.850Z
Learning: In the Unraid API project, the duplicate GraphQL query and mutation strings in gql.ts files are intentionally generated by GraphQL CodeGen tool and are necessary for the type system to function properly.

Applied to files:

  • web/composables/gql/graphql.ts
  • api/generated-schema.graphql
📚 Learning: 2025-03-14T16:10:38.562Z
Learnt from: pujitm
PR: unraid/api#1211
File: api/src/graphql/schema/types/connect/connect.graphql:142-146
Timestamp: 2025-03-14T16:10:38.562Z
Learning: The updateApiSettings mutation in api/src/unraid-api/graph/connect/connect.resolver.ts is protected with the UsePermissions decorator that requires UPDATE permission on the CONFIG resource.

Applied to files:

  • web/composables/gql/graphql.ts
  • api/generated-schema.graphql
📚 Learning: 2025-07-21T14:00:25.140Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-07-21T14:00:25.140Z
Learning: Applies to web/**/*.{query,mutation}.ts : All GraphQL under `web/` must follow this naming convention

Applied to files:

  • web/composables/gql/graphql.ts
📚 Learning: 2025-07-28T15:34:24.715Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-07-28T15:34:24.715Z
Learning: Applies to web/**/*.{query,mutation}.ts : All GraphQL under web/ must follow this naming convention (.query.ts for queries, .mutation.ts for mutations)

Applied to files:

  • web/composables/gql/graphql.ts
📚 Learning: 2025-07-21T14:00:25.140Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-07-21T14:00:25.140Z
Learning: Applies to web/**/*.mutation.ts : GraphQL mutations must be placed in `.mutation.ts` files

Applied to files:

  • web/composables/gql/graphql.ts
📚 Learning: 2025-01-30T19:56:42.055Z
Learnt from: pujitm
PR: unraid/api#1075
File: web/components/Notifications/Sidebar.vue:52-57
Timestamp: 2025-01-30T19:56:42.055Z
Learning: Notification links in the Unraid API are validated at creation time on the server side, making additional client-side validation unnecessary when receiving notifications through GraphQL subscriptions.

Applied to files:

  • web/composables/gql/graphql.ts
📚 Learning: 2025-02-24T14:51:21.328Z
Learnt from: elibosley
PR: unraid/api#1181
File: web/store/theme.ts:0-0
Timestamp: 2025-02-24T14:51:21.328Z
Learning: In the Unraid API project's theme system, exact TypeScript type definitions are preferred over index signatures for theme variables to ensure better type safety.

Applied to files:

  • web/composables/gql/graphql.ts
📚 Learning: 2025-07-28T15:34:24.715Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-07-28T15:34:24.715Z
Learning: Always run pnpm codegen for GraphQL code generation in the web directory

Applied to files:

  • api/.env.development
📚 Learning: 2025-07-21T14:00:25.140Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-07-21T14:00:25.140Z
Learning: Always run `pnpm codegen` for GraphQL code generation in the web directory

Applied to files:

  • api/.env.development
📚 Learning: 2025-06-20T20:50:14.050Z
Learnt from: elibosley
PR: unraid/api#1425
File: api/dev/configs/connect.json:5-13
Timestamp: 2025-06-20T20:50:14.050Z
Learning: Connect configuration files like api/dev/configs/connect.json use their own naming conventions (e.g., apikey, accesstoken, idtoken, refreshtoken) as implementation details, and these should not be changed to follow general camelCase patterns.

Applied to files:

  • api/.env.development
📚 Learning: 2025-07-28T15:34:24.715Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-07-28T15:34:24.715Z
Learning: Always run scripts from api/package.json unless requested

Applied to files:

  • api/.env.development
📚 Learning: 2025-07-21T14:01:04.139Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-07-21T14:01:04.139Z
Learning: Applies to web/__test__/store/**/*.ts : Test getter dependencies are properly mocked in store tests

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts
📚 Learning: 2025-07-21T14:01:04.139Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-07-21T14:01:04.139Z
Learning: Applies to web/__test__/store/**/*.ts : Verify state changes after actions in store tests

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts
📚 Learning: 2025-07-21T14:01:04.139Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-07-21T14:01:04.139Z
Learning: Applies to web/__test__/store/**/*.ts : Verify state changes by updating the store in store tests

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts
📚 Learning: 2025-07-21T14:01:04.139Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-07-21T14:01:04.139Z
Learning: Applies to web/__test__/store/**/*.ts : Mock external dependencies appropriately in store tests

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts
📚 Learning: 2025-07-28T15:34:24.715Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-07-28T15:34:24.715Z
Learning: Applies to api/**/*.{test,spec}.{ts,js} : Test suite is VITEST, do not use jest in the API package

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts
📚 Learning: 2025-07-21T14:01:04.139Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-07-21T14:01:04.139Z
Learning: Applies to web/__test__/store/**/*.ts : Test action side effects and state changes in store tests

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts
📚 Learning: 2025-07-21T14:01:04.139Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-07-21T14:01:04.139Z
Learning: Applies to web/__test__/{components,store,mocks}/**/*.ts : Clear mocks between tests to ensure isolation

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts
📚 Learning: 2025-07-21T14:01:04.139Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-07-21T14:01:04.139Z
Learning: Applies to web/__test__/store/**/*.ts : Test async operations completely in store tests

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts
📚 Learning: 2025-07-21T14:01:04.139Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-07-21T14:01:04.139Z
Learning: Applies to web/__test__/{components,store,mocks}/**/*.ts : Mock external dependencies and services in tests

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts
📚 Learning: 2025-07-28T15:34:24.715Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-07-28T15:34:24.715Z
Learning: Applies to web/__test__/**/*store*.{test,spec}.{ts,js} : Clear mocks between tests to ensure isolation in store tests

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts
📚 Learning: 2025-08-09T01:03:29.676Z
Learnt from: elibosley
PR: unraid/api#1575
File: packages/unraid-shared/src/services/socket-config.service.spec.ts:10-13
Timestamp: 2025-08-09T01:03:29.676Z
Learning: Vitest is used for all testing across all repositories in the unraid organization, not Jest. Always use `vi` for mocking utilities, not `jest`.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts
📚 Learning: 2025-07-28T15:34:24.715Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-07-28T15:34:24.715Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,js} : Mock external dependencies and services in web/__test__

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts
📚 Learning: 2025-07-21T14:01:04.139Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-07-21T14:01:04.139Z
Learning: Applies to web/__test__/{components,store,mocks}/**/*.ts : Remember that `vi.mock()` calls are hoisted

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts
📚 Learning: 2025-07-21T14:01:04.139Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-07-21T14:01:04.139Z
Learning: Applies to web/__test__/{components,store,mocks}/**/*.ts : Reset mocks between tests with `vi.clearAllMocks()`

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts
🧬 Code Graph Analysis (1)
web/composables/gql/graphql.ts (2)
api/src/unraid-api/cli/generated/graphql.ts (17)
  • Scalars (11-75)
  • OidcProvider (1188-1220)
  • MutationConfigureUpsArgs (1003-1005)
  • UpsConfigInput (1861-1882)
  • MutationDeleteOidcProviderArgs (1031-1033)
  • MutationUpsertOidcProviderArgs (1087-1089)
  • OidcProviderInput (1222-1253)
  • Maybe (3-3)
  • InputMaybe (4-4)
  • PublicOidcProvider (1390-1398)
  • UpsDevice (1916-1930)
  • QueryOidcProviderArgs (1486-1488)
  • QueryUpsDeviceByIdArgs (1491-1493)
  • SsoSettings (1773-1778)
  • Node (1097-1099)
  • UpsBattery (1842-1850)
  • UpsPower (1938-1946)
web/components/sso/useSsoProviders.ts (1)
  • OidcProvider (5-12)
🪛 GitHub Actions: CI - Main (API)
web/pages/login.vue

[error] 64-64: TypeScript error TS2339: Property 'stdout' does not exist on the specified type.

🪛 dotenv-linter (3.3.0)
api/.env.development

[warning] 20-20: [UnorderedKey] The PATHS_OIDC_JSON key should go before the PATHS_PARITY_CHECKS key

🪛 Gitleaks (8.27.2)
api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts

91-91: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)

🔇 Additional comments (4)
api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts (1)

70-76: Possible misuse of ConfigService default value

this.configService.get<string[]>('api.ssoSubIds', []) relies on a non-standard second-parameter default. Ensure the custom ConfigService in this repo supports it, otherwise handle undefined explicitly:

const ssoSubIds = this.configService.get<string[]>('api.ssoSubIds') ?? [];
api/src/unraid-api/rest/rest.controller.ts (1)

121-128: 302 redirect implementation looks good.

Manual Location header with 302 is appropriate for proxy compatibility here.

web/composables/gql/graphql.ts (2)

2441-2450: Public OIDC providers query looks correct.

Only public-safe fields are requested; no secrets. Good.


2524-2525: Admin OIDC providers query selection is appropriate.

No secret fields are requested; fields align with admin UI needs.

@github-actions
Copy link
Contributor

github-actions bot commented Aug 9, 2025

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

1 similar comment
@github-actions
Copy link
Contributor

github-actions bot commented Aug 9, 2025

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

Copy link
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: 5

🔭 Outside diff range comments (1)
unraid-ui/src/forms/SteppedLayout.vue (1)

84-92: Gracefully handle elements that omit options.step

Currently, any element without a step value is silently dropped. If the intention is “step 0 by default”, treat the absence explicitly:

-  const filtered = elements.filter((element) => {
-    return (
-      typeof element.options === 'object' &&
-      element.options !== null &&
-      element.options.step === currentStep.value
-    );
-  });
+  const filtered = elements.filter((el) => {
+    const step = typeof el.options === 'object' && el.options
+      ? el.options.step ?? 0
+      : 0;
+    return step === currentStep.value;
+  });

This avoids accidental loss of untargeted elements and removes the cast when you switch to the helper type suggested above.

♻️ Duplicate comments (10)
api/src/unraid-api/graph/resolvers/settings/settings.model.ts (1)

51-53: Missing @Type decorator breaks nested validation
@ValidateNested() alone won’t instantiate SsoSettings; add @Type(() => SsoSettings) (from class-transformer) to ensure proper validation & transformation.

api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts (2)

127-129: Avoid unsafe cast from input to entity

Casting provider as OidcProvider bypasses type safety and may hide missing required fields – map explicitly or change the service signature.


151-156: validateOidcSession lacks access control

The endpoint is marked “internal use” but is publicly accessible. Add appropriate guards or an explicit @Public() with justification.

api/src/unraid-api/rest/rest.controller.ts (1)

64-76: Validate providerId input to mitigate injection / path-traversal risks

Reject IDs not matching [a-zA-Z0-9._-]+ before using them.

api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts (2)

433-503: Authorization rule evaluation rejects arrays and treats them as objects

Array-valued claims (e.g., groups) should be supported. Also, the object-check returns early before the array-check. Handle arrays explicitly and evaluate against each element.

-        // Log detailed claim analysis
-        if (typeof claimValue === 'object' && claimValue !== null) {
-            this.logger.warn(
-                `unexpected JWT claim value encountered - claim ${rule.claim} is object type: ${JSON.stringify(claimValue)}`
-            );
-            return false;
-        }
-
-        if (Array.isArray(claimValue)) {
-            this.logger.warn(
-                `unexpected JWT claim value encountered - claim ${rule.claim} is array type: ${JSON.stringify(claimValue)}`
-            );
-            return false;
-        }
-
-        const value = String(claimValue);
-        this.logger.debug(`Processing claim ${rule.claim} with string value: "${value}"`);
+        // Normalize to list of string values; support array claims
+        let values: string[];
+        if (Array.isArray(claimValue)) {
+            values = (claimValue as unknown[]).map((v) => String(v));
+        } else if (typeof claimValue === 'object' && claimValue !== null) {
+            this.logger.warn(
+                `unexpected JWT claim value encountered - claim ${rule.claim} is complex object: ${JSON.stringify(claimValue)}`
+            );
+            return false;
+        } else {
+            values = [String(claimValue)];
+        }
+        this.logger.debug(`Processing claim ${rule.claim} with values: ${JSON.stringify(values)}`);
@@
-            case AuthorizationOperator.EQUALS:
-                result = rule.value.some((v) => value === v);
+            case AuthorizationOperator.EQUALS:
+                result = rule.value.some((v) => values.includes(v));
@@
-            case AuthorizationOperator.CONTAINS:
-                result = rule.value.some((v) => value.includes(v));
+            case AuthorizationOperator.CONTAINS:
+                result = rule.value.some((v) => values.some((val) => val.includes(v)));
@@
-            case AuthorizationOperator.STARTS_WITH:
-                result = rule.value.some((v) => value.startsWith(v));
+            case AuthorizationOperator.STARTS_WITH:
+                result = rule.value.some((v) => values.some((val) => val.startsWith(v)));
@@
-            case AuthorizationOperator.ENDS_WITH:
-                result = rule.value.some((v) => value.endsWith(v));
+            case AuthorizationOperator.ENDS_WITH:
+                result = rule.value.some((v) => values.some((val) => val.endsWith(v)));

518-528: Refactor redirect URI configuration (avoid hardcoded URLs)

Make redirect URIs fully configurable and environment-aware to avoid brittle localhost checks.

-    private getRedirectUri(requestHost?: string): string {
-        // Always use the proxied path through /graphql to match production
-        if (requestHost && requestHost.includes('localhost')) {
-            // In development, use the Nuxt proxy at port 3000
-            return `http://localhost:3000/graphql/api/auth/oidc/callback`;
-        }
-
-        // In production, use the configured base URL with /graphql prefix
-        const baseUrl = this.configService.get('BASE_URL', 'http://tower.local');
-        return `${baseUrl}/graphql/api/auth/oidc/callback`;
-    }
+    private getRedirectUri(requestHost?: string): string {
+        const isDevelopment = this.configService.get('NODE_ENV') === 'development';
+        if (isDevelopment && requestHost) {
+            const protocol = requestHost.includes('localhost') ? 'http' : 'https';
+            return `${protocol}://${requestHost}/graphql/api/auth/oidc/callback`;
+        }
+        const callbackUrl = this.configService.get('OIDC_CALLBACK_URL');
+        if (callbackUrl) return callbackUrl;
+        const baseUrl = this.configService.get('BASE_URL', 'http://tower.local');
+        return `${baseUrl}/graphql/api/auth/oidc/callback`;
+    }
api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts (1)

110-120: cleanProvider misses optional string fields (jwksUri, buttonText, buttonVariant)

Extend cleaning to all optional string fields for consistency.

         return {
             ...provider,
             clientSecret: provider.clientSecret?.trim() || undefined,
             authorizationEndpoint: provider.authorizationEndpoint?.trim() || undefined,
             tokenEndpoint: provider.tokenEndpoint?.trim() || undefined,
+            jwksUri: provider.jwksUri?.trim() || undefined,
+            buttonText: provider.buttonText?.trim() || undefined,
             buttonIcon: provider.buttonIcon?.trim() || undefined,
+            buttonVariant: provider.buttonVariant?.trim() || undefined,
             buttonStyle: provider.buttonStyle?.trim() || undefined,
         };
api/generated-schema.graphql (3)

1886-1945: Make id optional in OidcProviderInput for create vs update

For upsert ergonomics, let the server generate ids on create; require on update.

Apply in source schema:

-  id: String!
+  id: String

Document behavior: absent → create, present → update.


1466-1525: Do not expose clientSecret in public GraphQL type; implement Node

Leaking clientSecret is a high-severity risk. Remove it from OidcProvider output and align with the API by implementing Node for consistency.

Note: apply these changes to the source schema/resolvers so this file regenerates accordingly.

-type OidcProvider {
+type OidcProvider implements Node {
   """The unique identifier for the OIDC provider"""
   id: PrefixedID!
@@
-  """OAuth2 client secret (if required by provider)"""
-  clientSecret: String

1791-1801: Add authorization directives to admin/internal queries and fix id type

Protect sensitive queries with @usePermissions and use PrefixedID for id consistency if OidcProvider implements Node.

Apply in source schema:

-  publicOidcProviders: [PublicOidcProvider!]!
-  oidcProviders: [OidcProvider!]!
-  oidcProvider(id: String!): OidcProvider
-  validateOidcSession(token: String!): OidcSessionValidation!
+  publicOidcProviders: [PublicOidcProvider!]!
+  oidcProviders: [OidcProvider!]! @usePermissions(action: READ, resource: "SSO", possession: ANY)
+  oidcProvider(id: PrefixedID!): OidcProvider @usePermissions(action: READ, resource: "SSO", possession: ANY)
+  validateOidcSession(token: String!): OidcSessionValidation! @usePermissions(action: READ, resource: "SSO", possession: OWN)
🧹 Nitpick comments (7)
unraid-ui/src/forms/SteppedLayout.vue (1)

21-27: Avoid narrowing options on a subtype – keep the base typing and add a helper type instead

UISchemaElement already exposes an options?: any field. By redefining it here with a narrower shape you:
• create an unnecessary new interface,
• risk incompatibilities when options is not an object.

A lighter-weight, type-safe pattern:

type UISchemaWithStep = UISchemaElement & {
  options?: { step?: number } & Record<string, unknown>;
};

…and use UISchemaWithStep locally without extending the base.
This keeps the original contract intact while giving you the extra IntelliSense you want.

unraid-ui/src/forms/ObjectArrayField.vue (3)

126-149: Shadowed variable schema inside forEach hampers readability

Using the same identifier for the outer schema and the inner loop variable is confusing:

- Object.entries(properties).forEach(([key, propSchema]) => {
-   const schema = propSchema as JsonSchema;
-   if (schema.default !== undefined) {
+ Object.entries(properties).forEach(([key, propSchema]) => {
+   const prop = propSchema as JsonSchema;
+   if (prop.default !== undefined) {

Renaming prevents accidental misuse and improves clarity.


170-180: Index-based keys on TabsTrigger may cause state leaks on reorder

index works until items are removed from the middle, after which Vue reuses DOM nodes and bound state. Prefer a stable identifier:

- <TabsTrigger
-   v-for="(item, index) in items"
-   :key="index"
+ <TabsTrigger
+   v-for="(item, index) in items"
+   :key="item.id ?? index"   <!-- requires each item to expose an `id` -->

If your schema doesn’t define a natural key, generate one (crypto.randomUUID()) when adding items.


241-245: Minor interpolation issue – call .toLowerCase() on the unwrapped value

itemTypeName is a computed ref. In templates it auto-unwraps, but chaining a method is brittle in strict templates. Use a helper:

<p class="text-muted-foreground mb-4">
  No {{ itemTypeNameLower }}s configured
</p>

with

const itemTypeNameLower = computed(() => itemTypeName.value.toLowerCase());

Safer and avoids surprises when switching to script-only render functions.

api/src/unraid-api/graph/resolvers/sso/oidc-validation.service.ts (1)

12-12: Unused injected ConfigService

configService is injected but never referenced. Drop the dependency or use it (e.g., to read whitelist/timeout configs) to avoid dead code.

api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts (2)

77-87: Unused allowInsecureRequests handling when building authorization URL

You compute clientOptions for HTTP issuers but don’t use it (and it isn’t needed for building an auth URL). Either remove this block or add a clarifying comment.

-        // For HTTP endpoints, we need to pass the allowInsecureRequests option
-        const serverUrl = new URL(provider.issuer || '');
-        let clientOptions: any = undefined;
-        if (serverUrl.protocol === 'http:') {
-            this.logger.debug(
-                `Building authorization URL with allowInsecureRequests for ${provider.id}`
-            );
-            clientOptions = {
-                execute: [client.allowInsecureRequests],
-            };
-        }
-
-        const authUrl = client.buildAuthorizationUrl(config, parameters);
+        // Build the authorization URL; allowInsecureRequests is only needed for network requests,
+        // not for URL construction.
+        const authUrl = client.buildAuthorizationUrl(config, parameters);

Also applies to: 68-76


78-81: Avoid new URL(provider.issuer || '') — rely on discovered issuer

Guard against empty issuer and keep consistency by using the discovered issuer.

-        const serverUrl = new URL(provider.issuer || '');
+        const serverUrl = new URL(config.serverMetadata().issuer);

Also applies to: 186-189

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 67b4a0c and 5f99431.

📒 Files selected for processing (20)
  • api/generated-schema.graphql (6 hunks)
  • api/package.json (2 hunks)
  • api/src/unraid-api/graph/resolvers/settings/settings.model.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/settings/settings.resolver.ts (6 hunks)
  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.test.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/oidc-validation.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/sso.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/utils/form-utils.ts (1 hunks)
  • api/src/unraid-api/rest/rest.controller.ts (2 hunks)
  • packages/unraid-shared/src/services/user-settings.ts (3 hunks)
  • unraid-ui/src/components/common/index.ts (1 hunks)
  • unraid-ui/src/components/form/index.ts (1 hunks)
  • unraid-ui/src/forms/HorizontalLayout.vue (2 hunks)
  • unraid-ui/src/forms/ObjectArrayField.vue (1 hunks)
  • unraid-ui/src/forms/SteppedLayout.vue (2 hunks)
  • unraid-ui/src/forms/UnraidSettingsLayout.vue (2 hunks)
  • unraid-ui/src/forms/VerticalLayout.vue (2 hunks)
✅ Files skipped from review due to trivial changes (5)
  • unraid-ui/src/forms/UnraidSettingsLayout.vue
  • unraid-ui/src/forms/VerticalLayout.vue
  • unraid-ui/src/components/form/index.ts
  • unraid-ui/src/forms/HorizontalLayout.vue
  • unraid-ui/src/components/common/index.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • api/package.json
  • api/src/unraid-api/graph/resolvers/sso/sso.module.ts
  • api/src/unraid-api/graph/utils/form-utils.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.test.ts
  • api/src/unraid-api/graph/resolvers/settings/settings.resolver.ts
🧰 Additional context used
📓 Path-based instructions (2)
api/**/*

📄 CodeRabbit Inference Engine (.cursor/rules/api-rules.mdc)

api/**/*: Use pnpm as the only package manager
Always run scripts from api/package.json unless specifically requested otherwise

Files:

  • api/src/unraid-api/graph/resolvers/settings/settings.model.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
  • api/src/unraid-api/rest/rest.controller.ts
  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-validation.service.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
  • api/generated-schema.graphql
api/src/unraid-api/**/*

📄 CodeRabbit Inference Engine (.cursor/rules/api-rules.mdc)

Prefer adding new files to the NestJS repository located at api/src/unraid-api/ instead of the legacy code

Prefer adding new files to the NestJS repo located at api/src/unraid-api/ instead of the legacy code

Files:

  • api/src/unraid-api/graph/resolvers/settings/settings.model.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
  • api/src/unraid-api/rest/rest.controller.ts
  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-validation.service.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
🧠 Learnings (49)
📓 Common learnings
Learnt from: mdatelle
PR: unraid/api#942
File: api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts:111-113
Timestamp: 2024-11-06T20:59:25.809Z
Learning: In the Unraid API project, error handling for mutations is handled at the service level rather than in the GraphQL resolvers. Specifically, in `api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts`, methods like `removeRoleFromApiKey` rely on service-level error handling.
📚 Learning: 2025-03-14T16:10:38.562Z
Learnt from: pujitm
PR: unraid/api#1211
File: api/src/graphql/schema/types/connect/connect.graphql:142-146
Timestamp: 2025-03-14T16:10:38.562Z
Learning: The updateApiSettings mutation in api/src/unraid-api/graph/connect/connect.resolver.ts is protected with the UsePermissions decorator that requires UPDATE permission on the CONFIG resource.

Applied to files:

  • api/src/unraid-api/graph/resolvers/settings/settings.model.ts
  • packages/unraid-shared/src/services/user-settings.ts
  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts
  • api/generated-schema.graphql
📚 Learning: 2025-01-27T14:31:42.305Z
Learnt from: elibosley
PR: unraid/api#1063
File: web/components/SsoButton.ce.vue:5-8
Timestamp: 2025-01-27T14:31:42.305Z
Learning: In the Unraid API web components, SSO-related props are intentionally provided in both camelCase (`ssoEnabled`) and lowercase (`ssoenabled`) variants to support interchangeable usage across different contexts (e.g., HTML attributes vs Vue props).

Applied to files:

  • api/src/unraid-api/graph/resolvers/settings/settings.model.ts
  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
📚 Learning: 2025-03-12T13:35:43.900Z
Learnt from: pujitm
PR: unraid/api#1211
File: web/codegen.ts:14-14
Timestamp: 2025-03-12T13:35:43.900Z
Learning: The JSON scalar type in web/codegen.ts was temporarily changed from 'string' to 'any' for compatibility with JsonForms integration. This change facilitates the implementation of the Connect settings web component.

Applied to files:

  • api/src/unraid-api/graph/resolvers/settings/settings.model.ts
  • unraid-ui/src/forms/SteppedLayout.vue
  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
📚 Learning: 2024-11-06T20:59:25.809Z
Learnt from: mdatelle
PR: unraid/api#942
File: api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts:111-113
Timestamp: 2024-11-06T20:59:25.809Z
Learning: In the Unraid API project, error handling for mutations is handled at the service level rather than in the GraphQL resolvers. Specifically, in `api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts`, methods like `removeRoleFromApiKey` rely on service-level error handling.

Applied to files:

  • api/src/unraid-api/graph/resolvers/settings/settings.model.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
  • api/src/unraid-api/rest/rest.controller.ts
  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-validation.service.ts
  • api/generated-schema.graphql
📚 Learning: 2025-03-12T13:48:14.850Z
Learnt from: pujitm
PR: unraid/api#1211
File: web/composables/gql/gql.ts:17-18
Timestamp: 2025-03-12T13:48:14.850Z
Learning: In the Unraid API project, the duplicate GraphQL query and mutation strings in gql.ts files are intentionally generated by GraphQL CodeGen tool and are necessary for the type system to function properly.

Applied to files:

  • api/src/unraid-api/graph/resolvers/settings/settings.model.ts
  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts
  • api/generated-schema.graphql
📚 Learning: 2025-03-07T19:29:12.540Z
Learnt from: pujitm
PR: unraid/api#1219
File: api/src/unraid-api/main.ts:18-25
Timestamp: 2025-03-07T19:29:12.540Z
Learning: When handling type mismatches between dependencies in TypeScript (such as between NestJS and Fastify), prefer using `ts-expect-error` with a detailed explanation comment instead of `as any` type assertions. This better documents the reason for the type bypass and makes the issue more visible for future maintenance.

Applied to files:

  • api/src/unraid-api/graph/resolvers/settings/settings.model.ts
  • api/src/unraid-api/rest/rest.controller.ts
  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts
📚 Learning: 2025-04-21T18:44:15.414Z
Learnt from: elibosley
PR: unraid/api#1352
File: packages/unraid-api-plugin-connect/src/config.entity.ts:16-26
Timestamp: 2025-04-21T18:44:15.414Z
Learning: For the Unraid API project, class-validator should be used for validation to avoid mismatches between different validation schemas (like Zod).

Applied to files:

  • api/src/unraid-api/graph/resolvers/settings/settings.model.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-validation.service.ts
📚 Learning: 2025-04-21T18:44:24.383Z
Learnt from: elibosley
PR: unraid/api#1352
File: packages/unraid-api-plugin-connect/src/config.entity.ts:37-39
Timestamp: 2025-04-21T18:44:24.383Z
Learning: The project prefers using class-validator decorators directly on entity classes rather than separate Zod schemas to maintain type consistency between GraphQL entities and validation rules.

Applied to files:

  • api/src/unraid-api/graph/resolvers/settings/settings.model.ts
📚 Learning: 2025-01-28T16:39:50.278Z
Learnt from: elibosley
PR: unraid/api#1078
File: api/src/unraid-api/unraid-file-modifier/modifications/sso.modification.ts:35-36
Timestamp: 2025-01-28T16:39:50.278Z
Learning: When reviewing TypeScript files containing PHP code snippets intended for file modifications (like in `SSOFileModification`), PHP functions that appear undefined should not be flagged as issues since they will be available in the target PHP environment where the code is injected.

Applied to files:

  • api/src/unraid-api/graph/resolvers/settings/settings.model.ts
📚 Learning: 2025-07-21T14:01:04.139Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-07-21T14:01:04.139Z
Learning: Applies to web/__test__/{components,store,mocks}/**/*.ts : Don't mix mock declarations and module mocks incorrectly

Applied to files:

  • api/src/unraid-api/graph/resolvers/settings/settings.model.ts
📚 Learning: 2024-11-05T14:48:56.949Z
Learnt from: elibosley
PR: unraid/api#942
File: api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts:41-47
Timestamp: 2024-11-05T14:48:56.949Z
Learning: Input types for GraphQL mutations can be pulled from the GraphQL codegen types, as they are generated into the API files. Therefore, explicit typing in the resolver files may be unnecessary.

Applied to files:

  • api/src/unraid-api/graph/resolvers/settings/settings.model.ts
  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts
  • api/generated-schema.graphql
📚 Learning: 2025-01-15T21:34:00.006Z
Learnt from: pujitm
PR: unraid/api#1047
File: api/src/unraid-api/graph/sandbox-plugin.ts:57-57
Timestamp: 2025-01-15T21:34:00.006Z
Learning: In the GraphQL sandbox (api/src/unraid-api/graph/sandbox-plugin.ts), CSRF token validation should fail silently with a fallback value to maintain sandbox accessibility, as it's a development tool where strict security measures aren't required.

Applied to files:

  • api/src/unraid-api/graph/resolvers/settings/settings.model.ts
  • api/src/unraid-api/rest/rest.controller.ts
  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
  • api/generated-schema.graphql
📚 Learning: 2025-02-24T14:51:21.328Z
Learnt from: elibosley
PR: unraid/api#1181
File: web/store/theme.ts:0-0
Timestamp: 2025-02-24T14:51:21.328Z
Learning: In the Unraid API project's theme system, exact TypeScript type definitions are preferred over index signatures for theme variables to ensure better type safety.

Applied to files:

  • unraid-ui/src/forms/SteppedLayout.vue
📚 Learning: 2025-02-04T17:21:39.710Z
Learnt from: mdatelle
PR: unraid/api#1106
File: unraid-ui/src/components/index.ts:2-2
Timestamp: 2025-02-04T17:21:39.710Z
Learning: The unraid-ui package is undergoing a major refactoring process, and breaking changes are expected during this transition period.

Applied to files:

  • unraid-ui/src/forms/SteppedLayout.vue
  • unraid-ui/src/forms/ObjectArrayField.vue
  • packages/unraid-shared/src/services/user-settings.ts
📚 Learning: 2025-03-14T16:56:20.520Z
Learnt from: pujitm
PR: unraid/api#1211
File: api/src/unraid-api/graph/connect/connect-settings.service.ts:199-199
Timestamp: 2025-03-14T16:56:20.520Z
Learning: The JSONForms library has narrow type definitions for interfaces like SchemaBasedCondition that make augmentation difficult. Type assertions (using 'as') are sometimes necessary despite not being ideal from a typing perspective.

Applied to files:

  • unraid-ui/src/forms/SteppedLayout.vue
📚 Learning: 2025-04-02T21:21:29.168Z
Learnt from: elibosley
PR: unraid/api#1308
File: unraid-ui/src/components/common/loading/Error.vue:2-2
Timestamp: 2025-04-02T21:21:29.168Z
Learning: Components in the unraid-ui folder require explicit imports and are not autoloaded, unlike other parts of the project that may use Nuxt.js autoloading features.

Applied to files:

  • unraid-ui/src/forms/ObjectArrayField.vue
📚 Learning: 2024-12-09T15:47:29.325Z
Learnt from: pujitm
PR: unraid/api#975
File: web/components/Notifications/TabList.vue:1-24
Timestamp: 2024-12-09T15:47:29.325Z
Learning: In our Nuxt setup using Vue.js, components defined within the codebase are autoloaded and do not require explicit import statements in the script section. For example, components like `TabsList` and `TabsTrigger` used in `web/components/Notifications/TabList.vue` are automatically available without imports.

Applied to files:

  • unraid-ui/src/forms/ObjectArrayField.vue
📚 Learning: 2025-03-14T19:14:10.408Z
Learnt from: pujitm
PR: unraid/api#1211
File: plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/Connect.page:0-0
Timestamp: 2025-03-14T19:14:10.408Z
Learning: *.ce.vue components in `web/components` get built as custom elements and prefixed with `unraid-`, making them available as HTML custom elements at runtime.

Applied to files:

  • unraid-ui/src/forms/ObjectArrayField.vue
📚 Learning: 2025-07-28T15:34:24.715Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-07-28T15:34:24.715Z
Learning: Applies to web/**/store/**/*.{ts,js} : Ensure Vue reactivity imports are added to store files (computed, ref, watchEffect)

Applied to files:

  • unraid-ui/src/forms/ObjectArrayField.vue
📚 Learning: 2025-03-13T16:17:21.897Z
Learnt from: elibosley
PR: unraid/api#1211
File: unraid-ui/src/components/form/number/NumberFieldInput.vue:1-21
Timestamp: 2025-03-13T16:17:21.897Z
Learning: The unraid-ui is a UI library being published externally, where wrapping third-party components (like those from reka-ui) is an intentional design choice to maintain a consistent interface, standardize styling, and control the exposed API.

Applied to files:

  • unraid-ui/src/forms/ObjectArrayField.vue
📚 Learning: 2025-03-27T23:52:57.888Z
Learnt from: zackspear
PR: unraid/api#0
File: :0-0
Timestamp: 2025-03-27T23:52:57.888Z
Learning: In the unraid/api project, Vue components are compiled into web components. The `setActivePinia(createPinia())` call at the module level in store files is intentional and ensures all web components share a single Pinia store instance, which is the desired behavior. This shared state approach is critical for the application's architecture to function correctly.

Applied to files:

  • unraid-ui/src/forms/ObjectArrayField.vue
📚 Learning: 2025-03-27T23:33:13.215Z
Learnt from: zackspear
PR: unraid/api#0
File: :0-0
Timestamp: 2025-03-27T23:33:13.215Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files would break the build by causing all web components to share a singular Pinia store instance. Each web component needs its own Pinia store instance to maintain proper isolation and encapsulation.

Applied to files:

  • unraid-ui/src/forms/ObjectArrayField.vue
📚 Learning: 2025-02-18T18:38:24.981Z
Learnt from: zackspear
PR: unraid/api#1143
File: web/pages/webComponents.vue:28-29
Timestamp: 2025-02-18T18:38:24.981Z
Learning: In Vue components, when there's a repeating pattern of heading + content + divider, prefer using a wrapper component that takes the heading as a prop and content via slots, rather than duplicating the structure. This improves maintainability and reduces code duplication.

Applied to files:

  • unraid-ui/src/forms/ObjectArrayField.vue
📚 Learning: 2025-01-30T19:38:02.478Z
Learnt from: pujitm
PR: unraid/api#1075
File: unraid-ui/src/register.ts:15-34
Timestamp: 2025-01-30T19:38:02.478Z
Learning: In the web components registration process for unraid-ui, use a soft-fail approach (logging warnings/errors) instead of throwing errors, to ensure other components can still register successfully even if one component fails.

Applied to files:

  • packages/unraid-shared/src/services/user-settings.ts
📚 Learning: 2025-07-21T14:01:04.139Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-07-21T14:01:04.139Z
Learning: Applies to web/__test__/components/**/*.ts : Set input values with `await input.setValue('value')`

Applied to files:

  • packages/unraid-shared/src/services/user-settings.ts
📚 Learning: 2024-11-05T14:49:07.308Z
Learnt from: elibosley
PR: unraid/api#942
File: api/src/unraid-api/auth/api-key.service.ts:62-70
Timestamp: 2024-11-05T14:49:07.308Z
Learning: In `api/src/unraid-api/auth/api-key.service.ts`, when handling read errors in the `findById` method, throw a `GraphQLError` instead of an `InternalServerErrorException`.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
  • api/src/unraid-api/rest/rest.controller.ts
  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-validation.service.ts
📚 Learning: 2024-11-04T22:00:41.946Z
Learnt from: mdatelle
PR: unraid/api#942
File: api/src/unraid-api/auth/auth.service.ts:42-105
Timestamp: 2024-11-04T22:00:41.946Z
Learning: In the 'AuthService' within `api/src/unraid-api/auth/auth.service.ts`, rate limiting is not required because the API is not public facing.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
  • api/src/unraid-api/rest/rest.controller.ts
  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts
📚 Learning: 2024-11-15T16:22:12.460Z
Learnt from: elibosley
PR: unraid/api#942
File: api/src/unraid-api/auth/auth.module.ts:94-96
Timestamp: 2024-11-15T16:22:12.460Z
Learning: In `CookieService.defaultOpts()` in `api/src/unraid-api/auth/cookie.service.ts`, use `sameSite: 'lax'` for session cookie configuration.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
  • api/src/unraid-api/rest/rest.controller.ts
  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts
📚 Learning: 2025-03-04T14:55:00.903Z
Learnt from: elibosley
PR: unraid/api#1151
File: plugin/builder/utils/consts.ts:6-6
Timestamp: 2025-03-04T14:55:00.903Z
Learning: The startingDir constant in plugin/builder/utils/consts.ts is defined using process.cwd(), which can cause issues if directory changes occur after importing this constant. Using __dirname.split('/builder')[0] would make it more reliable by making it relative to the file location rather than the current working directory.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
📚 Learning: 2025-07-24T18:48:44.035Z
Learnt from: mdatelle
PR: unraid/api#1533
File: web/components/Docker/Edit.vue:16-32
Timestamp: 2025-07-24T18:48:44.035Z
Learning: In web/components/Docker/Edit.vue, the hardcoded configuration values in the config ref are intentional temporary/dummy data used during initial UI development phase while building out the real Docker components, as clarified by mdatelle in PR #1533.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
📚 Learning: 2025-05-08T19:28:37.034Z
Learnt from: elibosley
PR: unraid/api#1381
File: plugin/builder/cli/common-environment.ts:18-22
Timestamp: 2025-05-08T19:28:37.034Z
Learning: In plugin/builder/cli/common-environment.ts, the getDefaultBaseUrl() function intentionally returns an invalid URL when running in CI environments to force explicit specification of the base URL parameter rather than relying on defaults.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
📚 Learning: 2025-01-27T14:57:46.617Z
Learnt from: elibosley
PR: unraid/api#1063
File: web/_data/serverState.ts:137-147
Timestamp: 2025-01-27T14:57:46.617Z
Learning: The values in `web/_data/serverState.ts` are used for testing purposes and should remain as hardcoded mock data to facilitate testing different scenarios.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
📚 Learning: 2025-03-10T18:43:13.016Z
Learnt from: mdatelle
PR: unraid/api#1219
File: api/src/unraid-api/types/fastify.ts:25-30
Timestamp: 2025-03-10T18:43:13.016Z
Learning: In the Unraid API, cookies should be marked as optional (with `?`) in the FastifyRequest type definition since the cookies may not always be present, which is consistent with the authentication flow throughout the codebase.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
  • api/src/unraid-api/rest/rest.controller.ts
  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
  • api/generated-schema.graphql
📚 Learning: 2024-11-18T20:52:47.800Z
Learnt from: elibosley
PR: unraid/api#963
File: web/helpers/markdown.ts:1-13
Timestamp: 2024-11-18T20:52:47.800Z
Learning: All configurations and direct usage of `marked` should be centralized in the `safeParseMarkdown` function in `web/helpers/markdown.ts` to maintain security and consistency.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
📚 Learning: 2024-11-06T20:59:06.096Z
Learnt from: mdatelle
PR: unraid/api#942
File: api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts:87-89
Timestamp: 2024-11-06T20:59:06.096Z
Learning: Error handling in mutations like `addRoleForUser` in `auth.resolver.ts` is performed at the service level, so additional error handling in the resolver is unnecessary.

Applied to files:

  • api/src/unraid-api/rest/rest.controller.ts
  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts
📚 Learning: 2025-01-23T21:26:17.766Z
Learnt from: elibosley
PR: unraid/api#1063
File: api/src/unraid-api/cli/sso/validate-token.command.ts:76-81
Timestamp: 2025-01-23T21:26:17.766Z
Learning: In the unraid/api codebase, store dispatches using Redux Toolkit's `unwrap()` method already handle errors appropriately and don't require additional try-catch blocks.

Applied to files:

  • api/src/unraid-api/rest/rest.controller.ts
📚 Learning: 2025-05-23T20:11:08.857Z
Learnt from: mdatelle
PR: unraid/api#1362
File: api/src/unraid-api/graph/resolvers/rclone/rclone-api.service.ts:421-424
Timestamp: 2025-05-23T20:11:08.857Z
Learning: When logging parameters or request data in error messages, always sanitize sensitive information by checking for common sensitive parameter names (pass, secret, token, key) and replacing their values with '***REDACTED***' or similar placeholders to prevent credential exposure in logs.

Applied to files:

  • api/src/unraid-api/rest/rest.controller.ts
📚 Learning: 2025-07-21T14:00:06.258Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/api-rules.mdc:0-0
Timestamp: 2025-07-21T14:00:06.258Z
Learning: Applies to api/src/unraid-api/**/* : Prefer adding new files to the NestJS repository located at api/src/unraid-api/ instead of the legacy code

Applied to files:

  • api/src/unraid-api/rest/rest.controller.ts
📚 Learning: 2025-07-28T15:34:24.715Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-07-28T15:34:24.715Z
Learning: Applies to api/src/unraid-api/**/* : Prefer adding new files to the NestJS repo located at api/src/unraid-api/ instead of the legacy code

Applied to files:

  • api/src/unraid-api/rest/rest.controller.ts
📚 Learning: 2024-12-11T17:20:09.604Z
Learnt from: pujitm
PR: unraid/api#978
File: web/helpers/apollo-cache/index.ts:133-157
Timestamp: 2024-12-11T17:20:09.604Z
Learning: In `web/helpers/apollo-cache/index.ts`, when updating the cache in the `cache.updateQuery` function, it's sufficient to check if `data` is present, as type checking inside `data` is handled at the GraphQL layer.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts
📚 Learning: 2024-11-15T16:22:03.485Z
Learnt from: elibosley
PR: unraid/api#942
File: api/src/unraid-api/auth/api-key.service.ts:176-188
Timestamp: 2024-11-15T16:22:03.485Z
Learning: Atomic writes are not required for the `saveApiKey` method in `ApiKeyService` (`api/src/unraid-api/auth/api-key.service.ts`) unless specifically needed.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts
📚 Learning: 2024-11-04T20:44:46.432Z
Learnt from: mdatelle
PR: unraid/api#942
File: api/src/unraid-api/auth/auth.service.ts:0-0
Timestamp: 2024-11-04T20:44:46.432Z
Learning: When modifying `apiKey.roles` in `removeRoleFromApiKey` and `addRoleToApiKey` within `api/src/unraid-api/auth/auth.service.ts`, concurrency issues are not a concern because the keys are stored in the file system.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts
📚 Learning: 2025-03-10T17:24:06.914Z
Learnt from: mdatelle
PR: unraid/api#1219
File: api/src/unraid-api/auth/cookie.strategy.ts:19-20
Timestamp: 2025-03-10T17:24:06.914Z
Learning: In the auth system, CSRF token validation and cookie validation have been unified in the `validateCookiesCasbin()` method in the AuthService class, which takes the entire FastifyRequest object and performs both validations sequentially.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts
📚 Learning: 2025-04-23T20:19:42.542Z
Learnt from: pujitm
PR: unraid/api#1367
File: packages/unraid-api-plugin-connect/src/pubsub/user.service.ts:44-52
Timestamp: 2025-04-23T20:19:42.542Z
Learning: The project uses a custom or extended implementation of NestJS ConfigService that includes a `set()` method for runtime configuration mutation, unlike the standard nestjs/config package which only provides getter methods.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
📚 Learning: 2025-04-23T20:19:42.542Z
Learnt from: pujitm
PR: unraid/api#1367
File: packages/unraid-api-plugin-connect/src/pubsub/user.service.ts:44-52
Timestamp: 2025-04-23T20:19:42.542Z
Learning: The project uses a custom ConfigService implementation that includes a `set()` method for runtime configuration mutation, unlike the standard nestjs/config package which only provides getter methods.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
📚 Learning: 2025-03-07T17:35:50.406Z
Learnt from: mdatelle
PR: unraid/api#1219
File: api/src/unraid-api/main.ts:32-55
Timestamp: 2025-03-07T17:35:50.406Z
Learning: Helmet security configuration in the Unraid API is intentionally relaxed (with disabled CSP, CORS policies, and HSTS) to maintain compatibility with existing Unraid plugins. Stricter security settings might break current plugin functionality.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
📚 Learning: 2025-04-23T20:22:36.001Z
Learnt from: pujitm
PR: unraid/api#1367
File: api/src/unraid-api/app/pubsub.module.ts:24-28
Timestamp: 2025-04-23T20:22:36.001Z
Learning: In the Unraid API project, string literals are intentionally used for dependency injection tokens instead of Symbols to enable "collision" behavior across modules that don't directly import from each other. This is a temporary approach during refactoring until a shared library for constants is implemented.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
📚 Learning: 2025-03-07T17:36:52.790Z
Learnt from: mdatelle
PR: unraid/api#1219
File: api/src/unraid-api/main.ts:57-63
Timestamp: 2025-03-07T17:36:52.790Z
Learning: The CORS configuration in the unraid API is intentionally set to allow all origins (`origin: true`). This was a deliberate architectural decision when removing the previous custom CORS implementation and implementing helmet security headers instead.

Applied to files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
🧬 Code Graph Analysis (1)
api/src/unraid-api/graph/resolvers/settings/settings.model.ts (1)
web/composables/gql/graphql.ts (1)
  • SsoSettings (1673-1678)
🪛 GitHub Check: CodeQL
api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts

[failure] 292-292: Incomplete URL substring sanitization
'unraid.net' can be anywhere in the URL, and arbitrary hosts may come before or after it.

🔇 Additional comments (4)
api/src/unraid-api/graph/resolvers/sso/oidc-validation.service.ts (1)

25-30: Deprecation / incompatibility of allowInsecureRequests

allowInsecureRequests was removed in openid-client ≥5. Instead set
custom.setHttpOptionsDefaults({ agent: new http.Agent({ rejectUnauthorized:false }) }) or run discovery over http with Issuer.discover("http://…", { … }). Verify against the library version used.

packages/unraid-shared/src/services/user-settings.ts (1)

151-159: Return shape now always includes restartRequired – verify callers

updateNamespacedValues previously forwarded fragment responses verbatim; it now forces restartRequired to always be present. Ensure all callers expect a non-optional boolean or keep it optional for backward compatibility.

api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts (1)

52-64: Custom auth params path only sets callbackUrl + state

Verify whether the custom endpoint requires client_id/scope/response_type; if so, include them to avoid provider errors.

Would you like me to adjust this branch to optionally include standard params when customAuthParams is true?

api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts (1)

36-45: PATHS_OIDC_JSON handling: ignoring directory segment

fileName() strips directories from PATHS_OIDC_JSON and returns only the filename. Confirm this is intentional; otherwise the override may not point to the requested path.

If the intent is to allow full path override, consider returning envPath as-is and updating ConfigFilePersister to honor absolute paths.

@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

1 similar comment
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

Copy link
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 (4)
api/generated-schema.graphql (4)

1466-1469: Remove clientSecret from API output and implement Node on OidcProvider

Exposing clientSecret risks accidental disclosure. Also, other resources implement Node; keep schema consistent.

-type OidcProvider {
+type OidcProvider implements Node {
   """The unique identifier for the OIDC provider"""
   id: PrefixedID!
@@
-  """OAuth2 client secret (if required by provider)"""
-  clientSecret: String
+  # clientSecret intentionally omitted from API output

Also applies to: 1476-1478


1945-1956: Add authorization directives and fix ID type for provider lookup

Protect admin/owner-scoped data and keep ID type consistent across API.

   """Get public OIDC provider information for login buttons"""
   publicOidcProviders: [PublicOidcProvider!]!
 
   """Get all configured OIDC providers (admin only)"""
-  oidcProviders: [OidcProvider!]!
+  oidcProviders: [OidcProvider!]! @usePermissions(action: READ, resource: "SSO", possession: ANY)
 
   """Get a specific OIDC provider by ID"""
-  oidcProvider(id: String!): OidcProvider
+  oidcProvider(id: PrefixedID!): OidcProvider @usePermissions(action: READ, resource: "SSO", possession: ANY)
 
   """Validate an OIDC session token (internal use for CLI validation)"""
-  validateOidcSession(token: String!): OidcSessionValidation!
+  validateOidcSession(token: String!): OidcSessionValidation! @usePermissions(action: READ, resource: "SSO", possession: OWN)

2002-2006: Protect mutations with permissions and use PrefixedID for delete

Enforce admin-only changes and maintain ID consistency.

-  upsertOidcProvider(provider: OidcProviderInput!): OidcProvider!
-  deleteOidcProvider(id: String!): Boolean!
+  upsertOidcProvider(provider: OidcProviderInput!): OidcProvider! @usePermissions(action: UPDATE, resource: "SSO", possession: ANY)
+  deleteOidcProvider(id: PrefixedID!): Boolean! @usePermissions(action: DELETE, resource: "SSO", possession: ANY)

2049-2052: Make id optional in OidcProviderInput for create vs update

Avoid forcing clients to pre-generate IDs. Absent → create, present → update. Optionally use PrefixedID for consistency.

 input OidcProviderInput {
   """The unique identifier for the OIDC provider"""
-  id: String!
+  id: String
 }
🧹 Nitpick comments (4)
api/generated-schema.graphql (4)

1429-1431: Prefer non-null list for warnings

Using a non-null list avoids null handling in clients; return an empty array when no warnings.

-  warnings: [String!]
+  warnings: [String!]!

1447-1456: Document evaluation semantics for authorization rules

Clarify behavior on multiple values (any vs all), claim case-sensitivity, and missing-claim handling. This avoids mismatched expectations between UI and backend.


1458-1464: Operator set: consider future-proofing (optional)

If needed later, consider NOT_EQUALS or regex-like operator. Fine to defer until a real use-case emerges.


1527-1530: Optional: include reason for invalid sessions

Adding a short code or message field can help clients present actionable errors (e.g., expired, invalid_signature).

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5f99431 and df150f2.

📒 Files selected for processing (3)
  • api/generated-schema.graphql (6 hunks)
  • api/src/unraid-api/graph/resolvers/sso/sso.module.ts (1 hunks)
  • web/pages/login.vue (9 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • api/src/unraid-api/graph/resolvers/sso/sso.module.ts
  • web/pages/login.vue
🔇 Additional comments (3)
api/generated-schema.graphql (3)

1403-1408: Good addition: SSO settings shape looks consistent and implements Node

Type and field naming align with existing conventions. No issues.


1440-1442: Settings.sso field addition LGTM

Clear namespacing under Settings; sensible default for future SSO providers.


1532-1539: Public provider type looks safe and minimal

Contains only UI-safe fields. No issues.

@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

Copy link
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 (1)
web/__test__/components/SsoButton.test.ts (1)

2-2: Update the header comment to match the component name

The header comment references "SsoButton" but the actual component being tested is "SsoButtons".

-  * SsoButton Component Test Coverage
+  * SsoButtons Component Test Coverage
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between df150f2 and 3510f8a.

📒 Files selected for processing (1)
  • web/__test__/components/SsoButton.test.ts (5 hunks)
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • web/__test__/components/SsoButton.test.ts
web/**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

All web GraphQL code must follow the .query.ts/.mutation.ts filename conventions (do not place operations in other file types)

Files:

  • web/__test__/components/SsoButton.test.ts
**/*.{test,spec}.{js,ts,jsx,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

**/*.{test,spec}.{js,ts,jsx,tsx}: General testing: use .rejects.toThrow() without arguments when asserting thrown errors
General testing: focus on behavior, not exact error message wording
General testing: avoid brittle tests tied to non-essential details (exact messages, log formats)
General testing: use mocks as nouns (objects), not verbs (behavioral overuse)

Files:

  • web/__test__/components/SsoButton.test.ts
web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx}: Web (Vue) component tests should use mount from Vue Test Utils
Web (Vue) component tests should stub complex child components not under test
Web (Vue) component tests should mock external dependencies and services
Web (Vue) component tests should query elements semantically (e.g., find('button')) rather than using data-test IDs
Web (Vue) component tests should use createTestingPinia() for mocking stores in components
Web (Vue) component tests should use await nextTick() for DOM updates and await all async operations before assertions

Files:

  • web/__test__/components/SsoButton.test.ts
web/**/*.{test,spec}.{ts,js,tsx,jsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

web/**/*.{test,spec}.{ts,js,tsx,jsx}: Store testing with Pinia: use createPinia() and setActivePinia; only use createTestingPinia when you need its features
Store testing with Pinia: let stores initialize with natural defaults; do not mock the store under test
Store testing with Pinia: place all mock declarations at top level; use factory functions for module mocks; clear mocks between tests

Files:

  • web/__test__/components/SsoButton.test.ts
web/__test__/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Tests for the web app are located under web/test

Files:

  • web/__test__/components/SsoButton.test.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • web/__test__/components/SsoButton.test.ts
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}

📄 CodeRabbit Inference Engine (.cursor/rules/web-testing-rules.mdc)

{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Files:

  • web/__test__/components/SsoButton.test.ts
**/__test__/components/**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/web-testing-rules.mdc)

**/__test__/components/**/*.ts: Component tests should use mount from @vue/test-utils
Stub complex child components that are not the focus of a component test
Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Use createTestingPinia() to mock Pinia stores when testing Vue components
Test that expected elements are rendered and verify component output rather than internals
Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary
Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Use findComponent(ComponentName) for locating child components and findAll for collections

Files:

  • web/__test__/components/SsoButton.test.ts
🧠 Learnings (18)
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:06:30.877Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T15:06:30.877Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx} : Web (Vue) component tests should mock external dependencies and services

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Component tests should use mount from vue/test-utils

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:06:30.877Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T15:06:30.877Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx} : Web (Vue) component tests should stub complex child components not under test

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Use createTestingPinia() to mock Pinia stores when testing Vue components

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:06:30.877Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T15:06:30.877Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx} : Web (Vue) component tests should use mount from Vue Test Utils

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Stub complex child components that are not the focus of a component test

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:06:30.877Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T15:06:30.877Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx} : Web (Vue) component tests should use createTestingPinia() for mocking stores in components

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:06:30.877Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T15:06:30.877Z
Learning: Applies to web/**/*.{test,spec}.{ts,js,tsx,jsx} : Store testing with Pinia: place all mock declarations at top level; use factory functions for module mocks; clear mocks between tests

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/store/**/*.ts : In store tests, mock external dependencies used by the store and verify interactions with those mocks

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use vi.spyOn() to specify return values or behavior of methods under test

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/mocks/**/*.ts : Frequently used mocks can also be placed under __test__/mocks

Applied to files:

  • web/__test__/components/SsoButton.test.ts
🔇 Additional comments (8)
web/__test__/components/SsoButton.test.ts (8)

12-21: Good stubbing of child component

The SsoProviderButtonStub properly follows the testing guideline to stub complex child components that are not the focus of the test.


83-121: Proper test setup and cleanup

Good use of vi.clearAllTimers(), vi.useFakeTimers(), and vi.restoreAllMocks() for test isolation, following the testing guidelines.


123-156: Well-structured test for provider rendering

The test properly uses flushPromises() and fake timers to handle async operations, following the testing guidelines for Vue component tests.


158-179: Good coverage of empty state

The test properly verifies the behavior when no OIDC providers are configured, using semantic queries as recommended.


181-209: Thorough test of polling behavior

Excellent test coverage of the polling mechanism, properly simulating API unavailability and recovery.


211-252: Comprehensive test of OIDC navigation flow

The test thoroughly verifies the navigation behavior, including state generation and sessionStorage usage.


254-368: Excellent coverage of OIDC callback scenarios

The tests comprehensively cover token-based authentication, error handling, and authorization code flow scenarios, with proper async handling throughout.


370-413: Good test for multi-provider support

The test effectively verifies that multiple OIDC providers are rendered correctly, using semantic queries as recommended.

@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

@coderabbitai coderabbitai bot requested a review from mdatelle August 13, 2025 18:47
Copy link
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: 3

♻️ Duplicate comments (15)
api/generated-schema.graphql (4)

1466-1525: Remove clientSecret from output type and implement Node on OidcProvider.

Leaking clientSecret is a security risk; also this top-level resource should implement Node for consistency. Keep clientSecret only on input/mutations.

-type OidcProvider {
+type OidcProvider implements Node {
   """The unique identifier for the OIDC provider"""
   id: PrefixedID!
@@
-  """OAuth2 client secret (if required by provider)"""
-  clientSecret: String

Note: Apply this change in the source SDL/model; this file is generated.


1945-1956: Protect OIDC queries and fix id type for oidcProvider.

Add authorization directives and use PrefixedID for id consistency.

   """Get public OIDC provider information for login buttons"""
   publicOidcProviders: [PublicOidcProvider!]!

   """Get all configured OIDC providers (admin only)"""
-  oidcProviders: [OidcProvider!]!
+  oidcProviders: [OidcProvider!]! @usePermissions(action: READ, resource: "SSO", possession: ANY)

   """Get a specific OIDC provider by ID"""
-  oidcProvider(id: String!): OidcProvider
+  oidcProvider(id: PrefixedID!): OidcProvider @usePermissions(action: READ, resource: "SSO", possession: ANY)

   """Validate an OIDC session token (internal use for CLI validation)"""
-  validateOidcSession(token: String!): OidcSessionValidation!
+  validateOidcSession(token: String!): OidcSessionValidation! @usePermissions(action: READ, resource: "SSO", possession: OWN)

Note: Apply in the source schema.


2003-2007: Enforce permissions on mutations and use PrefixedID for delete.

Restrict upsert/delete to admins and keep ID types consistent.

   """Create or update an OIDC provider"""
-  upsertOidcProvider(provider: OidcProviderInput!): OidcProvider!
+  upsertOidcProvider(provider: OidcProviderInput!): OidcProvider! @usePermissions(action: UPDATE, resource: "SSO", possession: ANY)

   """Delete an OIDC provider"""
-  deleteOidcProvider(id: String!): Boolean!
+  deleteOidcProvider(id: PrefixedID!): Boolean! @usePermissions(action: DELETE, resource: "SSO", possession: ANY)

Note: Apply in the source schema.


2050-2110: Make id optional in OidcProviderInput to support create vs update.

For upsert semantics, allow clients to omit id on create.

 input OidcProviderInput {
   """The unique identifier for the OIDC provider"""
-  id: String!
+  id: String

Add documentation: absent → create, present → update.

web/pages/login.vue (1)

31-48: Debug modal displaying passwords is acceptable for test view

As previously discussed and documented in the learnings, this debug modal is intentionally designed as a test view where displaying the password in plain text is acceptable for debugging purposes.

web/components/sso/useSsoAuth.ts (1)

11-23: Improve error handling for missing DOM elements

The function logs a warning but still returns potentially null elements, which could cause runtime errors in consuming code when these elements are accessed in enterCallbackTokenIntoField and other functions.

Consider returning null when elements are missing and handling this case in calling functions:

-  const getInputFields = (): {
-    form: HTMLFormElement;
-    passwordField: HTMLInputElement;
-    usernameField: HTMLInputElement;
-  } => {
+  const getInputFields = (): {
+    form: HTMLFormElement;
+    passwordField: HTMLInputElement;
+    usernameField: HTMLInputElement;
+  } | null => {
     const form = document.querySelector('form[action="/login"]') as HTMLFormElement;
     const passwordField = document.querySelector('input[name=password]') as HTMLInputElement;
     const usernameField = document.querySelector('input[name=username]') as HTMLInputElement;
     if (!form || !passwordField || !usernameField) {
       console.warn('Could not find form, username, or password field');
+      return null;
     }
     return { form, passwordField, usernameField };
   };

Then update the calling functions to handle null:

   const enterCallbackTokenIntoField = (token: string) => {
-    const { form, passwordField, usernameField } = getInputFields();
-    if (!passwordField || !usernameField || !form) {
-      console.warn('Could not find form, username, or password field');
-    } else {
+    const fields = getInputFields();
+    if (fields) {
+      const { form, passwordField, usernameField } = fields;
       usernameField.value = 'root';
       passwordField.value = token;
       // Submit the form
       form.requestSubmit();
+    } else {
+      console.warn('Could not find form, username, or password field');
     }
   };
api/src/unraid-api/graph/resolvers/sso/oidc-provider.model.ts (2)

28-43: Duplicate code between OidcAuthorizationRule and OidcAuthorizationRuleInput classes.

These classes have identical field definitions, violating the DRY principle.


62-174: Significant code duplication between OidcProvider and OidcProviderInput classes.

Both classes have nearly identical field definitions and validation decorators. This duplication increases maintenance burden and risk of inconsistencies.

api/src/unraid-api/rest/rest.controller.ts (1)

61-97: Add parameter validation for providerId.

The providerId parameter is passed directly to the service without validation, which could lead to injection attacks.

Apply this diff to add validation:

 async oidcAuthorize(
     @Param('providerId') providerId: string,
     @Query('state') state: string,
     @Req() req: FastifyRequest,
     @Res() res: FastifyReply
 ) {
     try {
         if (!state) {
             return res.status(400).send('State parameter is required');
         }
+        
+        // Validate providerId format to prevent injection attacks
+        if (!/^[a-zA-Z0-9._-]+$/.test(providerId)) {
+            return res.status(400).send('Invalid provider ID format');
+        }

         // Get the host from the request headers
         const host = req.headers.host || undefined;
api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts (3)

96-108: Avoid mutating in-memory config when ensuring unraid.net provider.

The code mutates the providers array obtained directly from the config service by using unshift, which can cause side effects.

Apply this diff to avoid mutation:

 async getProviders(): Promise<OidcProvider[]> {
     const config = this.configService.get<OidcConfig>(this.configKey());
-    const providers = config?.providers || [];
+    const providers = [...(config?.providers || [])];

     // Ensure unraid.net provider is always present
     const hasUnraidNet = providers.some((p) => p.id === 'unraid.net');
     if (!hasUnraidNet) {
         providers.unshift(this.getUnraidNetSsoProvider());
     }

     // Clean up providers - convert empty strings to undefined
     return providers.map((provider) => this.cleanProvider(provider));
 }

110-120: Incomplete field cleaning in cleanProvider method.

The cleanProvider method doesn't clean all optional string fields. jwksUri, buttonText, and buttonVariant are missing.

Apply this diff to ensure consistent behavior:

 private cleanProvider(provider: OidcProvider): OidcProvider {
     // Convert empty strings to undefined for optional fields
     return {
         ...provider,
         clientSecret: provider.clientSecret?.trim() || undefined,
         authorizationEndpoint: provider.authorizationEndpoint?.trim() || undefined,
         tokenEndpoint: provider.tokenEndpoint?.trim() || undefined,
+        jwksUri: provider.jwksUri?.trim() || undefined,
         buttonIcon: provider.buttonIcon?.trim() || undefined,
+        buttonText: provider.buttonText?.trim() || undefined,
+        buttonVariant: provider.buttonVariant?.trim() || undefined,
         buttonStyle: provider.buttonStyle?.trim() || undefined,
     };
 }

291-304: Strengthen URL validation for Unraid.net issuer check.

The issuer validation uses a substring match with includes('unraid.net') which is insufficient and can match invalid URLs.

Apply this diff to properly validate the issuer URL:

 for (const provider of processedConfig.providers) {
-    if (provider.issuer && !provider.issuer.includes('unraid.net')) {
-        // Skip validation for unraid.net as it uses custom auth flow
+    if (provider.issuer) {
+        let isUnraidIssuer = false;
+        try {
+            const host = new URL(provider.issuer).hostname;
+            // Built-in provider runs on account.unraid.net; allow subdomains if needed
+            isUnraidIssuer = host === 'account.unraid.net' || host.endsWith('.unraid.net');
+        } catch {
+            isUnraidIssuer = false;
+        }
+        if (!isUnraidIssuer) {
+            // Validate non-Unraid issuers via discovery
         try {
             const validation = await this.validationService.validateProvider(provider);
             if (!validation.isValid) {
                 validationErrors.push(`❌ ${provider.name}: ${validation.error}`);
             }
         } catch (error) {
             // Don't fail the save, just warn
             this.logger.warn(`Failed to validate provider ${provider.id}: ${error}`);
         }
+        }
     }
 }
api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts (3)

518-528: Consider making redirect URI configurable.

The redirect URI logic has hardcoded values and fragile localhost detection.


239-242: JWT decode uses base64 instead of base64url encoding.

The ID token parsing uses base64 decoding which fails for base64url-encoded tokens. Additionally, it logs full claims which may contain PII.

Apply this diff to fix the decoding and remove PII from logs:

 // Decode the JWT manually
 const payload = tokens.id_token.split('.')[1];
-claims = JSON.parse(Buffer.from(payload, 'base64').toString());
-this.logger.debug(`ID token claims: ${JSON.stringify(claims)}`);
+// Properly handle base64url decoding
+const normalized = payload.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(payload.length / 4) * 4, '=');
+claims = JSON.parse(Buffer.from(normalized, 'base64').toString('utf8'));
+// Avoid logging full claims to prevent PII exposure
+this.logger.debug(`ID token parsed; available claim keys: ${claims ? Object.keys(claims).join(', ') : 'none'}`);

433-502: Consider handling array claims properly in evaluateRule.

The method converts claim values to strings, which collapses array claims (e.g., groups) into comma-separated strings, potentially breaking EQUALS and CONTAINS operators.

Apply this diff to properly handle array claims:

 private evaluateRule(rule: OidcAuthorizationRule, claims: JwtClaims): boolean {
     const claimValue = claims[rule.claim];

     this.logger.debug(
         `Evaluating rule for claim ${rule.claim}: ${JSON.stringify({
             claimValue,
             claimType: typeof claimValue,
             ruleOperator: rule.operator,
             ruleValues: rule.value,
         })}`
     );

     if (claimValue === undefined || claimValue === null) {
         this.logger.debug(`Claim ${rule.claim} not found in token`);
         return false;
     }

     // Log detailed claim analysis
     if (typeof claimValue === 'object' && claimValue !== null) {
         this.logger.warn(
             `unexpected JWT claim value encountered - claim ${rule.claim} is object type: ${JSON.stringify(claimValue)}`
         );
         return false;
     }

     if (Array.isArray(claimValue)) {
-        this.logger.warn(
-            `unexpected JWT claim value encountered - claim ${rule.claim} is array type: ${JSON.stringify(claimValue)}`
-        );
-        return false;
+        // Handle array claims by checking if any element matches
+        this.logger.debug(`Processing array claim ${rule.claim} with ${claimValue.length} values`);
+        const stringValues = claimValue.map(String);
+        
+        switch (rule.operator) {
+            case AuthorizationOperator.EQUALS:
+                return rule.value.some((v) => stringValues.includes(v));
+            case AuthorizationOperator.CONTAINS:
+                return stringValues.some((sv) => rule.value.some((v) => sv.includes(v)));
+            case AuthorizationOperator.STARTS_WITH:
+                return stringValues.some((sv) => rule.value.some((v) => sv.startsWith(v)));
+            case AuthorizationOperator.ENDS_WITH:
+                return stringValues.some((sv) => rule.value.some((v) => sv.endsWith(v)));
+            default:
+                this.logger.error(`Unknown authorization operator: ${rule.operator}`);
+                return false;
+        }
     }

     const value = String(claimValue);
     this.logger.debug(`Processing claim ${rule.claim} with string value: "${value}"`);

     let result: boolean;
     switch (rule.operator) {
         case AuthorizationOperator.EQUALS:
             result = rule.value.some((v) => value === v);
             this.logger.debug(
                 `EQUALS check: "${value}" matches any of [${rule.value.join(', ')}]: ${result}`
             );
             return result;

         case AuthorizationOperator.CONTAINS:
             result = rule.value.some((v) => value.includes(v));
             this.logger.debug(
                 `CONTAINS check: "${value}" contains any of [${rule.value.join(', ')}]: ${result}`
             );
             return result;

         case AuthorizationOperator.STARTS_WITH:
             result = rule.value.some((v) => value.startsWith(v));
             this.logger.debug(
                 `STARTS_WITH check: "${value}" starts with any of [${rule.value.join(', ')}]: ${result}`
             );
             return result;

         case AuthorizationOperator.ENDS_WITH:
             result = rule.value.some((v) => value.endsWith(v));
             this.logger.debug(
                 `ENDS_WITH check: "${value}" ends with any of [${rule.value.join(', ')}]: ${result}`
             );
             return result;

         default:
             this.logger.error(`Unknown authorization operator: ${rule.operator}`);
             return false;
     }
 }
🧹 Nitpick comments (10)
unraid-ui/src/forms/VerticalLayout.vue (1)

46-46: Explicit cast to UISchemaElement is fine; consider typing elements to avoid per-use casts.

Optional: type the computed so the template doesn’t need casting, and prefer a stable key over the array index if available (e.g., element.scope).

Example (outside the changed lines):

const elements = computed<UISchemaElement[]>(() => layout.layout.value.uischema.elements ?? []);

And in template (if available):
:key="(element as any).scope ?? _i"

api/src/unraid-api/graph/resolvers/sso/oidc-session-validation.model.ts (1)

1-10: Looks good; consider adding GraphQL descriptions to aid schema introspection.

Adding descriptions improves auto-generated docs and discoverability.

-import { Field, ObjectType } from '@nestjs/graphql';
+import { Field, ObjectType } from '@nestjs/graphql';
 
-@ObjectType()
+@ObjectType({ description: 'Result of validating a short-lived OIDC session token' })
 export class OidcSessionValidation {
-    @Field(() => Boolean)
+    @Field(() => Boolean, { description: 'Whether the provided token/session is valid' })
     valid!: boolean;
 
-    @Field(() => String, { nullable: true })
+    @Field(() => String, { nullable: true, description: 'Username associated with the token when valid' })
     username?: string;
 }
api/src/unraid-api/cli/sso/validate-token.command.ts (3)

7-12: Update CLI description to reflect actual output shape (includes username).

The command prints username on success, but the description only mentions error and valid.

 @SubCommand({
   name: 'validate-token',
   aliases: ['validate', 'v'],
-  description: 'Returns JSON: { error: string | null, valid: boolean }',
+  description: 'Returns JSON: { error: string | null, valid: boolean, username?: string }',
   arguments: '<token>',
 })

49-52: Unify GraphQL error handling with errorPolicy: 'all'.

Apollo Client throws on GraphQL errors by default, so errors will often be unset. Using errorPolicy: 'all' prevents throwing and ensures errors is populated, keeping all error handling in one branch.

-            const { data, errors } = await client.query({
+            const { data, errors } = await client.query({
                 query: VALIDATE_OIDC_SESSION_QUERY,
                 variables: { token },
+                errorPolicy: 'all',
             });

59-73: Make response handling robust and avoid hardcoding username to 'root'.

  • Fail fast if the response is missing (distinguish from an invalid token).
  • Only include username when present; avoid defaulting to "root" which can mislead downstream tooling.

Do you rely on "root" as a fallback in any consumer? If not, prefer omitting the field when absent (see diff).

-            const validation = data?.validateOidcSession;
+            const validation = data?.validateOidcSession;

-            if (validation?.valid) {
-                this.logger.always(
-                    JSON.stringify({
-                        error: null,
-                        valid: true,
-                        username: validation.username || 'root',
-                    })
-                );
-                process.exit(0);
-            } else {
-                this.createErrorAndExit('Invalid OIDC session token');
-            }
+            if (!validation || typeof validation.valid !== 'boolean') {
+                this.createErrorAndExit('Malformed response: validateOidcSession missing or invalid');
+            }
+            if (!validation.valid) {
+                this.createErrorAndExit('Invalid OIDC session token');
+            }
+            const payload: Record<string, unknown> = {
+                error: null,
+                valid: true,
+            };
+            if (validation.username) {
+                payload.username = validation.username;
+            }
+            this.logger.always(JSON.stringify(payload));
+            process.exit(0);
packages/unraid-shared/src/services/user-settings.ts (1)

126-160: Type-narrowing for fragment values to reduce casts; aggregate warnings looks good.

Logic is sound: skips unknown namespaces, OR’s restartRequired, aggregates warnings, returns full snapshot. Minor type-safety improvement can remove the broad cast in Line 140.

-  async updateNamespacedValues(
-    values: Record<string, unknown>
-  ): Promise<{ restartRequired: boolean; values: Record<string, unknown>; warnings?: string[] }> {
+  async updateNamespacedValues(
+    values: Record<string, unknown>
+  ): Promise<{ restartRequired: boolean; values: Record<string, unknown>; warnings?: string[] }> {
     let restartRequired = false;
     let allWarnings: string[] = [];

     for (const [key, fragmentValues] of Object.entries(values)) {
       if (!this.settings.has(key as keyof UserSettings)) {
         // Skip unknown namespaces – they may belong to other consumers
         continue;
       }
-
-      const result = await this.updateValues(
-        key as keyof UserSettings,
-        fragmentValues as Partial<UserSettings[keyof UserSettings]>
-      );
+      const k = key as keyof UserSettings;
+      // Narrow the type for this specific namespace; avoids casting to union of all fragments
+      const result = await this.updateValues(k, fragmentValues as Partial<UserSettings[typeof k]>);

If you prefer returning an empty warnings array instead of omitting the field (to stabilize response shape), I can provide a follow-up diff.

api/.env.development (1)

20-20: Align with dotenv-linter: keep keys ordered.

Move PATHS_OIDC_JSON before PATHS_PARITY_CHECKS for alphabetical ordering.

 PATHS_KEYFILE_BASE=./dev/Unraid.net # Keyfile location
 PATHS_MACHINE_ID=./dev/data/machine-id
-PATHS_PARITY_CHECKS=./dev/states/parity-checks.log
+PATHS_OIDC_JSON=./dev/configs/oidc.local.json
+PATHS_PARITY_CHECKS=./dev/states/parity-checks.log
 PATHS_CONFIG_MODULES=./dev/configs
 PATHS_ACTIVATION_BASE=./dev/activation
 PATHS_PASSWD=./dev/passwd
@@
-PATHS_OIDC_JSON=./dev/configs/oidc.local.json
api/dev/configs/README.md (2)

10-17: Clarify gitignore status and use the repo-relative path in PATHS_OIDC_JSON.

The file is already ignored in api/.gitignore; no need to edit .gitignore. Also prefer the repo-relative path used in .env.development.

-### Using a Local Configuration (gitignored)
-For local testing with real OAuth providers:
+### Using a Local Configuration (gitignored)
+For local testing with real OAuth providers:
 
-1. Create an `oidc.local.json` file based on `oidc.local.example.json`
-2. Add it to your local `.gitignore` to prevent committing secrets
-3. Set the environment variable: `PATHS_OIDC_JSON=oidc.local.json`
-4. The API will load your local configuration instead of the default
+1. Create an `oidc.local.json` file based on `oidc.local.example.json`
+2. No action needed for gitignore (already ignored by api/.gitignore)
+3. Set the environment variable: `PATHS_OIDC_JSON=./dev/configs/oidc.local.json`
+4. The API will load your local configuration instead of the default
-Example:
+Example:
 ```bash
-PATHS_OIDC_JSON=oidc.local.json pnpm dev
+PATHS_OIDC_JSON=./dev/configs/oidc.local.json pnpm dev

---

`32-35`: **Minor wording tweak for GitHub section.**

Optional, improves clarity.


```diff
-1. Go to GitHub Settings > Developer settings > OAuth Apps
+1. Go to GitHub Settings → Developer settings → OAuth Apps
 2. Create a new OAuth App
 3. Set Authorization callback URL: `http://localhost:3000/graphql/api/auth/oidc/callback`
unraid-ui/src/forms/ObjectArrayField.vue (1)

126-151: Consider extracting default value initialization logic

The default value initialization logic is complex and could benefit from being extracted into a separate utility function for better maintainability and testability.

Consider extracting this logic into a utility function:

+const getDefaultValueForSchema = (schema: JsonSchema): unknown => {
+  if (schema.default !== undefined) {
+    return schema.default;
+  }
+  switch (schema.type) {
+    case 'array':
+      return [];
+    case 'string':
+      return '';
+    case 'number':
+    case 'integer':
+      return 0;
+    case 'boolean':
+      return false;
+    default:
+      return undefined;
+  }
+};

 const addItem = () => {
   const schema = control.value.schema;
   const newItem: Record<string, unknown> = {};

   // Initialize with default values if available
   if (schema?.items && typeof schema.items === 'object' && !Array.isArray(schema.items)) {
     const properties = schema.items.properties;
     if (properties && typeof properties === 'object') {
       Object.entries(properties).forEach(([key, propSchema]) => {
         const schema = propSchema as JsonSchema;
-        if (schema.default !== undefined) {
-          newItem[key] = schema.default;
-        } else if (schema.type === 'array') {
-          newItem[key] = [];
-        } else if (schema.type === 'string') {
-          newItem[key] = '';
-        } else if (schema.type === 'number' || schema.type === 'integer') {
-          newItem[key] = 0;
-        } else if (schema.type === 'boolean') {
-          newItem[key] = false;
-        }
+        const defaultValue = getDefaultValueForSchema(schema);
+        if (defaultValue !== undefined) {
+          newItem[key] = defaultValue;
+        }
       });
     }
   }

   items.value = [...items.value, newItem];
 };
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3510f8a and 9134ffc.

⛔ Files ignored due to path filters (3)
  • api/src/unraid-api/cli/generated/gql.ts is excluded by !**/generated/**
  • api/src/unraid-api/cli/generated/graphql.ts is excluded by !**/generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (57)
  • api/.env.development (1 hunks)
  • api/.gitignore (1 hunks)
  • api/dev/configs/README.md (1 hunks)
  • api/dev/configs/api.json (1 hunks)
  • api/dev/configs/connect.json (1 hunks)
  • api/dev/configs/oidc.json (1 hunks)
  • api/docs/public/index.md (1 hunks)
  • api/docs/public/oidc-provider-examples.md (1 hunks)
  • api/generated-schema.graphql (6 hunks)
  • api/package.json (2 hunks)
  • api/src/unraid-api/app/app.module.ts (1 hunks)
  • api/src/unraid-api/cli/queries/validate-oidc-session.query.ts (1 hunks)
  • api/src/unraid-api/cli/sso/validate-token.command.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/settings/settings.model.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/settings/settings.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/settings/settings.resolver.ts (6 hunks)
  • api/src/unraid-api/graph/resolvers/settings/settings.service.ts (4 hunks)
  • api/src/unraid-api/graph/resolvers/settings/sso-settings.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.test.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/oidc-provider.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/oidc-session-validation.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/oidc-validation.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/public-oidc-provider.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/sso-settings.types.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/sso.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/utils/form-utils.ts (1 hunks)
  • api/src/unraid-api/rest/rest.controller.ts (2 hunks)
  • api/src/unraid-api/rest/rest.module.ts (1 hunks)
  • packages/unraid-shared/package.json (1 hunks)
  • packages/unraid-shared/src/services/user-settings.ts (3 hunks)
  • unraid-ui/src/components/common/index.ts (1 hunks)
  • unraid-ui/src/components/form/index.ts (1 hunks)
  • unraid-ui/src/forms/HorizontalLayout.vue (2 hunks)
  • unraid-ui/src/forms/ObjectArrayField.vue (1 hunks)
  • unraid-ui/src/forms/SteppedLayout.vue (2 hunks)
  • unraid-ui/src/forms/UnraidSettingsLayout.vue (2 hunks)
  • unraid-ui/src/forms/VerticalLayout.vue (2 hunks)
  • unraid-ui/src/forms/renderers.ts (4 hunks)
  • web/__test__/components/SsoButton.test.ts (5 hunks)
  • web/components/SsoButton.ce.vue (1 hunks)
  • web/components/queries/oidc-providers.query.ts (1 hunks)
  • web/components/queries/public-oidc-providers.query.ts (1 hunks)
  • web/components/sso/SsoButtons.vue (1 hunks)
  • web/components/sso/SsoProviderButton.vue (1 hunks)
  • web/components/sso/useSsoAuth.ts (1 hunks)
  • web/components/sso/useSsoProviders.ts (1 hunks)
  • web/composables/gql/gql.ts (3 hunks)
  • web/composables/gql/graphql.ts (16 hunks)
  • web/nuxt.config.ts (1 hunks)
  • web/pages/login.vue (9 hunks)
  • web/server/api/debug/validate-token.post.ts (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • api/dev/configs/api.json
  • api/docs/public/oidc-provider-examples.md
🚧 Files skipped from review as they are similar to previous changes (36)
  • packages/unraid-shared/package.json
  • api/src/unraid-api/graph/resolvers/sso/sso.module.ts
  • web/nuxt.config.ts
  • api/package.json
  • api/src/unraid-api/rest/rest.module.ts
  • api/src/unraid-api/graph/resolvers/settings/sso-settings.model.ts
  • api/src/unraid-api/graph/resolvers/sso/public-oidc-provider.model.ts
  • unraid-ui/src/forms/SteppedLayout.vue
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts
  • api/src/unraid-api/cli/queries/validate-oidc-session.query.ts
  • api/src/unraid-api/graph/resolvers/settings/settings.model.ts
  • web/components/queries/oidc-providers.query.ts
  • api/dev/configs/connect.json
  • api/src/unraid-api/graph/resolvers/sso/sso-settings.types.ts
  • api/src/unraid-api/graph/utils/form-utils.ts
  • api/.gitignore
  • api/dev/configs/oidc.json
  • web/composables/gql/gql.ts
  • unraid-ui/src/forms/UnraidSettingsLayout.vue
  • unraid-ui/src/components/form/index.ts
  • api/src/unraid-api/app/app.module.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.test.ts
  • web/components/sso/SsoProviderButton.vue
  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts
  • web/components/sso/SsoButtons.vue
  • unraid-ui/src/components/common/index.ts
  • web/components/SsoButton.ce.vue
  • api/src/unraid-api/graph/resolvers/settings/settings.service.ts
  • web/server/api/debug/validate-token.post.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-validation.service.ts
  • api/src/unraid-api/graph/resolvers/settings/settings.module.ts
  • web/test/components/SsoButton.test.ts
  • web/components/sso/useSsoProviders.ts
  • web/composables/gql/graphql.ts
  • api/src/unraid-api/graph/resolvers/settings/settings.resolver.ts
🧰 Additional context used
📓 Path-based instructions (9)
web/**/*.query.ts

📄 CodeRabbit Inference Engine (.cursor/rules/web-graphql.mdc)

GraphQL queries must be placed in .query.ts files

In the web app, GraphQL queries must live in files ending with .query.ts

Files:

  • web/components/queries/public-oidc-providers.query.ts
web/**/*.{query,mutation}.ts

📄 CodeRabbit Inference Engine (.cursor/rules/web-graphql.mdc)

All GraphQL under web/ must follow this naming convention

Files:

  • web/components/queries/public-oidc-providers.query.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • web/components/queries/public-oidc-providers.query.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
  • web/components/sso/useSsoAuth.ts
  • packages/unraid-shared/src/services/user-settings.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-session-validation.model.ts
  • api/src/unraid-api/rest/rest.controller.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-provider.model.ts
  • unraid-ui/src/forms/renderers.ts
  • api/src/unraid-api/cli/sso/validate-token.command.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts
web/**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

All web GraphQL code must follow the .query.ts/.mutation.ts filename conventions (do not place operations in other file types)

Files:

  • web/components/queries/public-oidc-providers.query.ts
  • web/components/sso/useSsoAuth.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • web/components/queries/public-oidc-providers.query.ts
  • unraid-ui/src/forms/HorizontalLayout.vue
  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
  • unraid-ui/src/forms/VerticalLayout.vue
  • web/components/sso/useSsoAuth.ts
  • packages/unraid-shared/src/services/user-settings.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-session-validation.model.ts
  • api/src/unraid-api/rest/rest.controller.ts
  • web/pages/login.vue
  • unraid-ui/src/forms/ObjectArrayField.vue
  • api/src/unraid-api/graph/resolvers/sso/oidc-provider.model.ts
  • unraid-ui/src/forms/renderers.ts
  • api/src/unraid-api/cli/sso/validate-token.command.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts
api/src/unraid-api/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Place new API source files in api/src/unraid-api/ rather than legacy locations

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-session-validation.model.ts
  • api/src/unraid-api/rest/rest.controller.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-provider.model.ts
  • api/src/unraid-api/cli/sso/validate-token.command.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts
api/**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Prefer not to mock simple dependencies in API tests

Files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts
**/*.{test,spec}.{js,ts,jsx,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

**/*.{test,spec}.{js,ts,jsx,tsx}: General testing: use .rejects.toThrow() without arguments when asserting thrown errors
General testing: focus on behavior, not exact error message wording
General testing: avoid brittle tests tied to non-essential details (exact messages, log formats)
General testing: use mocks as nouns (objects), not verbs (behavioral overuse)

Files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts
api/**/*.{test,spec}.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/api-rules.mdc)

api/**/*.{test,spec}.{js,jsx,ts,tsx}: Use Vitest for tests in the api; do not use Jest
Prefer not to mock simple dependencies in tests
For error testing, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless the message format is the subject under test

Files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts
🧠 Learnings (5)
📚 Learning: 2025-08-11T15:41:23.573Z
Learnt from: elibosley
PR: unraid/api#1563
File: web/pages/login.vue:31-48
Timestamp: 2025-08-11T15:41:23.573Z
Learning: In the Unraid API codebase, debug/test views (such as the debug modal in `web/pages/login.vue`) may intentionally display sensitive information like passwords in plain text for debugging and testing purposes. This is an accepted practice for development tools in this project.

Applied to files:

  • web/pages/login.vue
📚 Learning: 2025-04-25T13:29:07.868Z
Learnt from: elibosley
PR: unraid/api#1369
File: web/components/Activation/WelcomeModal.ce.vue:40-40
Timestamp: 2025-04-25T13:29:07.868Z
Learning: The `#confirmPassword` selector in the WelcomeModal.ce.vue component is intentionally looking for an element that exists in the Unraid OS login page (.login.php), not within the component itself. This is used as part of a font-size adjustment workaround to handle different CSS between login and authenticated pages.

Applied to files:

  • web/pages/login.vue
📚 Learning: 2024-12-09T15:45:46.492Z
Learnt from: pujitm
PR: unraid/api#975
File: web/components/Notifications/TabList.vue:1-4
Timestamp: 2024-12-09T15:45:46.492Z
Learning: In our Nuxt.js setup for the `web` project, it's not necessary to explicitly import `computed` from `vue` in Vue components, as it's globally available.

Applied to files:

  • web/pages/login.vue
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/{store,stores}/**/*.ts : In store files, explicitly import Vue reactivity utilities (e.g., computed, ref, watchEffect); do not rely on Nuxt auto-imports

Applied to files:

  • web/pages/login.vue
📚 Learning: 2025-03-12T13:35:43.900Z
Learnt from: pujitm
PR: unraid/api#1211
File: web/codegen.ts:14-14
Timestamp: 2025-03-12T13:35:43.900Z
Learning: The JSON scalar type in web/codegen.ts was temporarily changed from 'string' to 'any' for compatibility with JsonForms integration. This change facilitates the implementation of the Connect settings web component.

Applied to files:

  • unraid-ui/src/forms/renderers.ts
🧬 Code Graph Analysis (7)
web/components/queries/public-oidc-providers.query.ts (1)
web/composables/gql/gql.ts (1)
  • graphql (276-278)
api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts (2)
web/components/sso/useSsoProviders.ts (1)
  • OidcProvider (5-12)
web/composables/gql/graphql.ts (1)
  • OidcProvider (1145-1175)
packages/unraid-shared/src/services/user-settings.ts (1)
api/src/unraid-api/graph/resolvers/settings/settings.resolver.ts (1)
  • values (99-101)
api/src/unraid-api/graph/resolvers/sso/oidc-session-validation.model.ts (3)
api/src/unraid-api/graph/resolvers/sso/oidc-provider.model.ts (2)
  • ObjectType (28-43)
  • ObjectType (62-174)
api/src/unraid-api/graph/resolvers/sso/public-oidc-provider.model.ts (1)
  • ObjectType (3-22)
api/src/unraid-api/cli/generated/graphql.ts (1)
  • OidcSessionValidation (1255-1259)
api/src/unraid-api/graph/resolvers/sso/oidc-provider.model.ts (4)
api/src/unraid-api/graph/resolvers/sso/public-oidc-provider.model.ts (1)
  • ObjectType (3-22)
api/src/unraid-api/cli/generated/graphql.ts (3)
  • OidcAuthorizationRule (1169-1177)
  • OidcProvider (1188-1220)
  • OidcProviderInput (1222-1253)
web/components/sso/useSsoProviders.ts (1)
  • OidcProvider (5-12)
web/composables/gql/graphql.ts (2)
  • OidcProvider (1145-1175)
  • OidcProviderInput (1177-1206)
api/src/unraid-api/cli/sso/validate-token.command.ts (3)
web/helpers/create-apollo-client.ts (1)
  • client (93-96)
api/src/unraid-api/cli/queries/validate-oidc-session.query.ts (1)
  • VALIDATE_OIDC_SESSION_QUERY (3-10)
api/src/unraid-api/cli/log.service.ts (1)
  • error (65-69)
api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts (4)
web/components/sso/useSsoProviders.ts (1)
  • OidcProvider (5-12)
web/composables/gql/graphql.ts (1)
  • OidcProvider (1145-1175)
api/src/unraid-api/types/json-forms.ts (1)
  • SettingSlice (38-46)
api/src/unraid-api/graph/utils/form-utils.ts (1)
  • createSimpleLabeledControl (8-45)
🪛 dotenv-linter (3.3.0)
api/.env.development

[warning] 20-20: [UnorderedKey] The PATHS_OIDC_JSON key should go before the PATHS_PARITY_CHECKS key

🪛 LanguageTool
api/dev/configs/README.md

[grammar] ~10-~10: There might be a mistake here.
Context: ...Using a Local Configuration (gitignored) For local testing with real OAuth provid...

(QB_NEW_EN)


[grammar] ~25-~25: There might be a mistake here.
Context: ... ### Setting up OAuth Apps #### Google 1. Go to [Google Cloud Console](https://con...

(QB_NEW_EN)


[grammar] ~27-~27: There might be a mistake here.
Context: ...google.com/) 2. Create a new project or select existing 3. Enable Google+ API 4. Creat...

(QB_NEW_EN)


[grammar] ~27-~27: There might be a mistake here.
Context: ... Create a new project or select existing 3. Enable Google+ API 4. Create OAuth 2.0 c...

(QB_NEW_EN)


[grammar] ~32-~32: There might be a mistake here.
Context: ...hql/api/auth/oidc/callback` #### GitHub 1. Go to GitHub Settings > Developer settin...

(QB_NEW_EN)

api/docs/public/index.md

[grammar] ~25-~25: There might be a mistake here.
Context: ...xamples for SSO authentication providers - [Upcoming Features](./upcoming-features.m...

(QB_NEW_EN)

🪛 GitHub Check: CodeQL
api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts

[failure] 292-292: Incomplete URL substring sanitization
'unraid.net' can be anywhere in the URL, and arbitrary hosts may come before or after it.

🪛 Gitleaks (8.27.2)
api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts

91-91: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)

🔇 Additional comments (20)
unraid-ui/src/forms/VerticalLayout.vue (1)

18-18: Type-only UISchemaElement import is correct and consistent with other layouts.

Matches the typing pattern used across the layouts; no runtime impact.

unraid-ui/src/forms/HorizontalLayout.vue (2)

18-18: Type-only UISchemaElement import aligns with the shared typing approach.

Consistent with other form layouts; no runtime behavior change.


38-38: Casting uischema to UISchemaElement is appropriate here.

Straightforward and consistent with VerticalLayout; safe for DispatchRenderer typing.

web/components/queries/public-oidc-providers.query.ts (1)

3-14: Query shape matches the PublicOidcProvider GraphQL type.

Fields align with the backend model; placement under web/**/*.query.ts follows the convention.

packages/unraid-shared/src/services/user-settings.ts (1)

20-23: Propagate warnings from fragments: good addition.

Adding warnings?: string[] to fragment updates enables end-to-end surfacing of validation messages. This aligns with the schema’s UpdateSettingsResponse.warnings.

api/src/unraid-api/graph/resolvers/sso/oidc-session.service.spec.ts (3)

91-91: Hardcoded JWT in test is acceptable for testing purposes

The hardcoded JWT token detected by static analysis is intentionally used as test data. Since this is a test file and the token is not a real credential, this is an acceptable practice.


1-7: Correct import extension for TypeScript/ESM compatibility

The import statement correctly uses the .js extension for ESM compatibility, which aligns with the TypeScript configuration requirements.


34-120: Well-structured test coverage for one-time token validation

The test suite comprehensively covers the critical security aspects of one-time token validation:

  • Successful first validation
  • Prevention of token reuse
  • Invalid token format handling
  • Non-existent session handling
  • Expired session handling

The tests properly mock the cache manager and verify the expected behavior without brittle assertions on exact error messages.

web/pages/login.vue (2)

2-2: Explicit Vue imports follow framework patterns

The explicit imports of ref and onMounted from Vue are appropriate here as this is a page component, not a store file where explicit imports would be required according to the coding guidelines.


50-79: CLI command execution properly handles response typing

The function correctly handles the response from the debug endpoint with runtime type checking using the in operator to safely access properties. This approach is appropriate for handling the dynamic response structure.

web/components/sso/useSsoAuth.ts (2)

1-2: Correct import usage for Vue composition utilities

The explicit imports from vue and vue-i18n are appropriate for this composable function, following Vue 3 composition API patterns.


64-75: State management and CSRF protection implementation looks good

The navigateToProvider function correctly:

  • Generates a cryptographically secure state token for CSRF protection
  • Stores both the state and provider ID in sessionStorage
  • Properly encodes URI components when constructing the redirect URL
unraid-ui/src/forms/ObjectArrayField.vue (2)

199-211: Good fix for dynamic item type in remove button

The remove button now correctly uses getItemLabel(item, index) instead of the hardcoded "Remove Provider" text, making the component properly reusable for different item types.


32-43: Tab management logic handles edge cases correctly

The watch effect properly manages the active tab index when items are added or removed, ensuring the UI always shows a valid tab.

unraid-ui/src/forms/renderers.ts (3)

51-55: Object array detection logic is correct

The isObjectArray function correctly identifies arrays of objects by checking both the array type and the nested items type.


57-71: String schema detection handles optional URL pattern correctly

The isStringOrAnyOfString function properly identifies string fields including the special case of optional URL fields using the anyOf pattern with minLength and maxLength constraints.


102-103: Renderer priority adjustments ensure correct precedence

The updated renderer priorities ensure that:

  • Object arrays (priority 5) are handled before generic controls
  • Enum controls (priority 6) have the highest priority to avoid conflicts with string renderers
  • The new string renderer (priority 4) doesn't interfere with enums due to the explicit enum exclusion in isStringOrAnyOfString

Also applies to: 105-106, 112-115, 124-127

api/src/unraid-api/graph/resolvers/sso/oidc-provider.model.ts (1)

3-3: Correct import extension for TypeScript ESM compatibility.

The import specifier should use .js extension for ESM compatibility in TypeScript source files, not .ts.

Apply this diff to correct the import extension:

-import { PrefixedID } from '@unraid/shared/prefixed-id-scalar.js';
+import { PrefixedID } from '@unraid/shared/prefixed-id-scalar.js';

Wait, the import already has .js extension which is correct. Let me reconsider.

api/src/unraid-api/rest/rest.controller.ts (2)

116-119: Properly honor X-Forwarded headers.

The code correctly retrieves reverse proxy headers for constructing the callback URL.


139-156: Secure error handling implementation.

The error handling correctly avoids exposing sensitive information to users while logging detailed errors for debugging purposes.

Copy link
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: 2

🧹 Nitpick comments (1)
web/__test__/components/SsoButton.test.ts (1)

13-21: Improve type safety of the component stub.

The stub's this typing in the handleClick method could be more robust by using proper component interfaces.

 const SsoProviderButtonStub = {
   template: '<button @click="handleClick" :disabled="disabled">{{ provider.buttonText || `Sign in with ${provider.name}` }}</button>',
   props: ['provider', 'disabled', 'onClick'],
   methods: {
-    handleClick(this: { onClick: (id: string) => void; provider: { id: string } }) {
+    handleClick(this: { onClick: (id: string) => void; provider: { id: string; name: string; buttonText?: string } }) {
       this.onClick(this.provider.id);
     }
   }
 };
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9134ffc and 2d949c7.

📒 Files selected for processing (2)
  • web/__test__/components/SsoButton.test.ts (5 hunks)
  • web/components/queries/public-oidc-providers.query.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/components/queries/public-oidc-providers.query.ts
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • web/__test__/components/SsoButton.test.ts
web/**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

All web GraphQL code must follow the .query.ts/.mutation.ts filename conventions (do not place operations in other file types)

Files:

  • web/__test__/components/SsoButton.test.ts
**/*.{test,spec}.{js,ts,jsx,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

**/*.{test,spec}.{js,ts,jsx,tsx}: General testing: use .rejects.toThrow() without arguments when asserting thrown errors
General testing: focus on behavior, not exact error message wording
General testing: avoid brittle tests tied to non-essential details (exact messages, log formats)
General testing: use mocks as nouns (objects), not verbs (behavioral overuse)

Files:

  • web/__test__/components/SsoButton.test.ts
web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx}: Web (Vue) component tests should use mount from Vue Test Utils
Web (Vue) component tests should stub complex child components not under test
Web (Vue) component tests should mock external dependencies and services
Web (Vue) component tests should query elements semantically (e.g., find('button')) rather than using data-test IDs
Web (Vue) component tests should use createTestingPinia() for mocking stores in components
Web (Vue) component tests should use await nextTick() for DOM updates and await all async operations before assertions

Files:

  • web/__test__/components/SsoButton.test.ts
web/**/*.{test,spec}.{ts,js,tsx,jsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

web/**/*.{test,spec}.{ts,js,tsx,jsx}: Store testing with Pinia: use createPinia() and setActivePinia; only use createTestingPinia when you need its features
Store testing with Pinia: let stores initialize with natural defaults; do not mock the store under test
Store testing with Pinia: place all mock declarations at top level; use factory functions for module mocks; clear mocks between tests

Files:

  • web/__test__/components/SsoButton.test.ts
web/__test__/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Tests for the web app are located under web/test

Files:

  • web/__test__/components/SsoButton.test.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • web/__test__/components/SsoButton.test.ts
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}

📄 CodeRabbit Inference Engine (.cursor/rules/web-testing-rules.mdc)

{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Files:

  • web/__test__/components/SsoButton.test.ts
**/__test__/components/**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/web-testing-rules.mdc)

**/__test__/components/**/*.ts: Component tests should use mount from @vue/test-utils
Stub complex child components that are not the focus of a component test
Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Use createTestingPinia() to mock Pinia stores when testing Vue components
Test that expected elements are rendered and verify component output rather than internals
Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary
Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Use findComponent(ComponentName) for locating child components and findAll for collections

Files:

  • web/__test__/components/SsoButton.test.ts
🧠 Learnings (18)
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:06:30.877Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T15:06:30.877Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx} : Web (Vue) component tests should mock external dependencies and services

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Component tests should use mount from vue/test-utils

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:06:30.877Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T15:06:30.877Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx} : Web (Vue) component tests should stub complex child components not under test

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Use createTestingPinia() to mock Pinia stores when testing Vue components

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:06:30.877Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T15:06:30.877Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx} : Web (Vue) component tests should use mount from Vue Test Utils

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Stub complex child components that are not the focus of a component test

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:06:30.877Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T15:06:30.877Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx} : Web (Vue) component tests should use createTestingPinia() for mocking stores in components

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:06:30.877Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T15:06:30.877Z
Learning: Applies to web/**/*.{test,spec}.{ts,js,tsx,jsx} : Store testing with Pinia: place all mock declarations at top level; use factory functions for module mocks; clear mocks between tests

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/store/**/*.ts : In store tests, mock external dependencies used by the store and verify interactions with those mocks

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use vi.spyOn() to specify return values or behavior of methods under test

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/mocks/**/*.ts : Frequently used mocks can also be placed under __test__/mocks

Applied to files:

  • web/__test__/components/SsoButton.test.ts
🔇 Additional comments (9)
web/__test__/components/SsoButton.test.ts (9)

84-85: Good testing practice - using fake timers for time-dependent behavior.

The use of fake timers to control time-based behavior in tests is excellent. This ensures deterministic testing of polling and timeout mechanisms.


122-155: Comprehensive test for provider rendering with proper async handling.

The test properly handles async operations with flushPromises() and vi.runAllTimers(), ensuring all promises and timers are resolved before assertions. The test coverage includes both the provider data and UI rendering verification.


180-208: Excellent test coverage for polling behavior.

This test effectively verifies the polling mechanism by mocking a failed first attempt followed by a successful one, and properly tests the "checking" state display. The timer advancement logic correctly simulates the polling interval.


210-251: Thorough navigation test with proper state management verification.

The test comprehensively verifies the SSO flow including sessionStorage interactions, URL construction, and navigation behavior. Good use of expect.any(String) for the generated state parameter.


253-289: Well-structured token callback test.

The test properly simulates the token callback scenario and verifies all expected side effects: form hiding, credential population, form submission, and URL cleanup. The mock setup is comprehensive and realistic.


291-331: Comprehensive error handling test.

This test effectively covers the error callback scenario, verifying both the error display and that the form remains visible for user interaction. The URL cleanup verification is a nice touch.


333-367: Good coverage of code/state callback redirect flow.

The test properly verifies the OAuth2 authorization code flow redirection to the callback endpoint with proper parameter encoding.


369-411: Excellent multi-provider test coverage.

This test validates the component's ability to handle multiple OIDC providers, ensuring both buttons are rendered and the correct text is displayed. The test data includes varied provider configurations which is realistic.


1-413: Overall test quality assessment.

This is a comprehensive test suite that follows Vue testing best practices:

  • Proper use of mount() from Vue Test Utils
  • Appropriate stubbing of child components
  • Comprehensive mocking of external dependencies
  • Good async operation handling with flushPromises() and fake timers
  • Semantic element queries using find('button')
  • Thorough coverage of component behavior including edge cases

The test suite effectively covers the multi-provider SSO functionality with realistic scenarios and proper mock isolation.

Copy link
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 (2)
web/__test__/components/SsoButton.test.ts (2)

13-21: Consider extracting the stub component for reusability

The SsoProviderButtonStub component is well-structured but could be extracted to a separate test utilities file for potential reuse across other tests.

Consider moving this stub to web/__test__/mocks/sso-provider-button.stub.ts to follow the testing conventions and make it reusable across other test files that might need to stub the SsoProviderButton component.


243-245: Consider adding assertion for state format

While the test verifies that a state is generated and stored, it could also validate the format or length of the generated state value to ensure it meets security requirements (e.g., sufficient entropy).

 // Should set state and provider in sessionStorage
 expect(sessionStorage.setItem).toHaveBeenCalledWith('sso_state', expect.any(String));
 expect(sessionStorage.setItem).toHaveBeenCalledWith('sso_provider', 'unraid-net');
+
+// Validate state has sufficient entropy (e.g., at least 32 characters)
+const generatedState = (sessionStorage.setItem as Mock).mock.calls[0][1];
+expect(generatedState.length).toBeGreaterThanOrEqual(32);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9134ffc and 2d949c7.

📒 Files selected for processing (2)
  • web/__test__/components/SsoButton.test.ts (5 hunks)
  • web/components/queries/public-oidc-providers.query.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/components/queries/public-oidc-providers.query.ts
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • web/__test__/components/SsoButton.test.ts
web/**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

All web GraphQL code must follow the .query.ts/.mutation.ts filename conventions (do not place operations in other file types)

Files:

  • web/__test__/components/SsoButton.test.ts
**/*.{test,spec}.{js,ts,jsx,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

**/*.{test,spec}.{js,ts,jsx,tsx}: General testing: use .rejects.toThrow() without arguments when asserting thrown errors
General testing: focus on behavior, not exact error message wording
General testing: avoid brittle tests tied to non-essential details (exact messages, log formats)
General testing: use mocks as nouns (objects), not verbs (behavioral overuse)

Files:

  • web/__test__/components/SsoButton.test.ts
web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx}: Web (Vue) component tests should use mount from Vue Test Utils
Web (Vue) component tests should stub complex child components not under test
Web (Vue) component tests should mock external dependencies and services
Web (Vue) component tests should query elements semantically (e.g., find('button')) rather than using data-test IDs
Web (Vue) component tests should use createTestingPinia() for mocking stores in components
Web (Vue) component tests should use await nextTick() for DOM updates and await all async operations before assertions

Files:

  • web/__test__/components/SsoButton.test.ts
web/**/*.{test,spec}.{ts,js,tsx,jsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

web/**/*.{test,spec}.{ts,js,tsx,jsx}: Store testing with Pinia: use createPinia() and setActivePinia; only use createTestingPinia when you need its features
Store testing with Pinia: let stores initialize with natural defaults; do not mock the store under test
Store testing with Pinia: place all mock declarations at top level; use factory functions for module mocks; clear mocks between tests

Files:

  • web/__test__/components/SsoButton.test.ts
web/__test__/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Tests for the web app are located under web/test

Files:

  • web/__test__/components/SsoButton.test.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • web/__test__/components/SsoButton.test.ts
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}

📄 CodeRabbit Inference Engine (.cursor/rules/web-testing-rules.mdc)

{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Files:

  • web/__test__/components/SsoButton.test.ts
**/__test__/components/**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/web-testing-rules.mdc)

**/__test__/components/**/*.ts: Component tests should use mount from @vue/test-utils
Stub complex child components that are not the focus of a component test
Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Use createTestingPinia() to mock Pinia stores when testing Vue components
Test that expected elements are rendered and verify component output rather than internals
Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary
Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Use findComponent(ComponentName) for locating child components and findAll for collections

Files:

  • web/__test__/components/SsoButton.test.ts
🧠 Learnings (18)
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:06:30.877Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T15:06:30.877Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx} : Web (Vue) component tests should mock external dependencies and services

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Component tests should use mount from vue/test-utils

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:06:30.877Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T15:06:30.877Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx} : Web (Vue) component tests should stub complex child components not under test

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Use createTestingPinia() to mock Pinia stores when testing Vue components

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:06:30.877Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T15:06:30.877Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx} : Web (Vue) component tests should use mount from Vue Test Utils

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Stub complex child components that are not the focus of a component test

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:06:30.877Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T15:06:30.877Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx} : Web (Vue) component tests should use createTestingPinia() for mocking stores in components

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:06:30.877Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T15:06:30.877Z
Learning: Applies to web/**/*.{test,spec}.{ts,js,tsx,jsx} : Store testing with Pinia: place all mock declarations at top level; use factory functions for module mocks; clear mocks between tests

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/store/**/*.ts : In store tests, mock external dependencies used by the store and verify interactions with those mocks

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use vi.spyOn() to specify return values or behavior of methods under test

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/mocks/**/*.ts : Frequently used mocks can also be placed under __test__/mocks

Applied to files:

  • web/__test__/components/SsoButton.test.ts
🔇 Additional comments (7)
web/__test__/components/SsoButton.test.ts (7)

84-86: Good practice: Using fake timers for polling behavior

Excellent use of fake timers to test the polling mechanism. This ensures tests run quickly and deterministically without waiting for real timeouts.


122-155: Well-structured test for OIDC providers rendering

The test properly validates the rendering of provider buttons and includes appropriate waiting for async operations with flushPromises() and timer advancement.


180-208: Comprehensive polling state test

Good coverage of the polling behavior, including error handling and retry logic. The test effectively simulates API unavailability and recovery.


253-289: Good test coverage for token-based authentication flow

The test properly validates the token handling flow, including form manipulation and submission. The cleanup of URL parameters is also correctly tested.


291-331: Thorough error handling test

Excellent coverage of the error scenario, including UI feedback (red error text) and URL cleanup while preserving the form's visibility.


333-367: Well-implemented OAuth code flow test

The test correctly validates the redirect behavior when OAuth authorization code and state are present in the URL parameters.


369-411: Comprehensive multi-provider test

Excellent test that validates the rendering and behavior with multiple OIDC providers, ensuring the component can handle various provider configurations including custom styling and icons.

Copy link
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

🧹 Nitpick comments (2)
web/__test__/components/SsoButton.test.ts (2)

13-21: Consider using a simpler stub implementation.

The current stub has a complex method binding. Consider simplifying it to avoid potential this context issues.

 const SsoProviderButtonStub = {
   template: '<button @click="handleClick" :disabled="disabled">{{ provider.buttonText || `Sign in with ${provider.name}` }}</button>',
   props: ['provider', 'disabled', 'onClick'],
   methods: {
-    handleClick(this: { onClick: (id: string) => void; provider: { id: string } }) {
-      this.onClick(this.provider.id);
+    handleClick() {
+      this.onClick?.(this.provider?.id);
     }
   }
 };

180-208: Consider adding error state assertions.

The test verifies the polling behavior and message display, but could be more robust by asserting the error state during failed API calls.

     const wrapper = mount(SsoButtons, {
       global: {
         stubs: { 
           SsoProviderButton: SsoProviderButtonStub,
           Button: { template: '<button><slot /></button>' }
         },
       },
     });
     
     expect(wrapper.text()).toContain('Checking authentication options...');
     
     // Advance timers to trigger the polling
     await flushPromises();
+    // Verify the first failed API call was made
+    expect(refetchMock).toHaveBeenCalledTimes(1);
     vi.advanceTimersByTime(2000);
     await flushPromises();
+    // Verify the retry was made after delay
+    expect(refetchMock).toHaveBeenCalledTimes(2);
     
     // After successful API response, checking message should disappear
     expect(wrapper.text()).not.toContain('Checking authentication options...');
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9134ffc and 2d949c7.

📒 Files selected for processing (2)
  • web/__test__/components/SsoButton.test.ts (5 hunks)
  • web/components/queries/public-oidc-providers.query.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/components/queries/public-oidc-providers.query.ts
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • web/__test__/components/SsoButton.test.ts
web/**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

All web GraphQL code must follow the .query.ts/.mutation.ts filename conventions (do not place operations in other file types)

Files:

  • web/__test__/components/SsoButton.test.ts
**/*.{test,spec}.{js,ts,jsx,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

**/*.{test,spec}.{js,ts,jsx,tsx}: General testing: use .rejects.toThrow() without arguments when asserting thrown errors
General testing: focus on behavior, not exact error message wording
General testing: avoid brittle tests tied to non-essential details (exact messages, log formats)
General testing: use mocks as nouns (objects), not verbs (behavioral overuse)

Files:

  • web/__test__/components/SsoButton.test.ts
web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx}: Web (Vue) component tests should use mount from Vue Test Utils
Web (Vue) component tests should stub complex child components not under test
Web (Vue) component tests should mock external dependencies and services
Web (Vue) component tests should query elements semantically (e.g., find('button')) rather than using data-test IDs
Web (Vue) component tests should use createTestingPinia() for mocking stores in components
Web (Vue) component tests should use await nextTick() for DOM updates and await all async operations before assertions

Files:

  • web/__test__/components/SsoButton.test.ts
web/**/*.{test,spec}.{ts,js,tsx,jsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

web/**/*.{test,spec}.{ts,js,tsx,jsx}: Store testing with Pinia: use createPinia() and setActivePinia; only use createTestingPinia when you need its features
Store testing with Pinia: let stores initialize with natural defaults; do not mock the store under test
Store testing with Pinia: place all mock declarations at top level; use factory functions for module mocks; clear mocks between tests

Files:

  • web/__test__/components/SsoButton.test.ts
web/__test__/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Tests for the web app are located under web/test

Files:

  • web/__test__/components/SsoButton.test.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • web/__test__/components/SsoButton.test.ts
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}

📄 CodeRabbit Inference Engine (.cursor/rules/web-testing-rules.mdc)

{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Files:

  • web/__test__/components/SsoButton.test.ts
**/__test__/components/**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/web-testing-rules.mdc)

**/__test__/components/**/*.ts: Component tests should use mount from @vue/test-utils
Stub complex child components that are not the focus of a component test
Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Use createTestingPinia() to mock Pinia stores when testing Vue components
Test that expected elements are rendered and verify component output rather than internals
Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary
Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Use findComponent(ComponentName) for locating child components and findAll for collections

Files:

  • web/__test__/components/SsoButton.test.ts
🧠 Learnings (18)
📚 Learning: 2025-08-11T15:06:30.877Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T15:06:30.877Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx} : Web (Vue) component tests should mock external dependencies and services

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Component tests should use mount from vue/test-utils

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:06:30.877Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T15:06:30.877Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx} : Web (Vue) component tests should stub complex child components not under test

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Use createTestingPinia() to mock Pinia stores when testing Vue components

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:06:30.877Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T15:06:30.877Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx} : Web (Vue) component tests should use mount from Vue Test Utils

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Stub complex child components that are not the focus of a component test

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:06:30.877Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T15:06:30.877Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx} : Web (Vue) component tests should use createTestingPinia() for mocking stores in components

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:06:30.877Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T15:06:30.877Z
Learning: Applies to web/**/*.{test,spec}.{ts,js,tsx,jsx} : Store testing with Pinia: place all mock declarations at top level; use factory functions for module mocks; clear mocks between tests

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/store/**/*.ts : In store tests, mock external dependencies used by the store and verify interactions with those mocks

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use vi.spyOn() to specify return values or behavior of methods under test

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/mocks/**/*.ts : Frequently used mocks can also be placed under __test__/mocks

Applied to files:

  • web/__test__/components/SsoButton.test.ts
🔇 Additional comments (5)
web/__test__/components/SsoButton.test.ts (5)

123-155: LGTM! Well-structured test for single provider rendering.

The test properly uses fake timers, waits for async operations, and verifies both the "or" separator text and the provider button text.


253-289: LGTM! Comprehensive token callback handling test.

The test thoroughly validates token-based authentication flow including form manipulation, auto-submission, and URL cleanup.


291-331: LGTM! Proper error handling verification.

The test correctly verifies error message display, form visibility preservation, and URL cleanup when authentication fails.


333-367: LGTM! Well-tested OAuth redirect flow.

The test properly validates the redirect behavior when authorization code and state parameters are present.


369-411: LGTM! Good coverage of multiple provider scenarios.

The test effectively validates rendering and interaction with multiple OIDC providers, including proper button count and text verification.

Copy link
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: 13

🔭 Outside diff range comments (1)
web/components/RClone/RCloneConfig.vue (1)

54-55: Fix timer types for browser and clear timeouts to prevent leaks

Using NodeJS.Timeout in browser builds is brittle (setTimeout returns number). Also, pending timers should be cleared on unmount.

Apply this diff:

-let refetchTimeout: NodeJS.Timeout | null = null;
+let refetchTimeout: ReturnType<typeof setTimeout> | null = null;
@@
-let changeTimeout: NodeJS.Timeout | null = null;
+let changeTimeout: ReturnType<typeof setTimeout> | null = null;

And add unmount cleanup:

-import { computed, provide, ref, watch } from 'vue';
+import { computed, provide, ref, watch, onBeforeUnmount } from 'vue';
@@
 );
 
+onBeforeUnmount(() => {
+  if (refetchTimeout) clearTimeout(refetchTimeout);
+  if (changeTimeout) clearTimeout(changeTimeout);
+});

Also applies to: 160-171

♻️ Duplicate comments (15)
api/docs/public/oidc-provider-examples.md (4)

455-461: Add language to fenced block (MD040) and include production HTTPS guidance

Also, present both local and production redirect URIs here for consistency and visibility.

Apply this diff:

-```
-http://YOUR_UNRAID_IP:3001/graphql/api/auth/oidc/callback
-```
-
-Replace `YOUR_UNRAID_IP` with your actual server IP address.
+```text
+# Local development (HTTP):
+http://YOUR_UNRAID_IP:3001/graphql/api/auth/oidc/callback
+
+# Production (HTTPS):
+https://YOUR_DOMAIN_OR_IP/graphql/api/auth/oidc/callback
+```
+
+> ⚠️ Security Warning: Always use HTTPS in production to prevent token interception. HTTP should only be used for local development.
+
+Replace `YOUR_UNRAID_IP` or `YOUR_DOMAIN_OR_IP` with your actual server address.

108-111: Emphasize HTTPS for production in Google OAuth setup

Add a short warning right where the redirect URI is introduced.

Apply this diff:

-    - Authorized JavaScript origins: `http://YOUR_UNRAID_IP:3001`
-    - Authorized redirect URIs: `http://YOUR_UNRAID_IP:3001/graphql/api/auth/oidc/callback`
+    - Authorized JavaScript origins: `http://YOUR_UNRAID_IP:3001`
+    - Authorized redirect URIs:
+      - Local: `http://YOUR_UNRAID_IP:3001/graphql/api/auth/oidc/callback`
+      - Production: `https://YOUR_DOMAIN_OR_IP/graphql/api/auth/oidc/callback`
+      > Use HTTPS for production deployments.

238-239: Azure AD: show both local and production redirect URIs

Aligns with earlier guidance and reduces misconfiguration.

Apply this diff:

-    - Redirect URI: `http://YOUR_UNRAID_IP:3001/graphql/api/auth/oidc/callback`
+    - Redirect URI:
+      - Local: `http://YOUR_UNRAID_IP:3001/graphql/api/auth/oidc/callback`
+      - Production: `https://YOUR_DOMAIN_OR_IP/graphql/api/auth/oidc/callback`

276-277: Keycloak: include HTTPS production example for Valid Redirect URIs

Keeps security guidance consistent across providers.

Apply this diff:

-    - Valid Redirect URIs: `http://YOUR_UNRAID_IP:3001/graphql/api/auth/oidc/callback`
+    - Valid Redirect URIs:
+      - Local: `http://YOUR_UNRAID_IP:3001/graphql/api/auth/oidc/callback`
+      - Production: `https://YOUR_DOMAIN_OR_IP/graphql/api/auth/oidc/callback`
api/src/unraid-api/graph/resolvers/sso/oidc-provider.model.ts (2)

39-71: Deduplicate OidcAuthorizationRule and OidcAuthorizationRuleInput definitions

The rule ObjectType and InputType share identical fields. Extract shared fields into a base class to reduce duplication and maintenance.

As previously suggested:

+abstract class OidcAuthorizationRuleBase {
+  @Field(() => String, { description: 'The claim to check (e.g., email, sub, groups, hd)' })
+  @IsString()
+  @IsNotEmpty()
+  claim!: string;
+  @Field(() => AuthorizationOperator, { description: 'The comparison operator' })
+  @IsEnum(AuthorizationOperator)
+  operator!: AuthorizationOperator;
+  @Field(() => [String], { description: 'The value(s) to match against' })
+  @IsArray()
+  @IsString({ each: true })
+  value!: string[];
+}
 @ObjectType()
-export class OidcAuthorizationRule {
-  // duplicated fields...
-}
+export class OidcAuthorizationRule extends OidcAuthorizationRuleBase {}
 @InputType()
-export class OidcAuthorizationRuleInput {
-  // duplicated fields...
-}
+export class OidcAuthorizationRuleInput extends OidcAuthorizationRuleBase {}

73-195: Deduplicate OidcProvider and OidcProviderInput field definitions

Both classes repeat nearly all fields and decorators, violating DRY and increasing drift risk. Extract common fields into a base, and override differences (e.g., id scalar vs string, rule input vs object) as needed.

Pattern:

  • OidcProviderBase with shared fields (name, clientId, clientSecret, issuer, endpoints, scopes, rules, ruleMode, button* fields, customAuthParams).
  • OidcProvider extends base and overrides id with PrefixedID.
  • OidcProviderInput extends base and overrides id as string; rules typed with OidcAuthorizationRuleInput.

If decorators differ, use protected members + getters with appropriate @field or split minimal overrides.

Also applies to: 197-319

api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts (3)

236-266: Fix base64url decoding and redact PII when logging ID token claims

Manual Buffer(..., 'base64') fails for base64url tokens and current logs include sensitive claims. Decode using base64url-safe normalization and log only claim keys.

-            // Parse ID token to get user info
+            // Parse ID token to get user info
             let claims: JwtClaims | null = null;
             if (tokens.id_token) {
                 try {
-                    // Decode the JWT manually
-                    const payload = tokens.id_token.split('.')[1];
-                    claims = JSON.parse(Buffer.from(payload, 'base64').toString());
-                    this.logger.debug(`ID token claims: ${JSON.stringify(claims)}`);
-
-                    // Log all claim types for debugging
-                    if (claims) {
-                        for (const [key, value] of Object.entries(claims)) {
-                            const valueType = Array.isArray(value) ? 'array' : typeof value;
-                            this.logger.debug(
-                                `Claim '${key}': type=${valueType}, value=${JSON.stringify(value)}`
-                            );
-
-                            // Check for unexpected claim types early
-                            if (valueType === 'object' && value !== null && !Array.isArray(value)) {
-                                this.logger.error(
-                                    `unexpected JWT claim value encountered - claim '${key}' is complex object: ${JSON.stringify(value)}`
-                                );
-                            }
-                        }
-                    }
+                    // Decode the JWT payload (base64url-safe)
+                    const payload = tokens.id_token.split('.')[1]!;
+                    const normalized = payload
+                        .replace(/-/g, '+')
+                        .replace(/_/g, '/')
+                        .padEnd(Math.ceil(payload.length / 4) * 4, '=');
+                    claims = JSON.parse(Buffer.from(normalized, 'base64').toString('utf8'));
+                    // Avoid logging PII; log only claim keys
+                    this.logger.debug(
+                        `ID token parsed; available claim keys: ${
+                            claims ? Object.keys(claims).join(', ') : 'none'
+                        }`
+                    );
                 } catch (e) {
                     this.logger.warn(`Failed to parse ID token: ${e}`);
                 }
             } else {
                 this.logger.error('No ID token received from provider');
             }

541-551: Make redirect URI configurable; drop hardcoded localhost and base URL logic

Redirect handling should be driven by configuration to avoid fragile env detection and hardcoded ports/hosts.

     private getRedirectUri(requestHost?: string): string {
-        // Always use the proxied path through /graphql to match production
-        if (requestHost && requestHost.includes('localhost')) {
-            // In development, use the Nuxt proxy at port 3000
-            return `http://localhost:3000/graphql/api/auth/oidc/callback`;
-        }
-
-        // In production, use the configured base URL with /graphql prefix
-        const baseUrl = this.configService.get('BASE_URL', 'http://tower.local');
-        return `${baseUrl}/graphql/api/auth/oidc/callback`;
+        // Prefer explicit callback URL if provided
+        const configured = this.configService.get<string>('OIDC_CALLBACK_URL');
+        if (configured) return configured;
+        // Fallback to BASE_URL + fixed path
+        const baseUrl = this.configService.get<string>('BASE_URL', 'http://tower.local');
+        return `${baseUrl.replace(/\/$/, '')}/graphql/api/auth/oidc/callback`;
     }

473-525: Array claims are incorrectly handled (and array check is unreachable)

The object check runs before Array.isArray, so the array branch is never hit. Also, array-valued claims (e.g., groups) should be supported, not rejected.

     private evaluateRule(rule: OidcAuthorizationRule, claims: JwtClaims): boolean {
         const claimValue = claims[rule.claim];

         this.logger.debug(
             `Evaluating rule for claim ${rule.claim}: ${JSON.stringify({
                 claimValue,
                 claimType: typeof claimValue,
                 ruleOperator: rule.operator,
                 ruleValues: rule.value,
             })}`
         );

         if (claimValue === undefined || claimValue === null) {
             this.logger.debug(`Claim ${rule.claim} not found in token`);
             return false;
         }

-        // Log detailed claim analysis
-        if (typeof claimValue === 'object' && claimValue !== null) {
-            this.logger.warn(
-                `unexpected JWT claim value encountered - claim ${rule.claim} is object type: ${JSON.stringify(claimValue)}`
-            );
-            return false;
-        }
-
-        if (Array.isArray(claimValue)) {
-            this.logger.warn(
-                `unexpected JWT claim value encountered - claim ${rule.claim} is array type: ${JSON.stringify(claimValue)}`
-            );
-            return false;
-        }
-
-        const value = String(claimValue);
-        this.logger.debug(`Processing claim ${rule.claim} with string value: "${value}"`);
+        // Normalize to a string array of values for uniform matching
+        const values: string[] = Array.isArray(claimValue)
+            ? (claimValue as unknown[]).map((v) => String(v))
+            : [String(claimValue)];
+        this.logger.debug(
+            `Processing claim ${rule.claim} with values: [${values.map((v) => `"${v}"`).join(', ')}]`
+        );

         let result: boolean;
         switch (rule.operator) {
             case AuthorizationOperator.EQUALS:
-                result = rule.value.some((v) => value === v);
+                result = rule.value.some((v) => values.includes(v));
                 this.logger.debug(
-                    `EQUALS check: "${value}" matches any of [${rule.value.join(', ')}]: ${result}`
+                    `EQUALS check: any of [${values.join(', ')}] matches any of [${rule.value.join(', ')}]: ${result}`
                 );
                 return result;

             case AuthorizationOperator.CONTAINS:
-                result = rule.value.some((v) => value.includes(v));
+                result = values.some((val) => rule.value.some((v) => val.includes(v)));
                 this.logger.debug(
-                    `CONTAINS check: "${value}" contains any of [${rule.value.join(', ')}]: ${result}`
+                    `CONTAINS check: any of [${values.join(', ')}] contains any of [${rule.value.join(', ')}]: ${result}`
                 );
                 return result;

             case AuthorizationOperator.STARTS_WITH:
-                result = rule.value.some((v) => value.startsWith(v));
+                result = values.some((val) => rule.value.some((v) => val.startsWith(v)));
                 this.logger.debug(
-                    `STARTS_WITH check: "${value}" starts with any of [${rule.value.join(', ')}]: ${result}`
+                    `STARTS_WITH check: any of [${values.join(', ')}] starts with any of [${rule.value.join(', ')}]: ${result}`
                 );
                 return result;

             case AuthorizationOperator.ENDS_WITH:
-                result = rule.value.some((v) => value.endsWith(v));
+                result = values.some((val) => rule.value.some((v) => val.endsWith(v)));
                 this.logger.debug(
-                    `ENDS_WITH check: "${value}" ends with any of [${rule.value.join(', ')}]: ${result}`
+                    `ENDS_WITH check: any of [${values.join(', ')}] ends with any of [${rule.value.join(', ')}]: ${result}`
                 );
                 return result;

             default:
                 this.logger.error(`Unknown authorization operator: ${rule.operator}`);
                 return false;
         }
     }
api/generated-schema.graphql (4)

2064-2066: Make id optional in OidcProviderInput to support create semantics

Clients shouldn’t be forced to generate IDs. Optional id implies create; present id implies update.

 input OidcProviderInput {
   """The unique identifier for the OIDC provider"""
-  id: String!
+  id: String

1466-1530: Do not expose clientSecret and implement Node on OidcProvider

Exposing clientSecret leaks sensitive credentials. Also, to align with schema consistency, OidcProvider should implement Node.

-type OidcProvider {
+type OidcProvider implements Node {
   """The unique identifier for the OIDC provider"""
   id: PrefixedID!
@@
-  """OAuth2 client secret (if required by provider)"""
-  clientSecret: String
+  # clientSecret intentionally omitted from API output for security

1958-1969: Protect admin/internal OIDC queries with authorization directives

Add @usePermissions to admin and internal queries to prevent unauthorized enumeration and probing. Leave public providers open.

 """Get public OIDC provider information for login buttons"""
 publicOidcProviders: [PublicOidcProvider!]!
 
 """Get all configured OIDC providers (admin only)"""
-oidcProviders: [OidcProvider!]!
+oidcProviders: [OidcProvider!]! @usePermissions(action: READ, resource: "SSO", possession: ANY)
 
 """Get a specific OIDC provider by ID"""
-oidcProvider(id: String!): OidcProvider
+oidcProvider(id: PrefixedID!): OidcProvider @usePermissions(action: READ, resource: "SSO", possession: ANY)
 
 """Validate an OIDC session token (internal use for CLI validation)"""
-validateOidcSession(token: String!): OidcSessionValidation!
+validateOidcSession(token: String!): OidcSessionValidation! @usePermissions(action: READ, resource: "SSO", possession: OWN)

2016-2020: Protect mutations and use consistent ID type

Restrict provider mutations to authorized users and use PrefixedID consistently.

 """Create or update an OIDC provider"""
-upsertOidcProvider(provider: OidcProviderInput!): OidcProvider!
+upsertOidcProvider(provider: OidcProviderInput!): OidcProvider! @usePermissions(action: UPDATE, resource: "SSO", possession: ANY)
 
 """Delete an OIDC provider"""
-deleteOidcProvider(id: String!): Boolean!
+deleteOidcProvider(id: PrefixedID!): Boolean! @usePermissions(action: DELETE, resource: "SSO", possession: ANY)
web/composables/gql/graphql.ts (2)

1208-1241: Remove clientSecret from OidcProvider output type to prevent credential leaks

Exposing clientSecret in an output type is a hard no. It will be visible via introspection and can be queried accidentally or maliciously. Restrict it to input-only, or return a masked/null value through a dedicated admin-only resolver type.

Recommended actions:

  • Schema: Remove clientSecret from OidcProvider (output) and, if absolutely necessary, create a separate admin-only payload (e.g., OidcProviderSecretView) used only in mutation responses and always masked.
  • Resolver: If you cannot remove immediately, override the field resolver to always return "***REDACTED***" or null.
  • Frontend: Keep excluding clientSecret in all queries (current OidcProvidersDocument does this correctly).

Apply this minimal stopgap diff to avoid exposing secrets in the generated surface (note: update the backend schema; this file will regenerate accordingly):

 export type OidcProvider = {
   __typename?: 'OidcProvider';
@@
   /** OAuth2 client ID registered with the provider */
   clientId: Scalars['String']['output'];
-  /** OAuth2 client secret (if required by provider) */
-  clientSecret?: Maybe<Scalars['String']['output']>;
   /** Use custom parameter names for authorization (e.g., callbackUrl instead of redirect_uri) */
   customAuthParams?: Maybe<Scalars['Boolean']['output']>;

If you want, I can draft a schema/resolver refactor to implement this safely end-to-end. Do you want me to open a tracking issue?


1413-1421: Sanitize or constrain buttonStyle to prevent CSS injection

buttonStyle accepts arbitrary CSS and is exposed in the public type. Without strict server-side sanitization or a whitelist, this is susceptible to style injection/abuse.

Recommended:

  • Backend: Sanitize buttonStyle on write or read; alternatively replace with a safe, explicit enum/JSON of allowed tokens (e.g., colors, gradients, radius).
  • Frontend: Avoid binding raw buttonStyle directly to style until sanitization is guaranteed.

Run this to verify whether any sanitization exists server-side:

#!/bin/bash
# Search for buttonStyle handling and sanitization
rg -n -A3 -B3 'buttonStyle' api/ | sed -n '1,200p'
rg -n -A3 -B3 'sanitize.*buttonStyle|buttonStyle.*sanitize' api/ || true
🧹 Nitpick comments (25)
api/docs/public/images/README.md (4)

42-46: Fix bare URL (MD034) and clarify path formatting

Wrap the URL in angle brackets and avoid leading slash in the path reference to keep paths consistently repo-root relative and satisfy markdownlint.

Apply this diff:

-1. Navigate to the OIDC configuration page at http://localhost:3000
+1. Navigate to the OIDC configuration page at <http://localhost:3000>
@@
-4. Place them in this directory (/api/docs/public/images/)
+4. Place them in this directory (`api/docs/public/images/`)

30-35: Consistently format operator names as code

Operators read better and avoid ambiguity when wrapped in backticks.

Apply this diff:

-   - Displays multiple rules with different operators (equals, contains, endsWith, startsWith)
+   - Displays multiple rules with different operators (`equals`, `contains`, `endsWith`, `startsWith`)
@@
-- **Rule operators**: Demonstrate equals, contains, endsWith, startsWith
+- **Rule operators**: Demonstrate `equals`, `contains`, `endsWith`, `startsWith`

Also applies to: 49-56


5-17: Tighten phrasing and expectations for screenshots

Minor editorial improvements to reduce ambiguity: specify that “Simple authorization mode” is selected, and prefer “tabs visible” to ensure the correct UI state is captured.

Apply this diff:

-   - Simple authorization mode selected
+   - Simple authorization mode selected (visible in the Authorization Mode dropdown)
@@
-   - Unraid.net provider tab selected
+   - Unraid.net provider tab selected (tab visible/active)

62-69: Add recommended image resolution guidance

High-DPI captures help readability in dark theme docs across devices.

Would you like me to add a short “Recommended resolution” note (e.g., capture at 2x, minimum width 1440px) here?

api/docs/public/oidc-provider-examples.md (7)

10-16: Fix nested list indentation (MD007)

The nested entries under “Provider Configuration Examples” should be indented by two spaces to satisfy markdownlint.

Apply this diff:

-- [Provider Configuration Examples](#provider-configuration-examples)
-    - [Unraid.net Provider](#unraidnet-provider)
-    - [Google](#google)
-    - [Authelia](#authelia)
-    - [Microsoft/Azure AD](#microsoftazure-ad)
-    - [Keycloak](#keycloak)
-    - [Authentik](#authentik)
-    - [Okta](#okta)
+- [Provider Configuration Examples](#provider-configuration-examples)
+  - [Unraid.net Provider](#unraidnet-provider)
+  - [Google](#google)
+  - [Authelia](#authelia)
+  - [Microsoft/Azure AD](#microsoftazure-ad)
+  - [Keycloak](#keycloak)
+  - [Authentik](#authentik)
+  - [Okta](#okta)

141-144: Avoid hotlinking external icons

Linking to https://www.google.com/favicon.ico can break or violate terms. Prefer bundling the icon or referencing a locally hosted asset.

Would you like me to add a note pointing to a locally stored icon under api/docs/public/images/ and update the example accordingly?


149-156: Clarify Google Workspace hd claim limitations

The hd claim is only present/reliable when the account is part of a Google Workspace domain and the app/org policies allow it. Add a brief caveat.

Proposed addition after the rule:

+Note: The `hd` claim is set for Google Workspace accounts. It may be absent for consumer accounts or if not included per your org’s policies. Verify tokens in your environment before enforcing this rule.

Would you like me to apply this note?


431-435: Fix sub-list indentation (MD007) under Simple/Advanced fields

Indentation for nested bullets under “Simple Authorization Fields” and “Advanced Authorization Fields” should be two spaces.

Apply this diff:

-    - Helper text: "Users with emails ending in these domains can login"
+  - Helper text: "Users with emails ending in these domains can login"
@@
-    - Helper text: "Only these exact email addresses can login"
+  - Helper text: "Only these exact email addresses can login"
@@
-    - **Claim**: The JWT claim to check
-    - **Operator**: How to compare (equals, contains, endsWith, startsWith)
-    - **Value**: What to match against
+  - **Claim**: The JWT claim to check
+  - **Operator**: How to compare (`equals`, `contains`, `endsWith`, `startsWith`)
+  - **Value**: What to match against

Also applies to: 443-446


449-452: Production hardening note for Developer Sandbox

If /graphql exposes an interactive sandbox, call out that it should be disabled in production.

Proposed addition:

 - **Enable Developer Sandbox**: Toggle to enable GraphQL sandbox at `/graphql`
+  - For security, disable the sandbox in production environments.

198-203: Secrets handling: clarify hashed vs unhashed and rotation

Good call noting “unhashed secret” for the Unraid config. Add a brief best-practice note about storing secrets securely and rotating regularly.

Proposed addition:

 - **Client Secret**: Your unhashed secret
+  - Store and handle this secret securely. Rotate periodically and avoid committing it to source control.

517-518: Suggest adding a failure-handling hint to the testing flow

If the SSO button doesn’t appear or login fails, reference Troubleshooting to close the loop.

Proposed addition:

 5. Click to test the login flow
+6. If the button is missing or login fails, see the Troubleshooting section above.
api/src/unraid-api/graph/utils/form-utils.ts (3)

24-35: Minor cleanups: use property shorthand and avoid emitting undefined options

  • Prefer property shorthand for scope.
  • Only include options when values are provided to avoid options: undefined/description: undefined.

Apply this diff:

             {
                 type: 'Label',
                 text: label,
-                options: {
-                    description,
-                },
+                options: description ? { description } : undefined,
             } as LabelElement,
             {
                 type: 'Control',
-                scope: scope,
-                options: controlOptions,
+                scope,
+                ...(controlOptions ? { options: controlOptions } : {}),
             } as ControlElement,

39-39: Remove obvious comment per guidelines

The “Add rule if provided” comment states the obvious and can be removed to align with the repo guideline on avoiding unnecessary comments.

Apply this diff:

-    // Add rule if provided

101-110: Broaden elements typing for createAccordionLayout to increase flexibility

Limiting accordion items to Layout prevents using non-layout UI schema elements (e.g., a single Control). If your renderer supports it, consider widening to UISchemaElement to make this helper more broadly applicable.

Apply this diff:

-    elements: Array<
-        Layout & {
-            options?: Layout['options'] & {
+    elements: Array<
+        UISchemaElement & {
+            options?: UISchemaElement['options'] & {
                 accordion?: {
                     title?: string;
                     description?: string;
                 };
             };
         }
     >;

Additionally, add UISchemaElement to the import (outside the selected range):

// at line 1
import type { ControlElement, LabelElement, Layout, Rule, UISchemaElement } from '@jsonforms/core';
unraid-ui/src/forms/config.ts (2)

20-26: AJV options: consider removing unsupported options and enabling formats

createAjv already configures Ajv for JSON Forms. The verbose option is no longer supported in Ajv v8; keep allErrors and strict, and ensure formats are enabled. JSON Forms’ createAjv typically adds ajv-formats, so no extra work needed if you rely on that. If you do need custom formats/keywords, expose a hook here.

Apply this diff to drop verbose and clarify intent:

 export function createJsonFormsAjv(): JsonFormsAjv {
   return createAjv({
     allErrors: true,
-    verbose: true,
-    strict: false,
+    // Use strict mode for better diagnostics; switch to 'false' only if necessary
+    strict: true,
   });
 }

32-41: Freeze and re-use the shared instances to avoid accidental mutation

Given these are global singletons used across many forms, freezing helps prevent accidental runtime mutation of shared config.

Apply this diff:

-export const jsonFormsAjv: JsonFormsAjv = createJsonFormsAjv();
+export const jsonFormsAjv: JsonFormsAjv = createJsonFormsAjv();

 export const defaultJsonFormsConfig: JsonFormsConfig = {
   restrict: false,
   trim: false,
   ajv: jsonFormsAjv,
 };
+
+Object.freeze(jsonFormsAjv);
+Object.freeze(defaultJsonFormsConfig);
web/components/RClone/RCloneConfig.vue (1)

75-81: Guard noisy console logs behind dev checks

Unscoped console logging can clutter production logs. Wrap logs with a dev guard or remove.

Apply this diff:

-        console.log('[RCloneConfig] providerType changed:', newValue.type);
+        if (import.meta.env?.DEV) {
+          console.log('[RCloneConfig] providerType changed:', newValue.type);
+        }
@@
-        console.log('[RCloneConfig] Refetching form schema');
+        if (import.meta.env?.DEV) {
+          console.log('[RCloneConfig] Refetching form schema');
+        }
@@
-    console.log('[RCloneConfig] onChange received data:', JSON.stringify(data));
+    if (import.meta.env?.DEV) {
+      console.log('[RCloneConfig] onChange received data:', JSON.stringify(data));
+    }

Also applies to: 169-170

web/components/ConnectSettings/ConnectSettings.ce.vue (2)

83-86: Reuse centralized default JSON Forms config to avoid drift

You already share Ajv; do the same for JSON Forms config to keep behavior consistent across forms.

Apply this diff:

-import { BrandButton, jsonFormsRenderers, jsonFormsAjv, Label, SettingsGrid } from '@unraid/ui';
+import { BrandButton, jsonFormsRenderers, jsonFormsAjv, defaultJsonFormsConfig, Label, SettingsGrid } from '@unraid/ui';
@@
-const jsonFormsConfig = {
-  restrict: false,
-  trim: false,
-};
+const jsonFormsConfig = defaultJsonFormsConfig;

Ajv binding looks correct:

<JsonForms ... :ajv="jsonFormsAjv" />

Also applies to: 11-13, 118-129


91-96: Optional: remove or guard console logs

Leaving console logs in a CE might propagate to host pages. Guard or remove to reduce noise.

-  console.log('[ConnectSettings] trying to update settings to', formState.value);
+  if (import.meta.env?.DEV) {
+    console.log('[ConnectSettings] trying to update settings to', formState.value);
+  }
unraid-ui/src/forms/AccordionLayout.vue (1)

60-84: Use the host-provided Ajv if available, fallback to shared ajv

Some forms may pass a custom Ajv via JsonForms config. Respect it before falling back to the shared instance.

Apply this diff:

-import { jsonFormsAjv } from '@/forms/config';
+import { jsonFormsAjv } from '@/forms/config';
@@
-  // Filter elements based on visibility rules
+  // Filter elements based on visibility rules
   return allElements.filter((element: UISchemaElement & Record<string, unknown>) => {
@@
-    try {
+    try {
       // Get the root data from JSONForms context for rule evaluation
       const rootData = jsonFormsContext?.core?.data || {};
       const formData = props.data || layout.data || rootData;
       const formPath = props.path || layout.path || '';
-
-      const visible = isVisible(element as UISchemaElement, formData, formPath, jsonFormsAjv);
+      // Prefer ajv from layout/config if provided, else use shared instance
+      const ajvFromConfig = (layout as unknown as { config?: { ajv?: unknown } })?.config?.ajv;
+      const ajvToUse = (ajvFromConfig as unknown) || jsonFormsAjv;
+      const visible = isVisible(element as UISchemaElement, formData, formPath, ajvToUse as any);
       return visible;
api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.test.ts (1)

61-66: Testing private methods via casting is brittle

These unit tests reach into private methods via (service as any). Prefer covering behavior through the public method (checkAuthorization) to avoid white-box coupling.

unraid-ui/src/forms/renderers.ts (1)

58-73: Broaden string schema detection to cover common optional patterns

Current anyOf detection only matches minLength=1 and maxLength=0. Optional string patterns often use const '' or nullable. Consider supporting those to increase renderer applicability.

 const isStringOrAnyOfString = (schema: JsonSchema): boolean => {
   if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return false;
   // Exclude enum fields - they should use select renderer
   if (schema.enum) return false;
   // Handle direct string type (but not enums)
   if (schema.type === 'string') return true;
-  // Handle anyOf with all string types (for optional URL fields)
-  if (schema.anyOf && Array.isArray(schema.anyOf) && schema.anyOf.length === 2) {
-    // Check if it's the pattern we expect: [{type: 'string', minLength: 1}, {type: 'string', maxLength: 0}]
-    const hasMinLength = schema.anyOf.some((s: JsonSchema) => s.type === 'string' && s.minLength === 1);
-    const hasMaxLength = schema.anyOf.some((s: JsonSchema) => s.type === 'string' && s.maxLength === 0);
-    return hasMinLength && hasMaxLength;
-  }
+  // Handle common optional string patterns via anyOf/oneOf:
+  const list = (schema.anyOf ?? schema.oneOf) as JsonSchema[] | undefined;
+  if (Array.isArray(list) && list.length >= 2) {
+    const hasString = list.some((s) => s && typeof s === 'object' && !Array.isArray(s) && s.type === 'string');
+    const hasEmptyConst = list.some((s: any) => s && typeof s === 'object' && s.const === '');
+    const hasEmptyMaxLen = list.some((s: any) => s && typeof s === 'object' && s.type === 'string' && s.maxLength === 0);
+    return hasString && (hasEmptyConst || hasEmptyMaxLen);
+  }
   return false;
 };
web/composables/gql/graphql.ts (3)

1460-1471: Avoid duplicative access paths for OIDC providers (Query.oidcProviders vs Settings.sso.oidcProviders)

Two surfaces expose provider lists: root oidcProviders and settings.sso.oidcProviders. This invites divergent permissions/caching and client confusion.

Propose deprecating one path (likely the root field) and consolidating on a single canonical field with clear access control and caching semantics. Mark the deprecated field in schema and provide a migration note in developer docs.


1106-1109: Confirm create semantics for upsert: OidcProviderInput.id is required

upsertOidcProvider requires provider.id: string. For pure create flows, is the ID user-defined (e.g., slug)? If backend generates IDs, consider making id optional on create or document that clients must supply it.

If creation requires a user-defined key, document allowed charset/length and collision behavior in the schema description.


1251-1258: Model buttonVariant as an enum to prevent invalid values

buttonVariant?: String shifts validation to clients. Converting to a GraphQL enum aligned with Reka UI variants prevents invalid inputs and enables better tooling.

Example:

enum ButtonVariant {
  DEFAULT
  OUTLINE
  GHOST
  LINK
  # ...align with supported Reka UI variants
}

# In OidcProvider / OidcProviderInput:
buttonVariant: ButtonVariant

Regenerate types after schema change.

…mentation

- Eliminated the customAuthParams field from the OidcProvider and OidcProviderInput types in the GraphQL schema.
- Updated the OidcAuthService to use standard OAuth2 parameters, removing the handling for custom parameter names.
- Adjusted related documentation comments for clarity and consistency.
- Modified REST controller routes to streamline OIDC authorization and callback handling based on the environment.
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

Copy link
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 (10)
api/generated-schema.graphql (4)

2058-2128: Make id optional and use PrefixedID in OidcProviderInput

For upsert flows, clients shouldn’t be forced to pre-generate IDs. Use:

  • absent id → create
  • present id → update

Also align input type with the PrefixedID scalar used across the API.

 input OidcProviderInput {
   """The unique identifier for the OIDC provider"""
-  id: String!
+  id: PrefixedID
@@
   """OAuth2 client secret (if required by provider)"""
   clientSecret: String

Optional follow-up: If you expect clients to use human-readable slugs (e.g., "google"), keep String but still make it optional and document it clearly.


1466-1525: Do not expose clientSecret; and implement Node for OidcProvider

Leaking clientSecret in the public type is a security risk (easy accidental disclosure). Also, top-level resources in this API generally implement Node. Align OIDC with that convention.

Apply the following schema changes:

-type OidcProvider {
+type OidcProvider implements Node {
   """The unique identifier for the OIDC provider"""
   id: PrefixedID!
@@
-  """OAuth2 client secret (if required by provider)"""
-  clientSecret: String
+  """
+  Indicates whether a client secret has been configured.
+  The actual secret value is never returned by the API.
+  """
+  hasClientSecret: Boolean!

Follow-up:

  • Ensure resolvers omit the secret from responses and compute hasClientSecret from server-side storage.
  • If UI needs to allow rotating the secret, keep clientSecret only in the input type (as already done).

1953-1963: Add permission guards to OIDC queries and fix argument type

Admin/config queries should be protected. Also keep oidcProvider argument consistent with the PrefixedID used on the type.

   """Get public OIDC provider information for login buttons"""
   publicOidcProviders: [PublicOidcProvider!]!
 
   """Get all configured OIDC providers (admin only)"""
-  oidcProviders: [OidcProvider!]!
+  oidcProviders: [OidcProvider!]! @usePermissions(action: READ, resource: "SSO", possession: ANY)
 
   """Get a specific OIDC provider by ID"""
-  oidcProvider(id: String!): OidcProvider
+  oidcProvider(id: PrefixedID!): OidcProvider @usePermissions(action: READ, resource: "SSO", possession: ANY)
 
   """Validate an OIDC session token (internal use for CLI validation)"""
-  validateOidcSession(token: String!): OidcSessionValidation!
+  validateOidcSession(token: String!): OidcSessionValidation! @usePermissions(action: READ, resource: "SSO", possession: OWN)

Note: publicOidcProviders remains public intentionally.


2011-2015: Protect mutations with permissions and use PrefixedID for delete

Mutations must be admin-only, and ID types should be consistent.

   """Create or update an OIDC provider"""
-  upsertOidcProvider(provider: OidcProviderInput!): OidcProvider!
+  upsertOidcProvider(provider: OidcProviderInput!): OidcProvider! @usePermissions(action: UPDATE, resource: "SSO", possession: ANY)
 
   """Delete an OIDC provider"""
-  deleteOidcProvider(id: String!): Boolean!
+  deleteOidcProvider(id: PrefixedID!): Boolean! @usePermissions(action: DELETE, resource: "SSO", possession: ANY)
api/src/unraid-api/rest/rest.controller.ts (6)

68-76: Validate providerId to prevent malformed input reaching the service

Add a simple allowlist-based check for providerId (defense in depth) before invoking the service.

         try {
             if (!state) {
                 return res.status(400).send('State parameter is required');
             }
 
+            // Validate providerId format (defense in depth)
+            if (!/^[A-Za-z0-9._-]{1,64}$/.test(providerId)) {
+                return res.status(400).send('Invalid provider ID format');
+            }

78-81: Honor reverse proxy headers when deriving host for authorize

Behind proxies, req.headers.host may be wrong. Prefer X-Forwarded-Host if present (mirrors your callback handler).

-            // Get the host from the request headers
-            const host = req.headers.host || undefined;
+            // Get the host from reverse-proxy-aware headers
+            const host =
+                (req.headers['x-forwarded-host'] as string) ||
+                req.headers.host ||
+                undefined;

115-118: Ensure state is server-generated and cryptographically validated

Controller accepts client-supplied state and passes it through. Please verify that OidcAuthService signs or stores/validates state robustly (prevents CSRF/state-fixation).

#!/bin/bash
# Inspect OidcAuthService for secure state generation and verification
fd -a 'oidc-auth.service.ts' | while read -r f; do
  echo "----- $f -----"
  rg -n -C3 $'class\\s+OidcAuthService\\b|getAuthorizationUrl\\(|handleCallback\\(|extractProviderFromState\\(|state|nonce|verify|sign|hmac|jose|jwt|pkce|code_verifier' "$f"
done

Also applies to: 121-121


139-145: Prevent caching of the redirect that carries the token (even in fragment)

Add no-store to avoid intermediaries caching the redirect.

             // Manually set redirect headers for better proxy compatibility
             res.status(302);
+            res.header('Cache-Control', 'no-store');
             res.header('Location', loginUrl);
             return res.send();

82-82: Do not log full authorization URL (contains sensitive query like state/nonce)

Logging the full URL can leak state/nonce/client_id to logs. Log only safe context.

-            this.logger.log(`Redirecting to OIDC provider: ${authUrl}`);
+            this.logger.log(`Redirecting to OIDC provider ${providerId}`);

123-129: Avoid logging the full callback URL (contains code and state)

Log only a sanitized path to prevent leaking authorization code and state.

-            this.logger.debug(`Full callback URL from request: ${fullUrl}`);
+            const { pathname } = new URL(fullUrl);
+            this.logger.debug(`OIDC callback received for provider ${providerId} at path: ${pathname}`);
🧹 Nitpick comments (4)
api/generated-schema.graphql (3)

1447-1465: Authorization rules DSL is clear and minimal

The rule and operator modeling is straightforward and flexible.

If you have a canonical set of supported claim names (e.g., email, sub, groups, hd), consider documenting them in the description to reduce guesswork for clients.


1535-1538: Consider adding an error reason for invalid sessions

Optional: add an error code/message to aid debugging (e.g., expired, signature_invalid, audience_mismatch). Keeps UI/CLI from guessing.

If you want, I can propose a minimal reason: String or code: String addition with a short list of canonical values.


1540-1547: Public provider surface is minimal and safe

Only UI-facing fields are exposed, which is good. Consider clarifying the semantics of id (slug vs. internal id) in the description.

api/src/unraid-api/rest/rest.controller.ts (1)

159-163: Also add no-store to error redirect

Prevents caching of error redirect pages that include error context in the fragment or query.

             res.status(302);
+            res.header('Cache-Control', 'no-store');
             res.header('Location', loginUrl);
             return res.send();
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 251676f and b331c81.

⛔ Files ignored due to path filters (1)
  • api/src/unraid-api/cli/generated/graphql.ts is excluded by !**/generated/**
📒 Files selected for processing (5)
  • api/dev/configs/oidc.json (1 hunks)
  • api/generated-schema.graphql (6 hunks)
  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/oidc-provider.model.ts (1 hunks)
  • api/src/unraid-api/rest/rest.controller.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • api/src/unraid-api/graph/resolvers/sso/oidc-auth.service.ts
  • api/dev/configs/oidc.json
  • api/src/unraid-api/graph/resolvers/sso/oidc-provider.model.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • api/src/unraid-api/rest/rest.controller.ts
api/src/unraid-api/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Place new API source files in api/src/unraid-api/ rather than legacy locations

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/rest/rest.controller.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • api/src/unraid-api/rest/rest.controller.ts
🧠 Learnings (1)
📚 Learning: 2024-11-15T16:22:12.460Z
Learnt from: elibosley
PR: unraid/api#942
File: api/src/unraid-api/auth/auth.module.ts:94-96
Timestamp: 2024-11-15T16:22:12.460Z
Learning: In `CookieService.defaultOpts()` in `api/src/unraid-api/auth/cookie.service.ts`, use `sameSite: 'lax'` for session cookie configuration.

Applied to files:

  • api/src/unraid-api/rest/rest.controller.ts
🧬 Code Graph Analysis (1)
api/src/unraid-api/rest/rest.controller.ts (2)
api/src/unraid-api/auth/public.decorator.ts (1)
  • Public (4-4)
api/src/unraid-api/types/fastify.ts (2)
  • FastifyRequest (25-30)
  • FastifyReply (33-33)
🔇 Additional comments (8)
api/generated-schema.graphql (4)

1403-1408: SSO settings type looks good and follows Node convention

Sensible separation of SSO config under Settings; implements Node keeps it consistent with the rest of the schema.


1430-1431: Good addition: warnings channel in update response

Non-blocking field for config validation warnings is useful and won’t break existing clients.


1440-1442: Settings.sso exposure is reasonable

Surface looks correct; assuming the query-level auth for settings is already enforced server-side.

If settings requires admin privileges elsewhere, consider documenting that in the schema description for the settings query to avoid misuse by clients. Do you want a PR to add a short note?


1527-1534: Rule mode enum is appropriate

Naming and semantics are clear.

api/src/unraid-api/rest/rest.controller.ts (4)

1-1: ESM import specifiers with .js extensions look good

Imports comply with our ESM guideline. No action needed.

Also applies to: 8-8


14-17: Constructor injection of OidcAuthService is correct and consistent

Dependency is injected cleanly; visibility and readonly usage look good.


89-99: Good: generic error response for authorize

Error path returns a generic message and avoids leaking details. Nice.


147-157: Good: sanitized error handling in callback

Only generic messages are exposed; detailed info is logged at debug level. This addresses prior concerns about leaking internals.

- Changed the id field type for the oidcProvider query and related mutations from String to PrefixedID for better type safety.
- Removed the OidcProviderInput and OidcAuthorizationRuleInput types from the schema, streamlining the OIDC provider management.
- Adjusted the OIDC provider resolver to reflect the updated schema and removed unnecessary mutation methods for upserting and deleting OIDC providers.
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

- Eliminated the delete and upsert mutations for OIDC providers from the GraphQL schema to simplify the API.
- Removed associated input types and comments to streamline the OIDC provider management.
- Updated the SSO resolver to include permission checks for validating OIDC sessions.
- Added a warning to the OIDC provider setup documentation emphasizing the importance of using HTTPS for production redirect URIs to ensure secure communication and prevent credential interception.
- Eliminated the customAuthParams field from the OIDC provider configuration in the OidcConfigPersistence class to streamline the provider setup.
- Updated related logic to ensure compatibility with the revised configuration structure.
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

Copy link
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 (3)
api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts (1)

96-101: Add authorization to validateOidcSession (security)

This endpoint is described as “internal use,” but it currently lacks explicit access control. If exposed, it could be abused to probe token validity. Protect it with permissions (preferred) or mark it @public only if you’ve explicitly designed for unauthenticated CLI usage and considered rate-limiting and token characteristics (short TTL, one-time use).

Recommended fix:

     @Query(() => OidcSessionValidation, {
         description: 'Validate an OIDC session token (internal use for CLI validation)',
     })
-    public async validateOidcSession(@Args('token') token: string): Promise<OidcSessionValidation> {
+    @UsePermissions({
+        action: AuthActionVerb.READ,
+        resource: 'sso',
+        possession: AuthPossession.OWN,
+    })
+    public async validateOidcSession(@Args('token') token: string): Promise<OidcSessionValidation> {
         return await this.oidcSessionService.validateSession(token);
     }
api/generated-schema.graphql (2)

1959-1961: ID type consistency resolved for oidcProvider argument

The argument now uses PrefixedID!, matching the OidcProvider.id field. This addresses the prior mismatch.


1962-1963: validateOidcSession is publicly visible in schema; ensure it’s guarded

As generated, it has no directive and the resolver currently lacks an authorization decorator. This endpoint should be restricted (e.g., @usePermissions READ/OWN on SSO/session) unless explicitly designed to be public with compensating controls.

🧹 Nitpick comments (2)
api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts (1)

34-62: Re-evaluate “must-have rules” gating for public visibility

Currently, providers with no authorizationRules are hidden from the login page. If “no rules” is intended to mean “unrestricted,” this would inadvertently hide legitimate providers. Consider either:

  • Treating empty/missing rules as “visible” and only validate rule completeness when rules are present, or
  • Making this gating configurable (e.g., a visiblePublicly flag), or pushing the policy into OidcConfigPersistence to centralize visibility logic.

Suggested in-place tweak (if “no rules” should be allowed):

-        const providersWithRules = providers.filter((provider) => {
-            // Check if provider has authorization rules
-            if (!provider.authorizationRules || provider.authorizationRules.length === 0) {
-                this.logger.debug(
-                    `Hiding provider ${provider.id} from login page - no authorization rules configured`
-                );
-                return false;
-            }
-
-            // Check if at least one rule is complete and valid
-            const hasValidRules = provider.authorizationRules.some(
+        const providersWithRules = providers.filter((provider) => {
+            const rules = provider.authorizationRules;
+            // No rules configured -> allow visibility; rely on downstream auth where applicable
+            if (!rules || rules.length === 0) {
+                return true;
+            }
+
+            // Check if at least one rule is complete and valid
+            const hasValidRules = rules.some(
                 (rule) =>
                     rule.claim && // Has a claim specified
                     rule.operator && // Has an operator specified
                     rule.value && // Has values array
                     rule.value.length > 0 && // Has at least one value
                     rule.value.some((v) => v && v.trim() !== '') // At least one non-empty value
             );
api/generated-schema.graphql (1)

1956-1958: Admin query appears without @usePermissions directive in schema output

The resolvers do enforce permissions, but the generated schema doesn’t show @usePermissions. If you intend for clients to see directives in SDL (e.g., for tooling or docs), ensure your code-first directive mapping is configured to emit them during schema generation. Otherwise, this is acceptable since runtime guards exist.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b331c81 and 4e24022.

⛔ Files ignored due to path filters (1)
  • api/src/unraid-api/cli/generated/graphql.ts is excluded by !**/generated/**
📒 Files selected for processing (5)
  • api/generated-schema.graphql (4 hunks)
  • api/src/__test__/graphql/resolvers/rclone-api.service.test.ts (11 hunks)
  • api/src/unraid-api/graph/resolvers/rclone/rclone-api.service.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/sso/oidc-provider.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/src/unraid-api/graph/resolvers/sso/oidc-provider.model.ts
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts
  • api/src/__test__/graphql/resolvers/rclone-api.service.test.ts
  • api/src/unraid-api/graph/resolvers/rclone/rclone-api.service.ts
api/src/unraid-api/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Place new API source files in api/src/unraid-api/ rather than legacy locations

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts
  • api/src/unraid-api/graph/resolvers/rclone/rclone-api.service.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts
  • api/src/__test__/graphql/resolvers/rclone-api.service.test.ts
  • api/src/unraid-api/graph/resolvers/rclone/rclone-api.service.ts
api/**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Prefer not to mock simple dependencies in API tests

Files:

  • api/src/__test__/graphql/resolvers/rclone-api.service.test.ts
**/*.{test,spec}.{js,ts,jsx,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

**/*.{test,spec}.{js,ts,jsx,tsx}: General testing: use .rejects.toThrow() without arguments when asserting thrown errors
General testing: focus on behavior, not exact error message wording
General testing: avoid brittle tests tied to non-essential details (exact messages, log formats)
General testing: use mocks as nouns (objects), not verbs (behavioral overuse)

Files:

  • api/src/__test__/graphql/resolvers/rclone-api.service.test.ts
api/**/*.{test,spec}.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/api-rules.mdc)

api/**/*.{test,spec}.{js,jsx,ts,tsx}: Use Vitest for tests in the api; do not use Jest
Prefer not to mock simple dependencies in tests
For error testing, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless the message format is the subject under test

Files:

  • api/src/__test__/graphql/resolvers/rclone-api.service.test.ts
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}

📄 CodeRabbit Inference Engine (.cursor/rules/web-testing-rules.mdc)

{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Files:

  • api/src/__test__/graphql/resolvers/rclone-api.service.test.ts
🧠 Learnings (11)
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/store/**/*.ts : In store tests, mock external dependencies used by the store and verify interactions with those mocks

Applied to files:

  • api/src/__test__/graphql/resolvers/rclone-api.service.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)

Applied to files:

  • api/src/__test__/graphql/resolvers/rclone-api.service.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/mocks/**/*.ts : Frequently used mocks can also be placed under __test__/mocks

Applied to files:

  • api/src/__test__/graphql/resolvers/rclone-api.service.test.ts
📚 Learning: 2025-08-11T15:06:30.877Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T15:06:30.877Z
Learning: Applies to api/**/*.{test,spec}.{ts,tsx,js,jsx} : Prefer not to mock simple dependencies in API tests

Applied to files:

  • api/src/__test__/graphql/resolvers/rclone-api.service.test.ts
📚 Learning: 2025-08-11T15:06:30.877Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T15:06:30.877Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,js,tsx,jsx} : Web (Vue) component tests should mock external dependencies and services

Applied to files:

  • api/src/__test__/graphql/resolvers/rclone-api.service.test.ts
📚 Learning: 2025-08-11T15:06:30.877Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-11T15:06:30.877Z
Learning: Applies to web/**/*.{test,spec}.{ts,js,tsx,jsx} : Store testing with Pinia: place all mock declarations at top level; use factory functions for module mocks; clear mocks between tests

Applied to files:

  • api/src/__test__/graphql/resolvers/rclone-api.service.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls

Applied to files:

  • api/src/__test__/graphql/resolvers/rclone-api.service.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues

Applied to files:

  • api/src/__test__/graphql/resolvers/rclone-api.service.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Applied to files:

  • api/src/__test__/graphql/resolvers/rclone-api.service.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation

Applied to files:

  • api/src/__test__/graphql/resolvers/rclone-api.service.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use vi.spyOn() to specify return values or behavior of methods under test

Applied to files:

  • api/src/__test__/graphql/resolvers/rclone-api.service.test.ts
🧬 Code Graph Analysis (2)
api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts (4)
api/src/unraid-api/cli/generated/graphql.ts (4)
  • Query (1429-1485)
  • PublicOidcProvider (1409-1417)
  • OidcProvider (1207-1239)
  • OidcSessionValidation (1274-1278)
web/composables/gql/graphql.ts (4)
  • Query (1433-1489)
  • PublicOidcProvider (1413-1421)
  • OidcProvider (1207-1241)
  • OidcSessionValidation (1278-1282)
api/src/unraid-api/auth/public.decorator.ts (1)
  • Public (4-4)
web/components/sso/useSsoProviders.ts (1)
  • OidcProvider (5-12)
api/src/unraid-api/graph/resolvers/rclone/rclone-api.service.ts (1)
api/src/environment.ts (1)
  • ENVIRONMENT (81-83)
🔇 Additional comments (17)
api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts (5)

1-16: ESM import specifiers look correct

All TypeScript imports use .js extensions and the file resides under api/src/unraid-api/, matching the repo’s ESM and placement guidelines.


27-33: Public query surface for login buttons is appropriate

Marking publicOidcProviders as @public and returning a constrained PublicOidcProvider shape avoids leaking sensitive fields while serving the UI needs.


64-71: Mapping to public shape is minimal and safe

Only UI-facing fields are returned; no config or secret data is exposed via this query.


74-83: Admin listing is properly protected

@UsePermissions on oidcProviders aligns with admin-only expectations. Looks good.


84-94: ID type consistency and access control are correct

Using PrefixedID for the id argument and enforcing permissions on the single-provider query is consistent and secure.

api/generated-schema.graphql (2)

1403-1408: SsoSettings type addition is aligned with design

SsoSettings implements Node and exposes oidcProviders for the settings surface. Matches the PR intent.


1440-1442: Settings.sso field surfaces SSO under Settings as expected

Clear integration point for SSO in the unified Settings surface.

api/src/unraid-api/graph/resolvers/rclone/rclone-api.service.ts (3)

13-13: LGTM! Proper import for environment-based configuration.

The import follows the correct ESM syntax with .js extension as required by the coding guidelines.


48-53: Production guard implementation looks correct.

The environment-based early return properly prevents RClone initialization in production environments and correctly sets the initialized state to false.


366-368: URL construction normalization is well-implemented.

The endpoint normalization correctly handles both cases - endpoints with and without leading slashes - ensuring consistent URL formation for Unix socket communication. This addresses the potential issue where base URLs ending with : could create malformed URLs.

api/src/__test__/graphql/resolvers/rclone-api.service.test.ts (7)

37-45: Environment and file system mocks properly configured.

The mocks correctly simulate the development environment and file existence checks needed for the tests. The environment mock ensures the production guard doesn't interfere with test execution.


75-75: Dynamic import follows proper patterns.

The dynamic import for file-exists utility is consistent with other mocked modules and properly typed.


82-87: Mock setup properly simulates RClone socket initialization.

The mock configuration correctly simulates a successful socket check with a PID response and ensures file existence checks pass, enabling proper test isolation.


102-103: Good practice: Mock reset after initialization.

Clearing the mock after initialization prevents test interference while preserving the initialization behavior needed for service setup.


120-120: URL patterns correctly updated to reflect endpoint normalization.

All test expectations properly validate the new URL format without leading slashes after the Unix socket path. The URLs consistently use the pattern http://unix:/tmp/rclone.sock:endpoint which matches the service implementation.

Also applies to: 152-152, 183-183, 222-222, 252-252, 278-278, 305-305, 332-332, 357-357


125-127: Authorization headers correctly validated.

The tests properly validate the presence of Basic authentication headers using regex matching, which ensures the authentication mechanism is working without exposing sensitive credentials in test output.

Also applies to: 157-159, 188-190, 231-233, 260-262, 283-285, 314-316, 337-339, 362-364


124-124: Request options properly validated.

All tests correctly validate the essential request options including enableUnixSockets: true and responseType: 'json', ensuring the HTTP client is configured correctly for Unix socket communication.

Also applies to: 156-156, 187-187, 229-229, 258-258, 281-281, 312-312, 335-335, 360-360

- Updated the timeout for the CloudService.hardCheckCloud integration test to 10 seconds to accommodate longer execution times and improve test reliability.
- Introduced a new dynamic introspection plugin to control introspection queries based on sandbox mode.
- Updated the GraphQL server configuration to utilize the new introspection plugin and adjusted the introspection setting to always return true.
- Refactored the settings service to simplify the update logic and removed unnecessary comments for clarity.
Copy link
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 (13)
api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts (8)

99-127: Avoid mutating providers array when ensuring the default provider

Unshifting mutates the array returned by configService.get(). Clone first and build a new array to prevent side effects.

Apply this diff:

-        const config = this.configService.get<OidcConfig>(this.configKey());
-        const providers = config?.providers || [];
+        const config = this.configService.get<OidcConfig>(this.configKey());
+        const providers = [...(config?.providers || [])];
@@
-        const hasUnraidNet = providers.some((p) => p.id === 'unraid.net');
-        if (!hasUnraidNet) {
-            providers.unshift(this.getUnraidNetSsoProvider());
-        }
+        const hasUnraidNet = providers.some((p) => p.id === 'unraid.net');
+        const withDefault = hasUnraidNet
+            ? providers
+            : [this.getUnraidNetSsoProvider(), ...providers];
@@
-        const updatedProviders = providers.map((provider) => {
+        const updatedProviders = withDefault.map((provider) => {

129-139: Clean all optional string fields consistently (jwksUri/buttonText/buttonVariant missing)

Currently missing cleaning for jwksUri, buttonText, and buttonVariant. Trim and set to undefined for empty strings to keep serialization consistent.

Apply this diff:

     private cleanProvider(provider: OidcProvider): OidcProvider {
         // Convert empty strings to undefined for optional fields
         return {
             ...provider,
             clientSecret: provider.clientSecret?.trim() || undefined,
             authorizationEndpoint: provider.authorizationEndpoint?.trim() || undefined,
             tokenEndpoint: provider.tokenEndpoint?.trim() || undefined,
+            jwksUri: provider.jwksUri?.trim() || undefined,
             buttonIcon: provider.buttonIcon?.trim() || undefined,
+            buttonText: provider.buttonText?.trim() || undefined,
+            buttonVariant: provider.buttonVariant?.trim() || undefined,
             buttonStyle: provider.buttonStyle?.trim() || undefined,
         };
     }

146-149: Accept resolver input type in upsertProvider to avoid unsafe casts

Align the service API with the resolver input type to centralize normalization and avoid casts upstream.

Add the missing import near the top:

+import { OidcProviderInput } from '@app/unraid-api/graph/resolvers/sso/oidc-provider.model.js';

Then change the signature:

-    async upsertProvider(
-        provider: OidcProvider & { authorizationMode?: string; simpleAuthorization?: any }
-    ): Promise<OidcProvider> {
+    async upsertProvider(
+        provider: OidcProviderInput & { authorizationMode?: string; simpleAuthorization?: any }
+    ): Promise<OidcProvider> {

955-971: UI show-rule condition compares lowercase 'and'

If users set AND mode, the advanced section will not show because the const is lowercase. Use uppercase to match enum.

Apply this diff:

                                                                 {
                                                                     scope: '#/properties/authorizationRuleMode',
-                                                                    schema: { const: 'and' },
+                                                                    schema: { const: 'AND' },
                                                                 },

158-174: authorizationRuleMode is not persisted

The cleanedProvider mapping omits authorizationRuleMode; changes to AND/OR will be lost.

Apply this diff:

         const cleanedProvider: OidcProvider = {
             id: provider.id,
             name: provider.name,
             clientId: provider.clientId,
             clientSecret: provider.clientSecret,
             issuer: provider.issuer,
             authorizationEndpoint: provider.authorizationEndpoint,
             tokenEndpoint: provider.tokenEndpoint,
             jwksUri: provider.jwksUri,
             scopes: provider.scopes,
             authorizationRules: provider.authorizationRules,
+            authorizationRuleMode: provider.authorizationRuleMode,
             buttonText: provider.buttonText,
             buttonIcon: provider.buttonIcon,
             buttonVariant: provider.buttonVariant,
             buttonStyle: provider.buttonStyle,
         };

310-317: Built-in issuer skip check is incorrect (compares to 'unraid.net' instead of account.unraid.net)

The current check never skips validation for the built-in Unraid provider. Parse the hostname and allow account.unraid.net (and optionally subdomains) to avoid false warnings.

Apply this diff:

-                            // Parse the issuer URL and check if hostname is exactly 'unraid.net'
-                            const issuerUrl = new URL(provider.issuer);
-                            if (issuerUrl.hostname === 'unraid.net') {
-                                // Skip validation for unraid.net as it uses custom auth flow
-                                continue;
-                            }
+                            // Parse the issuer URL and skip validation for built-in Unraid issuer(s)
+                            const issuerUrl = new URL(provider.issuer);
+                            const host = issuerUrl.hostname;
+                            const isUnraidIssuer =
+                                host === 'account.unraid.net' || host.endsWith('.unraid.net');
+                            if (isUnraidIssuer) {
+                                // Skip validation for Unraid as it uses a custom auth flow
+                                continue;
+                            }

574-581: Rule mode enum must match GraphQL enum (uppercase) to round-trip correctly

Lowercase values will not match AuthorizationRuleMode enum and will break UI round-trip and persistence.

Apply this diff:

                             authorizationRuleMode: {
                                 type: 'string',
                                 title: 'Rule Mode',
-                                enum: ['or', 'and'],
-                                default: 'or',
+                                enum: ['OR', 'AND'],
+                                default: 'OR',
                                 description:
                                     'How to evaluate multiple rules: OR (any rule passes) or AND (all rules must pass)',
                             },

593-596: Operator enum must match GraphQL enum (uppercase) to avoid validation mismatch

Lowercase operators won’t match AuthorizationOperator and will fail rule editing/evaluation.

Apply this diff:

                                         operator: {
                                             type: 'string',
                                             title: 'Operator',
-                                            enum: ['equals', 'contains', 'endsWith', 'startsWith'],
+                                            enum: ['EQUALS', 'CONTAINS', 'ENDS_WITH', 'STARTS_WITH'],
                                         },
web/composables/gql/graphql.ts (2)

1185-1216: Do not expose clientSecret on the OidcProvider output type

Even if not selected in admin queries, exposing clientSecret on the public type is a footgun. Remove it from the schema or always resolve to null/REDACTED, then regenerate this file.

I can open a follow-up to adjust the backend GraphQL schema/resolvers to omit or mask clientSecret and regenerate the frontend types accordingly.


2590-2590: Drop buttonStyle from public providers query until backend sanitization is enforced

Avoid passing arbitrary CSS to the client from a public endpoint. Remove buttonStyle from the PublicOidcProviders operation and regenerate this file.

Update the source operation (web/components/queries/public-oidc-providers.query.ts):

 query PublicOidcProviders {
   publicOidcProviders {
     id
     name
     buttonText
     buttonIcon
     buttonVariant
-    buttonStyle
   }
 }

Then regenerate GraphQL types so this generated DocumentNode reflects the change.

api/docs/public/oidc-provider-setup.md (3)

38-40: Align “Simple Mode” domain examples with the “without @” guidance

Use bare domains in examples and clarify the explanatory sentence.

Apply this diff:

- - Allow specific email domains (e.g., @company.com)
+ - Allow specific email domains (e.g., company.com)
@@
 - **Allowed Email Domains**: Enter `company.com`
-- This allows anyone with @company.com email
+- This allows anyone with email addresses ending in `@company.com`

Also applies to: 78-80


204-210: Reword Issuer URL guidance; don’t imply validation is disabled with full discovery URL

Prefer the issuer base URL, but avoid stating that validation is “disabled.”

Apply this diff:

-The **Issuer URL** field accepts both formats, but **base URL is strongly recommended** for security:
+The **Issuer URL** field accepts either the issuer base URL or the full discovery URL; prefer the issuer base URL (not the `/.well-known/...` path):
@@
-**⚠️ Security Note**: Always use the base URL format when possible. The system automatically appends `/.well-known/openid-configuration` for OIDC discovery. Using the full discovery URL directly disables important issuer validation checks and is not recommended by the OpenID Connect specification.
+**⚠️ Security note**: Prefer the issuer base URL. The system appends `/.well-known/openid-configuration` during discovery. Providing the full discovery URL also works but may skip issuer URL normalization in our implementation; token issuer validation remains enforced. Using the base URL reduces configuration mistakes.

374-379: Okta Issuer usually requires /oauth2/default (or a custom authorization server)

Use the default auth server path, and add a note for custom servers.

Apply this diff:

- - **Issuer URL**: `https://YOUR_DOMAIN.okta.com`
+ - **Issuer URL**: `https://YOUR_DOMAIN.okta.com/oauth2/default`
+   - If you use a custom authorization server, replace `default` with your server ID.
🧹 Nitpick comments (3)
api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts (1)

158-174: Consider sanitizing buttonStyle to avoid CSS injection risks

buttonStyle is free-form CSS that will be rendered on the login page. Whitelist safe properties or sanitize the string before persisting/serving to the client.

If helpful, I can add a minimal sanitizer utility (e.g., permit background-color, border-color, color, border, border-radius, transition) and apply it here before save and when building the public provider DTOs.

api/docs/public/oidc-provider-setup.md (2)

196-199: Add a language to the fenced code block (markdownlint MD040)

Specify a language for the redirect URI block.

Apply this diff:

-```
+```text
 http://YOUR_UNRAID_IP:3001/graphql/api/auth/oidc/callback

---

`167-172`: **Fix nested list indentation (markdownlint MD007)**

Indent nested bullets by two spaces.


Apply this diff:

```diff
 - **Allowed Email Domains**: Enter domains without @ (e.g., `company.com`)
-    - Helper text: "Users with emails ending in these domains can login"
+  - Helper text: "Users with emails ending in these domains can login"
 - **Specific Email Addresses**: Add individual email addresses
-    - Helper text: "Only these exact email addresses can login"
+  - Helper text: "Only these exact email addresses can login"
@@
 - **For each rule**:
-    - **Claim**: The JWT claim to check
-    - **Operator**: How to compare (equals, contains, endsWith, startsWith)
-    - **Value**: What to match against
+  - **Claim**: The JWT claim to check
+  - **Operator**: How to compare (equals, contains, endsWith, startsWith)
+  - **Value**: What to match against

Also applies to: 179-183

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4e24022 and bbf4a48.

⛔ Files ignored due to path filters (1)
  • api/src/unraid-api/cli/generated/graphql.ts is excluded by !**/generated/**
📒 Files selected for processing (8)
  • api/docs/public/oidc-provider-setup.md (1 hunks)
  • api/src/__test__/graphql/resolvers/rclone-api.service.test.ts (11 hunks)
  • api/src/unraid-api/graph/resolvers/rclone/rclone-api.service.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/utils/form-utils.ts (2 hunks)
  • unraid-ui/src/forms/AccordionLayout.vue (1 hunks)
  • web/composables/gql/graphql.ts (18 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • api/src/unraid-api/graph/utils/form-utils.ts
  • unraid-ui/src/forms/AccordionLayout.vue
  • api/src/unraid-api/graph/resolvers/sso/sso.resolver.ts
  • api/src/unraid-api/graph/resolvers/rclone/rclone-api.service.ts
  • api/src/test/graphql/resolvers/rclone-api.service.test.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
  • web/composables/gql/graphql.ts
api/src/unraid-api/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Place new API source files in api/src/unraid-api/ rather than legacy locations

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
  • web/composables/gql/graphql.ts
web/**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

All web GraphQL code must follow the .query.ts/.mutation.ts filename conventions (do not place operations in other file types)

Files:

  • web/composables/gql/graphql.ts
🧬 Code Graph Analysis (2)
api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts (4)
api/src/unraid-api/cli/generated/graphql.ts (2)
  • OidcProvider (1184-1216)
  • OidcAuthorizationRule (1174-1182)
web/components/sso/useSsoProviders.ts (1)
  • OidcProvider (5-12)
api/src/unraid-api/types/json-forms.ts (2)
  • SettingSlice (38-46)
  • mergeSettingSlices (69-69)
api/src/unraid-api/graph/utils/form-utils.ts (2)
  • createAccordionLayout (95-128)
  • createSimpleLabeledControl (7-44)
web/composables/gql/graphql.ts (2)
api/src/unraid-api/cli/generated/graphql.ts (4)
  • ResolvedOrganizerV1 (1597-1601)
  • Scalars (11-75)
  • OidcProvider (1184-1216)
  • ResolvedOrganizerView (1603-1609)
web/components/sso/useSsoProviders.ts (1)
  • OidcProvider (5-12)
🪛 LanguageTool
api/docs/public/oidc-provider-setup.md

[grammar] ~78-~78: There might be a mistake here.
Context: ...on: - Allowed Email Domains: Enter company.com - This allows anyone with @company.com ema...

(QB_NEW_EN)


[grammar] ~101-~101: There might be a mistake here.
Context: ...n Rule Mode** to AND 2. Add two rules: - Rule 1: - Claim: email -...

(QB_NEW_EN)


[grammar] ~102-~102: There might be a mistake here.
Context: ... to AND 2. Add two rules: - Rule 1: - Claim: email - Operator: `e...

(QB_NEW_EN)


[grammar] ~103-~103: There might be a mistake here.
Context: ...o rules: - Rule 1: - Claim: email - Operator: endsWith - Value:...

(QB_NEW_EN)


[grammar] ~104-~104: There might be a mistake here.
Context: ...Claim: email - Operator: endsWith - Value: @company.com - Rule 2: ...

(QB_NEW_EN)


[grammar] ~105-~105: There might be a mistake here.
Context: ...perator**: endsWith - Value: @company.com - Rule 2: - Claim: `email_verifie...

(QB_NEW_EN)


[grammar] ~106-~106: There might be a mistake here.
Context: ...- Value: @company.com - Rule 2: - Claim: email_verified - **Opera...

(QB_NEW_EN)


[grammar] ~107-~107: There might be a mistake here.
Context: ...any.com - Rule 2: - **Claim**:email_verified - **Operator**:equals - **Value**:...

(QB_NEW_EN)


[grammar] ~108-~108: There might be a mistake here.
Context: ...: email_verified - Operator: equals - Value: true This ensures users must...

(QB_NEW_EN)


[grammar] ~118-~118: There might be a mistake here.
Context: ...(default) 2. Add rules for each group: - **Claim**:groups - **Operator**:co...

(QB_NEW_EN)


[grammar] ~119-~119: There might be a mistake here.
Context: ...d rules for each group: - Claim: groups - Operator: contains - Value: `...

(QB_NEW_EN)


[grammar] ~120-~120: There might be a mistake here.
Context: ... Claim: groups - Operator: contains - Value: admins Or add another ...

(QB_NEW_EN)


[grammar] ~123-~123: There might be a mistake here.
Context: ...**: admins Or add another rule: - Claim: groups - Operator: `co...

(QB_NEW_EN)


[grammar] ~124-~124: There might be a mistake here.
Context: ... Or add another rule: - Claim: groups - Operator: contains - Value: `...

(QB_NEW_EN)


[grammar] ~125-~125: There might be a mistake here.
Context: ... Claim: groups - Operator: contains - Value: developers Users in either `...

(QB_NEW_EN)


[grammar] ~132-~132: There might be a mistake here.
Context: ...d. #### Multiple Domains - Claim: email - Operator: endsWith - Values: Add...

(QB_NEW_EN)


[grammar] ~133-~133: There might be a mistake here.
Context: ...s - Claim: email - Operator: endsWith - Values: Add multiple domains (e.g., `c...

(QB_NEW_EN)


[grammar] ~141-~141: There might be a mistake here.
Context: .... Add multiple rules that ALL must pass: - Email must be from company domain - E...

(QB_NEW_EN)


[grammar] ~160-~160: There might be a mistake here.
Context: ...thorization (recommended for most users) - advanced: For complex claim-based rule...

(QB_NEW_EN)


[grammar] ~179-~179: There might be a mistake here.
Context: ...e claim-based rules - For each rule: - Claim: The JWT claim to check - **...

(QB_NEW_EN)


[grammar] ~206-~206: There might be a mistake here.
Context: ...ecurity: - Base URL (recommended): https://accounts.google.com - Full discovery URL: `https://accounts....

(QB_NEW_EN)


[grammar] ~211-~211: There might be a mistake here.
Context: ...ation. Examples of correct base URLs: - Google: https://accounts.google.com - ...

(QB_NEW_EN)


[grammar] ~212-~212: There might be a mistake here.
Context: ...mples of correct base URLs:** - Google: https://accounts.google.com - Microsoft/Azure: `https://login.microsof...

(QB_NEW_EN)


[grammar] ~213-~213: There might be a mistake here.
Context: ...accounts.google.com- Microsoft/Azure:https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0` - Keycloak: `https://keycloak.example.com/...

(QB_NEW_EN)


[grammar] ~214-~214: There might be a mistake here.
Context: ...ne.com/YOUR_TENANT_ID/v2.0- Keycloak:https://keycloak.example.com/realms/YOUR_REALM` - Authelia: https://auth.yourdomain.com ...

(QB_NEW_EN)


[grammar] ~240-~240: There might be a mistake here.
Context: ...orrectly (without @) - In Advanced Mode: - Verify claim names match exactly what yo...

(QB_NEW_EN)


[grammar] ~269-~269: There might be a mistake here.
Context: ...gs for: - Received claims from provider - Authorization rule evaluation - Token va...

(QB_NEW_EN)


[grammar] ~270-~270: There might be a mistake here.
Context: ...provider - Authorization rule evaluation - Token validation errors ## Security Bes...

(QB_NEW_EN)


[grammar] ~286-~286: There might be a mistake here.
Context: ...ify network connectivity between Unraid and provider ## Provider-Specific Setup #...

(QB_NEW_EN)


[grammar] ~294-~294: There might be a mistake here.
Context: ...ules in the interface. Configuration: - Issuer URL: Pre-configured (built-in p...

(QB_NEW_EN)


[grammar] ~295-~295: There might be a mistake here.
Context: ...RL**: Pre-configured (built-in provider) - Client ID/Secret: Pre-configured (buil...

(QB_NEW_EN)


[grammar] ~296-~296: There might be a mistake here.
Context: ...et**: Pre-configured (built-in provider) - Redirect URI: `http://YOUR_UNRAID_IP:3...

(QB_NEW_EN)


[grammar] ~307-~307: There might be a mistake here.
Context: ...gle.com/). Create a new OAuth client ID with Web application type. **Configuration:...

(QB_NEW_EN)


[grammar] ~309-~309: There might be a mistake here.
Context: ... Web application type. Configuration: - Issuer URL: `https://accounts.google.c...

(QB_NEW_EN)


[grammar] ~310-~310: There might be a mistake here.
Context: .... Configuration: - Issuer URL: https://accounts.google.com - Client ID/Secret: From Google Cloud Co...

(QB_NEW_EN)


[grammar] ~311-~311: There might be a mistake here.
Context: ...m Google Cloud Console OAuth credentials - Required Scopes: openid, profile, ...

(QB_NEW_EN)


[grammar] ~312-~312: There might be a mistake here.
Context: ...Required Scopes**: openid, profile, email - Redirect URI: `http://YOUR_UNRAID_IP:3...

(QB_NEW_EN)


[grammar] ~321-~321: There might be a mistake here.
Context: ...hash-password command. Configuration: - Issuer URL: `https://auth.yourdomain.c...

(QB_NEW_EN)


[grammar] ~322-~322: There might be a mistake here.
Context: .... Configuration: - Issuer URL: https://auth.yourdomain.com - Client ID: unraid-api (or as configu...

(QB_NEW_EN)


[grammar] ~323-~323: There might be a mistake here.
Context: ...raid-api` (or as configured in Authelia) - Client Secret: Your unhashed secret - ...

(QB_NEW_EN)


[grammar] ~324-~324: There might be a mistake here.
Context: ... Client Secret: Your unhashed secret - Required Scopes: openid, profile, ...

(QB_NEW_EN)


[grammar] ~325-~325: There might be a mistake here.
Context: ...Scopes**: openid, profile, email, groups - Redirect URI: `http://YOUR_UNRAID_IP:3...

(QB_NEW_EN)


[grammar] ~334-~334: There might be a mistake here.
Context: ...d note your tenant ID. Configuration: - Issuer URL: `https://login.microsofton...

(QB_NEW_EN)


[grammar] ~335-~335: There might be a mistake here.
Context: .... Configuration: - Issuer URL: https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0 - Client ID: Your Application (client) I...

(QB_NEW_EN)


[grammar] ~336-~336: There might be a mistake here.
Context: ...lient ID**: Your Application (client) ID - Client Secret: Generated client secret...

(QB_NEW_EN)


[grammar] ~337-~337: There might be a mistake here.
Context: ...Client Secret**: Generated client secret - Required Scopes: openid, profile, ...

(QB_NEW_EN)


[grammar] ~338-~338: There might be a mistake here.
Context: ...Required Scopes**: openid, profile, email - Redirect URI: `http://YOUR_UNRAID_IP:3...

(QB_NEW_EN)


[grammar] ~345-~345: There might be a mistake here.
Context: ...cloak Create a new confidential client in Keycloak Admin Console with `openid-con...

(QB_NEW_EN)


[grammar] ~347-~347: There might be a mistake here.
Context: ...m the Credentials tab. Configuration: - Issuer URL: `https://keycloak.example....

(QB_NEW_EN)


[grammar] ~348-~348: There might be a mistake here.
Context: .... Configuration: - Issuer URL: https://keycloak.example.com/realms/YOUR_REALM - Client ID: unraid-api (or as configu...

(QB_NEW_EN)


[grammar] ~349-~349: There might be a mistake here.
Context: ...raid-api` (or as configured in Keycloak) - Client Secret: From Keycloak Credentia...

(QB_NEW_EN)


[grammar] ~350-~350: There might be a mistake here.
Context: ... Secret**: From Keycloak Credentials tab - Required Scopes: openid, profile, ...

(QB_NEW_EN)


[grammar] ~351-~351: There might be a mistake here.
Context: ...Required Scopes**: openid, profile, email - Redirect URI: `http://YOUR_UNRAID_IP:3...

(QB_NEW_EN)


[grammar] ~360-~360: There might be a mistake here.
Context: ...nk it to the provider. Configuration: - Issuer URL: `https://authentik.example...

(QB_NEW_EN)


[grammar] ~361-~361: There might be a mistake here.
Context: .... Configuration: - Issuer URL: https://authentik.example.com/application/o/unraid-api/ - Client ID: From Authentik provider con...

(QB_NEW_EN)


[grammar] ~362-~362: There might be a mistake here.
Context: ...*: From Authentik provider configuration - Client Secret: From Authentik provider...

(QB_NEW_EN)


[grammar] ~363-~363: There might be a mistake here.
Context: ...*: From Authentik provider configuration - Required Scopes: openid, profile, ...

(QB_NEW_EN)


[grammar] ~364-~364: There might be a mistake here.
Context: ...Required Scopes**: openid, profile, email - Redirect URI: `http://YOUR_UNRAID_IP:3...

(QB_NEW_EN)


[grammar] ~371-~371: There might be a mistake here.
Context: ...Okta Create a new OIDC Web Application in Okta Admin Console and assign appropria...

(QB_NEW_EN)


[grammar] ~373-~373: There might be a mistake here.
Context: ...riate users or groups. Configuration: - Issuer URL: `https://YOUR_DOMAIN.okta....

(QB_NEW_EN)


[grammar] ~374-~374: There might be a mistake here.
Context: .... Configuration: - Issuer URL: https://YOUR_DOMAIN.okta.com - Client ID: From Okta application confi...

(QB_NEW_EN)


[grammar] ~375-~375: There might be a mistake here.
Context: ...D**: From Okta application configuration - Client Secret: From Okta application c...

(QB_NEW_EN)


[grammar] ~376-~376: There might be a mistake here.
Context: ...t**: From Okta application configuration - Required Scopes: openid, profile, ...

(QB_NEW_EN)


[grammar] ~377-~377: There might be a mistake here.
Context: ...Required Scopes**: openid, profile, email - Redirect URI: `http://YOUR_UNRAID_IP:3...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
api/docs/public/oidc-provider-setup.md

168-168: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


170-170: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


180-180: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


181-181: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


182-182: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


196-196: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (1)
web/composables/gql/graphql.ts (1)

1449-1451: Good improvement: use PrefixedID for provider lookups

oidcProvider now accepts PrefixedID input, ensuring consistent ID normalization. LGTM.

- Added a preHandler hook to block GET requests to the /graphql endpoint when the sandbox mode is disabled, returning a 403 error with a descriptive message.
- Refactored the sandbox plugin to remove the isSandboxEnabled parameter, simplifying its usage.
- Updated the ConnectSettings component to reflect changes in restart logic based on mutation response instead of computed properties.
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

Copy link
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: 4

🔭 Outside diff range comments (2)
api/src/unraid-api/graph/resolvers/settings/settings.service.ts (1)

54-68: Fix: allow clearing SSO users and extra origins (don’t gate on truthiness)

Using truthiness prevents setting empty arrays, so users can’t clear lists. Check for undefined instead.

-        if (settings.ssoSubIds) {
+        if (settings.ssoSubIds !== undefined) {
             await this.ssoUserService.setSsoUsers(settings.ssoSubIds);
         }
-        if (settings.extraOrigins) {
+        if (settings.extraOrigins !== undefined) {
             // @ts-expect-error - this is correct, but the configService typescript implementation is too narrow
             this.configService.set('api.extraOrigins', settings.extraOrigins);
         }
api/src/unraid-api/graph/graph.module.ts (1)

30-31: Bug: Boolean('false') yields true — parse sandbox flag explicitly

Using Boolean(configService.get('api.sandbox')) treats the string "false" as true. Parse the value explicitly to avoid enabling features unintentionally.

-                const isSandboxEnabled = () => Boolean(configService.get('api.sandbox'));
+                const isSandboxEnabled = () => {
+                    const v = configService.get<boolean | string>('api.sandbox');
+                    return v === true || v === 'true' || v === 1 || v === '1';
+                };
♻️ Duplicate comments (8)
api/src/unraid-api/graph/resolvers/settings/settings.service.ts (1)

31-42: Refactor migration check: avoid dynamic import and improve clarity/perf

Importing fs/promises inside the method on every call is unnecessary. Use a static import and call access() directly.

Apply within-range changes:

-        try {
-            const { access } = await import('fs/promises');
-            await access(this.oidcConfig.configPath());
+        try {
+            await access(this.oidcConfig.configPath());

Add the missing import at the top of the file:

+import { access } from 'fs/promises';
api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts (7)

129-139: Clean all optional strings: add jwksUri, buttonText, buttonVariant

These fields can also be empty strings; normalize to undefined for consistency.

         return {
             ...provider,
             clientSecret: provider.clientSecret?.trim() || undefined,
             authorizationEndpoint: provider.authorizationEndpoint?.trim() || undefined,
             tokenEndpoint: provider.tokenEndpoint?.trim() || undefined,
+            jwksUri: provider.jwksUri?.trim() || undefined,
+            buttonText: provider.buttonText?.trim() || undefined,
             buttonIcon: provider.buttonIcon?.trim() || undefined,
+            buttonVariant: provider.buttonVariant?.trim() || undefined,
             buttonStyle: provider.buttonStyle?.trim() || undefined,
         };

9-13: Align service API with resolver input type (accept OidcProviderInput)

Let the service accept the GraphQL input + UI fields to avoid unsafe casts in callers; normalization here already builds a proper OidcProvider.

Imports:

-} from '@app/unraid-api/graph/resolvers/sso/oidc-provider.model.js';
+    OidcProviderInput,
+} from '@app/unraid-api/graph/resolvers/sso/oidc-provider.model.js';

Signature:

-    async upsertProvider(
-        provider: OidcProvider & { authorizationMode?: string; simpleAuthorization?: any }
-    ): Promise<OidcProvider> {
+    async upsertProvider(
+        provider: OidcProviderInput & { authorizationMode?: string; simpleAuthorization?: any }
+    ): Promise<OidcProvider> {

Also applies to: 146-149


158-174: Fix: authorizationRuleMode is dropped during upsert

The cleaned object omits authorizationRuleMode, so AND/OR won’t persist.

         const cleanedProvider: OidcProvider = {
             id: provider.id,
             name: provider.name,
             clientId: provider.clientId,
             clientSecret: provider.clientSecret,
             issuer: provider.issuer,
             authorizationEndpoint: provider.authorizationEndpoint,
             tokenEndpoint: provider.tokenEndpoint,
             jwksUri: provider.jwksUri,
             scopes: provider.scopes,
             authorizationRules: provider.authorizationRules,
+            authorizationRuleMode: provider.authorizationRuleMode,
             buttonText: provider.buttonText,
             buttonIcon: provider.buttonIcon,
             buttonVariant: provider.buttonVariant,
             buttonStyle: provider.buttonStyle,
         };

312-317: Correct issuer host check for Unraid; skip validation for account.unraid.net and subdomains

Exact equality to 'unraid.net' is wrong. Use proper host parsing and allow account.unraid.net or *.unraid.net per the custom auth flow.

-                            // Parse the issuer URL and check if hostname is exactly 'unraid.net'
-                            const issuerUrl = new URL(provider.issuer);
-                            if (issuerUrl.hostname === 'unraid.net') {
-                                // Skip validation for unraid.net as it uses custom auth flow
-                                continue;
-                            }
+                            const host = new URL(provider.issuer).hostname;
+                            const isUnraidIssuer =
+                                host === 'account.unraid.net' || host.endsWith('.unraid.net');
+                            if (isUnraidIssuer) {
+                                // Skip validation for Unraid built-in/custom flow
+                                continue;
+                            }

574-581: Enum mismatch: use uppercase for Rule Mode to match GraphQL (OR/AND)

Lowercase values won’t round-trip with the AuthorizationRuleMode enum.

-                            authorizationRuleMode: {
+                            authorizationRuleMode: {
                                 type: 'string',
                                 title: 'Rule Mode',
-                                enum: ['or', 'and'],
-                                default: 'or',
+                                enum: ['OR', 'AND'],
+                                default: 'OR',
                                 description:
                                     'How to evaluate multiple rules: OR (any rule passes) or AND (all rules must pass)',
-                            },
+                            },

593-596: Enum mismatch: use uppercase operator values to match GraphQL enum

These must match AuthorizationOperator (EQUALS, CONTAINS, ENDS_WITH, STARTS_WITH).

-                                        operator: {
+                                        operator: {
                                             type: 'string',
                                             title: 'Operator',
-                                            enum: ['equals', 'contains', 'endsWith', 'startsWith'],
+                                            enum: ['EQUALS', 'CONTAINS', 'ENDS_WITH', 'STARTS_WITH'],
-                                        },
+                                        },

99-110: Avoid mutating in-memory providers array returned from config service

Unshifting into providers mutates the array obtained from config, causing side-effects. Clone and build a new array.

-        const providers = config?.providers || [];
+        const providers = [...(config?.providers || [])];
@@
-        if (!hasUnraidNet) {
-            providers.unshift(this.getUnraidNetSsoProvider());
-        }
+        const withDefault = hasUnraidNet
+            ? providers
+            : [this.getUnraidNetSsoProvider(), ...providers];
@@
-        const updatedProviders = providers.map((provider) => {
+        const updatedProviders = withDefault.map((provider) => {
🧹 Nitpick comments (5)
packages/unraid-api-plugin-connect/src/__test__/cloud.service.test.ts (1)

41-45: Avoid asserting on error message substrings; make the test deterministic

Catching and special-casing errors by checking error.message.includes('Timeout') is brittle and violates the testing guideline to avoid relying on exact error messages. Prefer making the test deterministic by stubbing/mocking the network path so you can consistently assert:

  • Resolution path: await expect(promise).resolves.toMatchObject({ status: 'error' }), or
  • Rejection path: await expect(promise).rejects.toThrow() (without checking the message).

This will remove the need for the try/catch and the flakiness from real network timeouts.

api/src/unraid-api/graph/resolvers/settings/settings.service.ts (1)

156-164: Nit: “IDs” vs “ID's” and consistency across label/description

Use “IDs” (plural) not “ID's” (possessive) and keep it consistent in both places.

-                    description: `Provide a list of Unique Unraid Account ID's. Find yours at <a href="https://account.unraid.net/settings" target="_blank" rel="noopener noreferrer">account.unraid.net/settings</a>.`,
+                    description: `Provide a list of Unique Unraid Account IDs. Find yours at <a href="https://account.unraid.net/settings" target="_blank" rel="noopener noreferrer">account.unraid.net/settings</a>.`,
api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts (1)

37-46: Use path.basename for env override; avoid manual split

Manual split is brittle and not Windows-safe. Use node:path basename.

Apply within-range change:

-        const envPath = process.env.PATHS_OIDC_JSON;
-        if (envPath) {
-            // Extract just the filename from the path
-            const parts = envPath.split('/');
-            return parts[parts.length - 1];
-        }
+        const envPath = process.env.PATHS_OIDC_JSON;
+        if (envPath) {
+            return basename(envPath);
+        }

Add missing import at top:

+import { basename } from 'node:path';
api/src/unraid-api/graph/sandbox-plugin.ts (1)

64-68: Make CSRF token retrieval fault-tolerant with a safe fallback (per prior learning)

Per the team’s prior agreement, sandbox should remain accessible even if CSRF retrieval fails. Add a silent fallback to an empty token to avoid breaking the landing page.

-    const { getters } = await import('@app/store/index.js');
-    const csrfToken = getters.emhttp().var.csrfToken;
-    const plugin = await getSandboxPlugin(csrfToken);
+    let csrfToken = '';
+    try {
+        const { getters } = await import('@app/store/index.js');
+        csrfToken = getters.emhttp()?.var?.csrfToken ?? '';
+    } catch {
+        // Intentionally ignore to keep sandbox accessible (dev tool)
+    }
+    const plugin = await getSandboxPlugin(csrfToken);
api/src/unraid-api/graph/graph.module.ts (1)

38-39: Note: introspection=true with plugin gating is acceptable, but AST-based detection strengthens it

Given the server-level introspection is always on, the companion plugin must robustly detect introspection. Pair this with the AST-based detection suggested in introspection-plugin.ts to close edge cases where operationName is renamed.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bbf4a48 and a88c521.

📒 Files selected for processing (8)
  • api/src/unraid-api/graph/graph.module.ts (3 hunks)
  • api/src/unraid-api/graph/introspection-plugin.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/settings/settings.service.ts (7 hunks)
  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts (1 hunks)
  • api/src/unraid-api/graph/sandbox-plugin.ts (3 hunks)
  • api/src/unraid-api/main.ts (2 hunks)
  • packages/unraid-api-plugin-connect/src/__test__/cloud.service.test.ts (1 hunks)
  • web/components/ConnectSettings/ConnectSettings.ce.vue (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/components/ConnectSettings/ConnectSettings.ce.vue
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • packages/unraid-api-plugin-connect/src/__test__/cloud.service.test.ts
  • api/src/unraid-api/graph/introspection-plugin.ts
  • api/src/unraid-api/graph/sandbox-plugin.ts
  • api/src/unraid-api/main.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
  • api/src/unraid-api/graph/resolvers/settings/settings.service.ts
  • api/src/unraid-api/graph/graph.module.ts
**/*.{test,spec}.{js,ts,jsx,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

**/*.{test,spec}.{js,ts,jsx,tsx}: General testing: use .rejects.toThrow() without arguments when asserting thrown errors
General testing: focus on behavior, not exact error message wording
General testing: avoid brittle tests tied to non-essential details (exact messages, log formats)
General testing: use mocks as nouns (objects), not verbs (behavioral overuse)

Files:

  • packages/unraid-api-plugin-connect/src/__test__/cloud.service.test.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • packages/unraid-api-plugin-connect/src/__test__/cloud.service.test.ts
  • api/src/unraid-api/graph/introspection-plugin.ts
  • api/src/unraid-api/graph/sandbox-plugin.ts
  • api/src/unraid-api/main.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
  • api/src/unraid-api/graph/resolvers/settings/settings.service.ts
  • api/src/unraid-api/graph/graph.module.ts
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}

📄 CodeRabbit Inference Engine (.cursor/rules/web-testing-rules.mdc)

{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Files:

  • packages/unraid-api-plugin-connect/src/__test__/cloud.service.test.ts
api/src/unraid-api/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Place new API source files in api/src/unraid-api/ rather than legacy locations

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/graph/introspection-plugin.ts
  • api/src/unraid-api/graph/sandbox-plugin.ts
  • api/src/unraid-api/main.ts
  • api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts
  • api/src/unraid-api/graph/resolvers/settings/settings.service.ts
  • api/src/unraid-api/graph/graph.module.ts
🧠 Learnings (4)
📚 Learning: 2025-01-15T21:34:00.006Z
Learnt from: pujitm
PR: unraid/api#1047
File: api/src/unraid-api/graph/sandbox-plugin.ts:57-57
Timestamp: 2025-01-15T21:34:00.006Z
Learning: In the GraphQL sandbox (api/src/unraid-api/graph/sandbox-plugin.ts), CSRF token validation should fail silently with a fallback value to maintain sandbox accessibility, as it's a development tool where strict security measures aren't required.

Applied to files:

  • api/src/unraid-api/graph/sandbox-plugin.ts
  • api/src/unraid-api/main.ts
📚 Learning: 2025-04-23T20:19:42.542Z
Learnt from: pujitm
PR: unraid/api#1367
File: packages/unraid-api-plugin-connect/src/pubsub/user.service.ts:44-52
Timestamp: 2025-04-23T20:19:42.542Z
Learning: The project uses a custom or extended implementation of NestJS ConfigService that includes a `set()` method for runtime configuration mutation, unlike the standard nestjs/config package which only provides getter methods.

Applied to files:

  • api/src/unraid-api/main.ts
📚 Learning: 2025-01-27T14:31:42.305Z
Learnt from: elibosley
PR: unraid/api#1063
File: web/components/SsoButton.ce.vue:5-8
Timestamp: 2025-01-27T14:31:42.305Z
Learning: In the Unraid API web components, SSO-related props are intentionally provided in both camelCase (`ssoEnabled`) and lowercase (`ssoenabled`) variants to support interchangeable usage across different contexts (e.g., HTML attributes vs Vue props).

Applied to files:

  • api/src/unraid-api/graph/resolvers/settings/settings.service.ts
📚 Learning: 2025-03-12T13:48:14.850Z
Learnt from: pujitm
PR: unraid/api#1211
File: web/composables/gql/gql.ts:17-18
Timestamp: 2025-03-12T13:48:14.850Z
Learning: In the Unraid API project, the duplicate GraphQL query and mutation strings in gql.ts files are intentionally generated by GraphQL CodeGen tool and are necessary for the type system to function properly.

Applied to files:

  • api/src/unraid-api/graph/graph.module.ts
🧬 Code Graph Analysis (4)
api/src/unraid-api/graph/sandbox-plugin.ts (1)
api/src/store/index.ts (1)
  • getters (18-23)
api/src/unraid-api/graph/resolvers/sso/oidc-config.service.ts (5)
api/src/unraid-api/cli/generated/graphql.ts (2)
  • OidcProvider (1184-1216)
  • OidcAuthorizationRule (1174-1182)
web/composables/gql/graphql.ts (2)
  • OidcProvider (1184-1216)
  • OidcAuthorizationRule (1174-1182)
web/components/sso/useSsoProviders.ts (1)
  • OidcProvider (5-12)
api/src/unraid-api/types/json-forms.ts (2)
  • SettingSlice (38-46)
  • mergeSettingSlices (69-69)
api/src/unraid-api/graph/utils/form-utils.ts (2)
  • createAccordionLayout (95-128)
  • createSimpleLabeledControl (7-44)
api/src/unraid-api/graph/resolvers/settings/settings.service.ts (3)
packages/unraid-shared/src/services/sso.ts (1)
  • SsoUserService (1-14)
api/src/unraid-api/types/json-forms.ts (2)
  • SettingSlice (38-46)
  • mergeSettingSlices (69-69)
api/src/unraid-api/graph/utils/form-utils.ts (1)
  • createLabeledControl (49-89)
api/src/unraid-api/graph/graph.module.ts (2)
api/src/unraid-api/graph/introspection-plugin.ts (1)
  • createDynamicIntrospectionPlugin (3-30)
api/src/unraid-api/graph/sandbox-plugin.ts (1)
  • createSandboxPlugin (85-90)
🔇 Additional comments (5)
api/src/unraid-api/graph/resolvers/settings/settings.service.ts (1)

55-62: Confirm intentional behavior: ignoring restart flag from setSsoUsers

setSsoUsers returns a boolean indicating if a restart is required. You now ignore it and hardcode restartRequired=false. If that’s intentional due to OIDC changes (no restart needed), all good; otherwise, wire this through.

If it should propagate, adjust as:

-        const restartRequired = false;
+        let restartRequired = false;
...
-        if (settings.ssoSubIds) {
-            await this.ssoUserService.setSsoUsers(settings.ssoSubIds);
+        if (settings.ssoSubIds !== undefined) {
+            restartRequired = await this.ssoUserService.setSsoUsers(settings.ssoSubIds);
         }
api/src/unraid-api/graph/introspection-plugin.ts (1)

3-6: Overall approach LGTM

The dynamic plugin cleanly gates introspection at response time and composes well with GraphModule’s plugin wiring.

Also applies to: 29-30

api/src/unraid-api/graph/sandbox-plugin.ts (2)

31-47: LGTM: Local-default landing page plugin configuration

Dynamic import and embed headers look correct; includeCookies + initial document are appropriate defaults.


85-90: LGTM: Plugin wrapper is straightforward

Wrapping renderLandingPage via serverWillStart is clean and keeps concerns isolated.

api/src/unraid-api/graph/graph.module.ts (1)

47-50: LGTM: Plugin composition and dynamic introspection

The split between dynamic introspection gating and sandbox rendering is clean and aligns with per-request control. Order is fine.

- Improved the sandbox access control by robustly coercing the sandbox configuration to a boolean value.
- Updated the introspection plugin to detect introspection requests more effectively, allowing for better control over GraphQL introspection based on sandbox mode.
- Standardized the error code for introspection denial to 'INTROSPECTION_DISABLED' for clarity.
…oding

- Updated the introspection plugin to more accurately identify introspection queries by checking for the presence of __schema at the root level.
- Replaced manual JWT decoding with the jose library for better reliability and security.
- Adjusted logging to ensure sensitive information is not exposed while still providing useful debugging information.
- Reduced session timeout for one-time tokens to enhance security.
- Introduced a new test suite for the dynamic introspection plugin, covering various scenarios based on sandbox mode.
- Validated that introspection queries are allowed when sandbox mode is enabled and blocked when disabled.
- Ensured proper handling of edge cases, including missing or empty queries, and dynamic sandbox status checks.
- Enhanced test coverage for regular queries, mutations, and subscriptions to confirm expected behavior.
- Renamed the OIDC provider examples section to "OIDC Provider Setup" for better clarity.
- Enhanced the Google OAuth setup instructions with detailed steps and a warning about domain requirements for redirect URIs.
- Updated various sections to ensure consistency and clarity in the documentation.
- Made minor adjustments to the introspection plugin tests for type safety and improved error handling.
…d validation

- Added OidcStateService to handle secure state generation and validation for OIDC flows.
- Integrated state management into OidcAuthService, replacing the previous state handling logic.
- Implemented unit tests for OidcStateService to ensure secure state generation, validation, and extraction of provider information.
- Updated SsoModule to include OidcStateService for dependency injection.
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

- Added OidcStateService to the test suite for OidcAuthService.
- Updated tests to verify that the state parameter in authorization URLs includes a secure token format.
- Ensured that state handling logic aligns with the recent implementation of secure state management.
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

@github-actions
Copy link
Contributor

This plugin has been deployed to Cloudflare R2 and is available for testing.
Download it at this URL:

https://preview.dl.unraid.net/unraid-api/tag/PR1563/dynamix.unraid.net.plg

@elibosley elibosley merged commit 979a267 into main Aug 15, 2025
11 of 13 checks passed
@elibosley elibosley deleted the api-feat-oidc-sso branch August 15, 2025 15:59
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.

2 participants