Skip to content

Add OIDC/OAuth SSO support (rebased from #719)#916

Closed
reazndev wants to merge 7 commits into
rybbit-io:masterfrom
reazndev:master
Closed

Add OIDC/OAuth SSO support (rebased from #719)#916
reazndev wants to merge 7 commits into
rybbit-io:masterfrom
reazndev:master

Conversation

@reazndev
Copy link
Copy Markdown

@reazndev reazndev commented Feb 24, 2026

Summary

  • Adds OpenID Connect (OIDC) and OAuth SSO support for self-hosted instances
  • Enables dynamic provider configuration via environment variables
  • Supports multiple OIDC providers simultaneously

Based on #719 by @acvigue, updated for compatibility with current main.

Changes from original PR

  • Resolved merge conflicts with 3 months of main branch changes
  • Kept all OIDC functionality (genericOAuth, dynamic provider detection,
    INTERNAL_AUTHENTICATION_ENABLED)
  • Merged in main's updates (onboarding tips, invitation site access, auth-utils)
  • Server and client builds pass

Configuration

# Optional OIDC providers (can add multiple with different names)
OIDC_{PROVIDER}_NAME="SSO Provider"
OIDC_{PROVIDER}_CLIENT_ID=xxx
OIDC_{PROVIDER}_CLIENT_SECRET=xxx
OIDC_{PROVIDER}_DISCOVERY_URL=https://example.com/.well-known/openid-configuration

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
* SSO / OpenID Connect sign-in with dynamic provider discovery and config-driven UI; social logins shown only for configured providers; email/password auth can be toggled off and auto-redirects to single SSO provider when appropriate.

* **Documentation**
* README comparison table updated for SSO/OpenID Connect; compose files include commented guidance for OIDC configuration.

* **Chores**
* Added shared ExternalLink component and updated various empty-state/help links; added/ reordered localization keys across languages.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Copilot AI review requested due to automatic review settings February 24, 2026 21:22
@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 24, 2026

@reazndev is attempting to deploy a commit to the goldflag's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Server discovers OIDC and social providers from environment variables and exposes them plus an internal-auth toggle via /api/config; the client consumes these configs to render dynamic OIDC/social buttons, register generic OAuth clients, gate email/password UI, and support SSO sign-in/auto-redirect flows.

Changes

Cohort / File(s) Summary
Docs & Compose
README.md, docker-compose.yml, docker-compose.cloud.yml
Adds "SSO / OpenID Connect" row to README and inserts comment-only guidance for configuring OIDC providers in Docker Compose files; minor compose comment/whitespace edits.
Server: discovery & auth
server/src/lib/const.ts, server/src/api/getConfig.ts, server/src/lib/auth.ts
Adds INTERNAL_AUTHENTICATION_ENABLED, implements getOIDCProviders() and getSocialProviders() that read env vars, conditionally enables internal auth/email-password, plugs genericOAuth into server auth config, and exposes enabled providers in /api/config.
Client: configs & auth client
client/src/lib/configs.ts, client/src/lib/auth.ts
Extends Configs with internalAuthEnabled, enabledOIDCProviders, enabledSocialProviders; registers genericOAuthClient() in auth client plugins; narrows socialProviders to configured set.
Client: auth UI & flows
client/src/components/auth/SocialButtons.tsx, client/src/app/invitation/components/login.tsx, client/src/app/login/page.tsx, client/src/app/signup/page.tsx, client/src/app/signup/components/AccountStep.tsx
Refactors SocialButtons to be config-driven (OIDC + social), adds handleOIDCAuth/handleSocialAuth, waits for useConfigs() before render; gates email/password UI on internalAuthEnabled; adds SSO handlers and automatic OAuth redirect when a single OIDC provider exists; wires configs into signup AccountStep.
Client: new shared link + usages
client/src/components/ExternalLink.tsx, client/src/app/.../NoData.tsx, client/src/app/[site]/funnels/page.tsx, client/src/app/[site]/goals/page.tsx, client/src/app/[site]/users/components/TraitsExplorer.tsx, client/src/app/settings/account/page.tsx, client/src/app/settings/teams/layout.tsx
Adds ExternalLink component and replaces raw external /next/link usages across several empty-states and settings pages to use the new component and adjust layout/classes where needed.
Localization
client/messages/*.json
Adds new translation key Ix3wt3 (empty string) across many locales and reorders several existing keys' positions in JSON files.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Browser
    participant Client as Client (UI)
    participant Server as Server / API
    participant AuthClient as Auth Client
    participant OIDC as OIDC Provider

    Browser->>Client: GET /login
    Client->>Server: GET /api/config (useConfigs)
    Server->>Server: read env -> getOIDCProviders(), getSocialProviders(), INTERNAL_AUTHENTICATION_ENABLED
    Server-->>Client: { enabledOIDCProviders, enabledSocialProviders, internalAuthEnabled }
    Client->>Browser: render UI (email/password OR SSO buttons) based on configs

    Note right of Client: User selects SSO
    Browser->>Client: click OIDC/social button
    Client->>AuthClient: signIn.oauth2({ providerId, callbackURL, ... })
    AuthClient->>OIDC: redirect to authorize
    OIDC->>AuthClient: callback with tokens
    AuthClient-->>Client: complete sign-in (session established)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
I sniffed the env for keys that light the way,
Buttons bloomed from configs — hop, click, away.
One provider or many, we jump where they guide,
A rabbit's little redirect, swift and wide. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: adding OIDC/OAuth SSO support with a reference to the rebased origin.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

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

🧹 Nitpick comments (3)
client/src/components/auth/SocialButtons.tsx (1)

30-54: Narrow error types in catch blocks.

String(error) loses useful context and violates the specific-error guideline. Prefer instanceof Error handling with a fallback.

♻️ Suggested error handling
-    } catch (error) {
-      onError(String(error));
-    }
+    } catch (error) {
+      if (error instanceof Error) {
+        onError(error.message);
+      } else {
+        onError("Unknown error");
+      }
+    }
   }
 
   const handleSocialAuth = async (provider: string) => {
     try {
       await authClient.signIn.social({
@@
-    } catch (error) {
-      onError(String(error));
-    }
+    } catch (error) {
+      if (error instanceof Error) {
+        onError(error.message);
+      } else {
+        onError("Unknown error");
+      }
+    }
   };

As per coding guidelines: "Error handling: Use try/catch blocks with specific error types".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/auth/SocialButtons.tsx` around lines 30 - 54, The catch
blocks in handleOIDCAuth and handleSocialAuth convert errors with String(error)
which discards type/context—update both to check if (error instanceof Error) and
call onError(error.message) (or use error.stack if available), otherwise call
onError(String(error)) as a fallback; ensure you import or reference Error only
and preserve existing behavior for non-Error values.
client/src/lib/configs.ts (1)

7-11: Add runtime validation for the new config fields.

Since /config is external data, please update the runtime validation to include enabledOIDCProviders and enabledSocialProviders so malformed responses fail fast.

♻️ Suggested update with Zod parsing
-import { useQuery } from "@tanstack/react-query";
+import { useQuery } from "@tanstack/react-query";
+import { z } from "zod";
 import { authedFetch } from "../api/utils";
 
-interface Configs {
-  disableSignup: boolean;
-  mapboxToken: string;
-  enabledOIDCProviders: Array<{
-    providerId: string;
-    name: string;
-  }>;
-  enabledSocialProviders: string[];
-}
+const OidcProviderSchema = z.object({
+  providerId: z.string(),
+  name: z.string(),
+});
+
+const ConfigsSchema = z.object({
+  disableSignup: z.boolean(),
+  mapboxToken: z.string(),
+  enabledOIDCProviders: z.array(OidcProviderSchema),
+  enabledSocialProviders: z.array(z.string()),
+});
+
+type Configs = z.infer<typeof ConfigsSchema>;
 
 export function useConfigs() {
   const { data, isLoading, error } = useQuery<Configs>({
     queryKey: ["configs"],
-    queryFn: () => authedFetch<Configs>("/config"),
+    queryFn: async () => ConfigsSchema.parse(await authedFetch<Configs>("/config")),
   });

As per coding guidelines: "Use TypeScript strict mode throughout; use Zod for runtime validation".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/lib/configs.ts` around lines 7 - 11, Add runtime Zod validation
for the new config fields by updating the config schema and parse logic to
include enabledOIDCProviders (array of objects with providerId:string and
name:string) and enabledSocialProviders (string array). Locate the existing Zod
schema (e.g., configSchema or similar) and extend it with z.array(z.object({
providerId: z.string(), name: z.string() })). Also update the runtime
parsing/validation call (e.g., parseConfig, validateConfig, or wherever response
from /config is consumed) to run the new schema.parse or safeParse so malformed
responses throw or are rejected immediately. Ensure TypeScript types are
inferred from the Zod schema so enabledOIDCProviders and enabledSocialProviders
are available throughout the codebase.
server/src/lib/const.ts (1)

501-502: Missing explicit return type annotation on getOIDCProviders.

The inline annotation on providers captures the element shape but the function itself lacks an explicit return type, which weakens TypeScript's strict-mode guarantees at call sites. As per coding guidelines, TypeScript strict typing should be used throughout.

♻️ Proposed refactor
-export const getOIDCProviders = () => {
-  const providers: Array<{ providerId: string; clientId: string; clientSecret: string; discoveryUrl: string; scopes: string[]; name: string }> = [];
+type OIDCProviderConfig = {
+  providerId: string;
+  clientId: string;
+  clientSecret: string;
+  discoveryUrl: string;
+  scopes: string[];
+  name: string;
+};
+
+export const getOIDCProviders = (): OIDCProviderConfig[] => {
+  const providers: OIDCProviderConfig[] = [];

As per coding guidelines: "Use TypeScript with strict typing throughout both client and server."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/lib/const.ts` around lines 501 - 502, The function
getOIDCProviders is missing an explicit return type; update its signature to
declare a strict return type (e.g., an array of the provider shape) rather than
relying solely on the inline providers variable annotation—either add a named
type/interface (e.g., OIDCProvider) and use getOIDCProviders(): OIDCProvider[]
or annotate directly as getOIDCProviders(): Array<{ providerId: string;
clientId: string; clientSecret: string; discoveryUrl: string; scopes: string[];
name: string }>[]; ensure the declared return type matches the providers
variable and update any imports/types as needed so callers receive a fully typed
return.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/src/lib/auth.ts`:
- Around line 4-11: The hard-coded socialProviders array in the createAuthClient
call (authClient / createAuthClient) will drift from server config; replace it
with a small injectable approach: export a factory function (e.g.,
createAuthClientWithProviders) or an initializer that accepts a providers array
and uses that value for the socialProviders option, and update any import sites
to call the factory with the server-provided list (or read from a
bootstrap/global config injected at app startup) so the client list is driven
from configuration rather than a TODO and static array.

In `@server/src/lib/const.ts`:
- Around line 532-554: The loop validates discoveryUrl before confirming
required env vars, causing partially-configured providers to be silently skipped
and making the later "&& discoveryUrl" check redundant; change the order in the
loop so you first check required fields (clientId, clientSecret, discoveryUrl)
and log/warn when any are missing (use the providerId to identify which), then
attempt URL validation with new URL(discoveryUrl) inside try/catch and continue
on invalid URLs; finally remove the redundant "&& discoveryUrl" from the
providers.push condition and only rely on the validated
clientId/clientSecret/discoveryUrl checks before pushing to providers.
- Around line 501-557: Add an explicit return type to getOIDCProviders: define
an interface/type named OIDCProviderConfig matching the object shape ({
providerId: string; clientId: string; clientSecret: string; discoveryUrl:
string; scopes: string[]; name: string }) and annotate the function as
getOIDCProviders(): OIDCProviderConfig[]; update any exports if needed so the
type is available where required (e.g., getConfig.ts) and ensure the
implementation still returns that array shape.

---

Nitpick comments:
In `@client/src/components/auth/SocialButtons.tsx`:
- Around line 30-54: The catch blocks in handleOIDCAuth and handleSocialAuth
convert errors with String(error) which discards type/context—update both to
check if (error instanceof Error) and call onError(error.message) (or use
error.stack if available), otherwise call onError(String(error)) as a fallback;
ensure you import or reference Error only and preserve existing behavior for
non-Error values.

In `@client/src/lib/configs.ts`:
- Around line 7-11: Add runtime Zod validation for the new config fields by
updating the config schema and parse logic to include enabledOIDCProviders
(array of objects with providerId:string and name:string) and
enabledSocialProviders (string array). Locate the existing Zod schema (e.g.,
configSchema or similar) and extend it with z.array(z.object({ providerId:
z.string(), name: z.string() })). Also update the runtime parsing/validation
call (e.g., parseConfig, validateConfig, or wherever response from /config is
consumed) to run the new schema.parse or safeParse so malformed responses throw
or are rejected immediately. Ensure TypeScript types are inferred from the Zod
schema so enabledOIDCProviders and enabledSocialProviders are available
throughout the codebase.

In `@server/src/lib/const.ts`:
- Around line 501-502: The function getOIDCProviders is missing an explicit
return type; update its signature to declare a strict return type (e.g., an
array of the provider shape) rather than relying solely on the inline providers
variable annotation—either add a named type/interface (e.g., OIDCProvider) and
use getOIDCProviders(): OIDCProvider[] or annotate directly as
getOIDCProviders(): Array<{ providerId: string; clientId: string; clientSecret:
string; discoveryUrl: string; scopes: string[]; name: string }>[]; ensure the
declared return type matches the providers variable and update any imports/types
as needed so callers receive a fully typed return.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 828c749 and d7f82f1.

📒 Files selected for processing (9)
  • README.md
  • client/src/components/auth/SocialButtons.tsx
  • client/src/lib/auth.ts
  • client/src/lib/configs.ts
  • docker-compose.cloud.yml
  • docker-compose.yml
  • server/src/api/getConfig.ts
  • server/src/lib/auth.ts
  • server/src/lib/const.ts

Comment thread client/src/lib/auth.ts Outdated
Comment on lines +4 to +11
//TODO: Load socialProviders from configs, but we can't use hooks here
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_BACKEND_URL,
plugins: [adminClient(), organizationClient(), emailOTPClient(), apiKeyClient()],
plugins: [adminClient(), organizationClient(), emailOTPClient(), apiKeyClient(), genericOAuthClient()],
fetchOptions: {
credentials: "include",
},
socialProviders: ["google", "github", "twitter"],
socialProviders: ['google', 'github'],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Track the TODO for dynamic social provider loading.

Leaving this TODO can cause long-term drift between server-configured providers and the client list. Consider a follow-up that injects enabled social providers into this module (e.g., via a factory or config bootstrap).

Would you like me to propose a small refactor to wire the provider list without hooks?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/lib/auth.ts` around lines 4 - 11, The hard-coded socialProviders
array in the createAuthClient call (authClient / createAuthClient) will drift
from server config; replace it with a small injectable approach: export a
factory function (e.g., createAuthClientWithProviders) or an initializer that
accepts a providers array and uses that value for the socialProviders option,
and update any import sites to call the factory with the server-provided list
(or read from a bootstrap/global config injected at app startup) so the client
list is driven from configuration rather than a TODO and static array.

Comment thread server/src/lib/const.ts
Comment on lines +501 to +557
export const getOIDCProviders = () => {
const providers: Array<{ providerId: string; clientId: string; clientSecret: string; discoveryUrl: string; scopes: string[]; name: string }> = [];
const oidcClientIdRegex = /^OIDC_([A-Z0-9_]+)_CLIENT_ID$/;
const oidcClientSecretRegex = /^OIDC_([A-Z0-9_]+)_CLIENT_SECRET$/;
const oidcDiscoveryUrlRegex = /^OIDC_([A-Z0-9_]+)_DISCOVERY_URL$/;
const oidcNameRegex = /^OIDC_([A-Z0-9_]+)_NAME$/;

const providerIds = new Set<string>();

for (const key in process.env) {
const clientIdMatch = key.match(oidcClientIdRegex);
if (clientIdMatch && process.env[key]) {
providerIds.add(clientIdMatch[1]);
}

const clientSecretMatch = key.match(oidcClientSecretRegex);
if (clientSecretMatch && process.env[key]) {
providerIds.add(clientSecretMatch[1]);
}

const discoveryUrlMatch = key.match(oidcDiscoveryUrlRegex);
if (discoveryUrlMatch && process.env[key]) {
providerIds.add(discoveryUrlMatch[1]);
}

const nameMatch = key.match(oidcNameRegex);
if (nameMatch && process.env[key]) {
providerIds.add(nameMatch[1]);
}
}

for (const providerId of providerIds) {
const clientId = process.env[`OIDC_${providerId}_CLIENT_ID`];
const clientSecret = process.env[`OIDC_${providerId}_CLIENT_SECRET`];
const discoveryUrl = process.env[`OIDC_${providerId}_DISCOVERY_URL`];
const name = process.env[`OIDC_${providerId}_NAME`] || `SSO (${providerId.toLowerCase()})`;

try {
new URL(discoveryUrl || "");
} catch {
continue; // Skip invalid URLs
}

if (clientId && clientSecret && discoveryUrl) {
providers.push({
providerId: providerId.toLowerCase(),
clientId,
clientSecret,
discoveryUrl,
scopes: ["openid", "profile", "email"],
name
});
}
}

return providers;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "getConfig.ts" -o -name "getConfig.js"

Repository: rybbit-io/rybbit

Length of output: 89


🏁 Script executed:

grep -r "getOIDCProviders" --include="*.ts" --include="*.tsx" -l

Repository: rybbit-io/rybbit

Length of output: 134


🏁 Script executed:

grep -r "from.*better-auth" --include="*.ts" --include="*.tsx" -A 2 | head -30

Repository: rybbit-io/rybbit

Length of output: 1405


🏁 Script executed:

cat server/src/api/getConfig.ts

Repository: rybbit-io/rybbit

Length of output: 869


🏁 Script executed:

# Check the SocialProviders import validity and the const.ts file structure
head -20 server/src/lib/const.ts

Repository: rybbit-io/rybbit

Length of output: 857


🏁 Script executed:

# Verify the return type of getOIDCProviders is annotated
grep -A 2 "export const getOIDCProviders" server/src/lib/const.ts

Repository: rybbit-io/rybbit

Length of output: 309


🏁 Script executed:

# Double-check that getSocialProviders() is also properly used
grep -A 10 "export const getSocialProviders" server/src/lib/const.ts

Repository: rybbit-io/rybbit

Length of output: 488


Add explicit return type annotation to getOIDCProviders() per TypeScript strict typing guidelines.

getOIDCProviders() lacks an explicit return type. It should be annotated as (): OIDCProviderConfig[] (after defining the type) to comply with strict TypeScript requirements.

Note: The code in getConfig.ts already properly sanitizes the response by mapping the result to only { providerId, name }, so clientSecret is never exposed to the client. The current implementation is secure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/lib/const.ts` around lines 501 - 557, Add an explicit return type
to getOIDCProviders: define an interface/type named OIDCProviderConfig matching
the object shape ({ providerId: string; clientId: string; clientSecret: string;
discoveryUrl: string; scopes: string[]; name: string }) and annotate the
function as getOIDCProviders(): OIDCProviderConfig[]; update any exports if
needed so the type is available where required (e.g., getConfig.ts) and ensure
the implementation still returns that array shape.

Comment thread server/src/lib/const.ts
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds OpenID Connect (OIDC) and OAuth single sign-on (SSO) support for self-hosted instances, rebased from PR #719. It enables dynamic configuration of multiple authentication providers through environment variables and updates both backend and frontend to support these providers alongside existing social authentication.

Changes:

  • Added OIDC provider discovery and configuration through environment variables (OIDC_{PROVIDER}_*)
  • Introduced INTERNAL_AUTHENTICATION_ENABLED flag to optionally disable email/password authentication
  • Updated UI to dynamically display available OIDC and social authentication providers based on server configuration

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
server/src/lib/const.ts Adds getOIDCProviders() function to dynamically discover OIDC providers from environment variables and getSocialProviders() to conditionally configure Google/GitHub OAuth
server/src/lib/auth.ts Integrates genericOAuth plugin, makes emailOTP conditional based on INTERNAL_AUTHENTICATION_ENABLED flag, and uses dynamic social provider configuration
server/src/api/getConfig.ts Exposes enabled OIDC and social providers to the client via config API endpoint
docker-compose.yml Documents OIDC provider environment variable configuration format in comments
docker-compose.cloud.yml Documents OIDC provider environment variable configuration format in comments
client/src/lib/configs.ts Adds TypeScript interfaces for OIDC and social provider configurations returned from server
client/src/lib/auth.ts Adds genericOAuthClient plugin and updates hardcoded social provider list
client/src/components/auth/SocialButtons.tsx Refactors to dynamically render OIDC and social provider buttons based on server configuration instead of IS_CLOUD check
README.md Updates feature comparison table to indicate SSO/OpenID Connect support
Comments suppressed due to low confidence (2)

client/src/components/auth/SocialButtons.tsx:84

  • The "Or continue with email" divider is always displayed even when internal authentication (email/password login) is disabled via INTERNAL_AUTHENTICATION_ENABLED. This can be confusing to users when only SSO/OAuth providers are available. Consider adding INTERNAL_AUTHENTICATION_ENABLED to the config API response and conditionally rendering this divider only when internal authentication is enabled.
      <div className="relative flex items-center text-xs uppercase">
        <div className="flex-1 border-t border-neutral-200 dark:border-neutral-800" />
        <span className="px-3 text-muted-foreground">{t("Or continue with email")}</span>
        <div className="flex-1 border-t border-neutral-200 dark:border-neutral-800" />
      </div>

client/src/components/auth/SocialButtons.tsx:47

  • Inconsistent callbackURL handling between OIDC and social authentication. For OIDC (line 34), callbackURL is excluded when mode is "signup", but for social auth (line 47), callbackURL is always included regardless of mode. This inconsistency could lead to different redirect behaviors. Both should handle callbackURL the same way, or the difference should be documented with a comment explaining why they differ.
        ...(callbackURL && mode !== "signup" ? { callbackURL } : {}),
        // For signup flow, new users should be redirected to the same callbackURL
        ...(mode === "signup" && callbackURL ? { newUserCallbackURL: callbackURL } : {}),
      });
    } catch (error) {
      onError(String(error));
    }
  }

  const handleSocialAuth = async (provider: string) => {
    try {
      await authClient.signIn.social({
        provider,
        ...(callbackURL ? { callbackURL } : {}),

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docker-compose.cloud.yml
# - OIDC_{PROVIDER}_NAME="SSO Provider"
# - OIDC_{PROVIDER}_CLIENT_ID=${OIDC_PROVIDER_CLIENT_ID}
# - OIDC_{PROVIDER}_CLIENT_SECRET=${OIDC_PROVIDER_CLIENT_SECRET}
# - OIDC_{PROVIDER}_DISCOVERY_URL=${OIDC_PROVIDER_DISCOVERY_URL} # e.g. https://accounts.google.com/.well-known/openid-configuration
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

The OIDC configuration comments are missing documentation for the INTERNAL_AUTHENTICATION_ENABLED environment variable, which controls whether email/password authentication is available. This is an important configuration option when using SSO-only authentication. Consider adding a comment like "# - INTERNAL_AUTHENTICATION_ENABLED=false # Set to false to disable email/password login and use SSO only" to help users understand how to configure SSO-only authentication.

Suggested change
# - OIDC_{PROVIDER}_DISCOVERY_URL=${OIDC_PROVIDER_DISCOVERY_URL} # e.g. https://accounts.google.com/.well-known/openid-configuration
# - OIDC_{PROVIDER}_DISCOVERY_URL=${OIDC_PROVIDER_DISCOVERY_URL} # e.g. https://accounts.google.com/.well-known/openid-configuration
# - INTERNAL_AUTHENTICATION_ENABLED=false # Set to false to disable email/password login and use SSO only

Copilot uses AI. Check for mistakes.
Comment thread server/src/lib/auth.ts Outdated
// OIDC back-redirect will only work if backend and frontend are running on the same port.
// For development, it will redirect back to the backend, and you will manually need to go back to the frontend.
//
// This is fine, because there's really no need for yet another seperate env variable for frontend URL just for dev.
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

Spelling error: "seperate" should be "separate".

Suggested change
// This is fine, because there's really no need for yet another seperate env variable for frontend URL just for dev.
// This is fine, because there's really no need for yet another separate env variable for frontend URL just for dev.

Copilot uses AI. Check for mistakes.
Comment thread client/src/lib/auth.ts Outdated
credentials: "include",
},
socialProviders: ["google", "github", "twitter"],
socialProviders: ['google', 'github'],
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

The socialProviders array is hardcoded to ['google', 'github'] even though the server dynamically determines which providers are available based on environment variables. This could cause issues if a self-hosted instance doesn't configure Google or GitHub credentials but configures other social providers. The TODO comment acknowledges this limitation, but it creates a mismatch between what the client expects and what the server provides. Consider moving the authClient creation into a provider component where configs can be accessed, or pass social providers as a configuration parameter that can be set at runtime.

Copilot uses AI. Check for mistakes.
{t("Continue with GitHub")}
</Button>
{configs?.enabledOIDCProviders.map((provider) => (
<Button key={provider.providerId} type="button" onClick={() => handleOIDCAuth(provider.providerId)}>
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

The OIDC provider buttons are missing the height class (h-11) that was present in the original social buttons, which could lead to inconsistent button sizing. Add className="h-11" to maintain consistent button heights across all authentication options.

Suggested change
<Button key={provider.providerId} type="button" onClick={() => handleOIDCAuth(provider.providerId)}>
<Button
key={provider.providerId}
type="button"
className="h-11"
onClick={() => handleOIDCAuth(provider.providerId)}
>

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +78
<Button type="button" onClick={() => handleSocialAuth("google")}>
<SiGoogle />
Google
</Button>
)}

{configs?.enabledSocialProviders.includes("github") && (
<Button type="button" onClick={() => handleSocialAuth("github")}>
<SiGithub />
GitHub
</Button>
)}
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

The social provider buttons (Google and GitHub) are missing the height class (h-11) that was present in the original implementation, which could lead to inconsistent button sizing. Add className="h-11" to maintain consistent button heights across all authentication options.

Copilot uses AI. Check for mistakes.
Comment thread docker-compose.yml
# - OIDC_{PROVIDER}_NAME="SSO Provider"
# - OIDC_{PROVIDER}_CLIENT_ID=${OIDC_PROVIDER_CLIENT_ID}
# - OIDC_{PROVIDER}_CLIENT_SECRET=${OIDC_PROVIDER_CLIENT_SECRET}
# - OIDC_{PROVIDER}_DISCOVERY_URL=${OIDC_PROVIDER_DISCOVERY_URL} # e.g. https://accounts.google.com/.well-known/openid-configuration
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

The OIDC configuration comments are missing documentation for the INTERNAL_AUTHENTICATION_ENABLED environment variable, which controls whether email/password authentication is available. This is an important configuration option when using SSO-only authentication. Consider adding a comment like "# - INTERNAL_AUTHENTICATION_ENABLED=false # Set to false to disable email/password login and use SSO only" to help users understand how to configure SSO-only authentication.

Suggested change
# - OIDC_{PROVIDER}_DISCOVERY_URL=${OIDC_PROVIDER_DISCOVERY_URL} # e.g. https://accounts.google.com/.well-known/openid-configuration
# - OIDC_{PROVIDER}_DISCOVERY_URL=${OIDC_PROVIDER_DISCOVERY_URL} # e.g. https://accounts.google.com/.well-known/openid-configuration
# - INTERNAL_AUTHENTICATION_ENABLED=false # Set to false to disable email/password login and use SSO only

Copilot uses AI. Check for mistakes.
Comment thread server/src/lib/const.ts
Comment on lines +532 to +554
for (const providerId of providerIds) {
const clientId = process.env[`OIDC_${providerId}_CLIENT_ID`];
const clientSecret = process.env[`OIDC_${providerId}_CLIENT_SECRET`];
const discoveryUrl = process.env[`OIDC_${providerId}_DISCOVERY_URL`];
const name = process.env[`OIDC_${providerId}_NAME`] || `SSO (${providerId.toLowerCase()})`;

try {
new URL(discoveryUrl || "");
} catch {
continue; // Skip invalid URLs
}

if (clientId && clientSecret && discoveryUrl) {
providers.push({
providerId: providerId.toLowerCase(),
clientId,
clientSecret,
discoveryUrl,
scopes: ["openid", "profile", "email"],
name
});
}
}
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

The getOIDCProviders function does not validate that all required environment variables are present before adding a provider to the list. If clientId, clientSecret, or discoveryUrl is missing (after the URL validation passes), a provider with undefined values could be added. While line 544 checks if all three exist, a provider could be partially configured if one variable is set but others are missing. Consider adding explicit validation and logging warnings when providers are skipped due to incomplete configuration to aid debugging.

Copilot uses AI. Check for mistakes.
Comment thread client/src/lib/auth.ts Outdated
credentials: "include",
},
socialProviders: ["google", "github", "twitter"],
socialProviders: ['google', 'github'],
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

Inconsistent quote style: The array uses single quotes while the rest of the codebase consistently uses double quotes for strings. Change ['google', 'github'] to ["google", "github"] to maintain consistency.

Suggested change
socialProviders: ['google', 'github'],
socialProviders: ["google", "github"],

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/src/app/invitation/components/login.tsx`:
- Line 26: The code access configs?.enabledOIDCProviders.length can throw if
enabledOIDCProviders is nullish; change both occurrences to safely handle
null/undefined (e.g. use optional chaining or a fallback array) by replacing
configs?.enabledOIDCProviders.length with (configs?.enabledOIDCProviders ??
[]).length or configs?.enabledOIDCProviders?.length, ensuring you update both
the check at the top of the Login component and the repeated check around the
later conditional so enabledOIDCProviders is never dereferenced when
null/undefined.
- Around line 95-101: The SSO button currently hardcodes isLoading={false} and
uses a raw string; add a dedicated SSO loading state (e.g., const [isSSOLoading,
setIsSSOLoading] = useState(false)), update handleSSOLogin to
setIsSSOLoading(true) at start and setIsSSOLoading(false) on any early
returns/errors (or allow redirect to proceed without resetting), and pass
isLoading={isSSOLoading} and disabled={isSSOLoading} to <AuthButton>; replace
the raw "Login with SSO" string with t('auth.loginWithSSO') using
useTranslations(), and add that key to all messages/*.json translation files.
- Around line 25-37: Remove the redundant SSO flow: delete the handleSSOLogin
function (which calls authClient.signIn.oauth2 and sets setError) and remove the
"Login with SSO" button JSX so that SocialButtons handles OIDC providers; ensure
SocialButtons (which maps over enabledOIDCProviders) remains imported and
rendered, and remove any now-unused references to handleSSOLogin or the SSO
button text while keeping existing authClient and setError usages elsewhere
intact.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d7f82f1 and f8827b2.

📒 Files selected for processing (1)
  • client/src/app/invitation/components/login.tsx

Comment thread client/src/app/invitation/components/login.tsx
const { configs } = useConfigs();

const handleSSOLogin = async () => {
if (!configs?.enabledOIDCProviders.length) return;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unsafe property access — enabledOIDCProviders.length can throw if the property is nullish

configs?.enabledOIDCProviders.length short-circuits to undefined when configs itself is nullish, but if configs is defined while enabledOIDCProviders is null or undefined (e.g. during an in-flight config fetch or a partial response), the unguarded .length access throws a TypeError. The same pattern is repeated on line 93.

🛡️ Proposed fix
-    if (!configs?.enabledOIDCProviders.length) return;
+    if (!configs?.enabledOIDCProviders?.length) return;
-        {configs?.enabledOIDCProviders.length ? (
+        {configs?.enabledOIDCProviders?.length ? (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/app/invitation/components/login.tsx` at line 26, The code access
configs?.enabledOIDCProviders.length can throw if enabledOIDCProviders is
nullish; change both occurrences to safely handle null/undefined (e.g. use
optional chaining or a fallback array) by replacing
configs?.enabledOIDCProviders.length with (configs?.enabledOIDCProviders ??
[]).length or configs?.enabledOIDCProviders?.length, ensuring you update both
the check at the top of the Login component and the repeated check around the
later conditional so enabledOIDCProviders is never dereferenced when
null/undefined.

Comment on lines +95 to +101
isLoading={false}
type="button"
variant="default"
onClick={handleSSOLogin}
>
Login with SSO
</AuthButton>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing SSO-specific loading state and hardcoded UI string violates i18n

Two issues in the SSO button:

  1. isLoading={false} is hardcoded — handleSSOLogin is async and the OAuth2 redirect can take a noticeable moment. There is no loading feedback to the user, and nothing prevents double-clicks.
  2. "Login with SSO" is a raw English string that bypasses t(), violating the project's i18n requirement.

As per coding guidelines: "Use next-intl's 'useTranslations()' hook for i18n".

♻️ Proposed fix

Add a dedicated loading state for SSO:

  const [isLoading, setIsLoading] = useState(false);
+ const [isSSOLoading, setIsSSOLoading] = useState(false);

Update handleSSOLogin:

  const handleSSOLogin = async () => {
    if (!configs?.enabledOIDCProviders?.length) return;
    const provider = configs.enabledOIDCProviders[0];
+   setIsSSOLoading(true);
    try {
      await authClient.signIn.oauth2({
        providerId: provider.providerId,
        callbackURL,
      });
    } catch (err) {
      setError(String(err));
+   } finally {
+     setIsSSOLoading(false);
    }
  };

Update the button:

-           <AuthButton
-             isLoading={false}
-             type="button"
-             variant="default"
-             onClick={handleSSOLogin}
-           >
-             Login with SSO
-           </AuthButton>
+           <AuthButton
+             isLoading={isSSOLoading}
+             type="button"
+             variant="default"
+             onClick={handleSSOLogin}
+           >
+             {t("Login with SSO")}
+           </AuthButton>

Remember to add the key to all translation files (messages/en.json, messages/de.json, etc.).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/app/invitation/components/login.tsx` around lines 95 - 101, The
SSO button currently hardcodes isLoading={false} and uses a raw string; add a
dedicated SSO loading state (e.g., const [isSSOLoading, setIsSSOLoading] =
useState(false)), update handleSSOLogin to setIsSSOLoading(true) at start and
setIsSSOLoading(false) on any early returns/errors (or allow redirect to proceed
without resetting), and pass isLoading={isSSOLoading} and
disabled={isSSOLoading} to <AuthButton>; replace the raw "Login with SSO" string
with t('auth.loginWithSSO') using useTranslations(), and add that key to all
messages/*.json translation files.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
client/src/app/signup/page.tsx (1)

196-251: ⚠️ Potential issue | 🟡 Minor

No actionable UI when internalAuthEnabled is false and no providers are configured

When configs.internalAuthEnabled is false and SocialButtons renders nothing (zero OIDC and social providers), step 1 displays only the "Already have an account?" link with no way to proceed. Consider a fallback message, or guard this page-level to surface a clear configuration error.

💡 Suggested guard
+              {!configs?.internalAuthEnabled &&
+                !configs?.enabledOIDCProviders?.length &&
+                !configs?.enabledSocialProviders?.length && (
+                  <p className="text-sm text-muted-foreground text-center">
+                    {t("No authentication methods are currently configured.")}
+                  </p>
+                )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/app/signup/page.tsx` around lines 196 - 251, The page can become
unusable when configs.internalAuthEnabled is false and SocialButtons renders no
providers; update the signup page render logic to detect "no auth methods
available" (check configs.internalAuthEnabled and whether SocialButtons would
render any providers/OIDC) and show a clear fallback UI or error message instead
of the empty form area; locate the relevant JSX around SocialButtons,
configs?.internalAuthEnabled, and the "Already have an account?" link and either
render a configuration warning/CTA (e.g., "No authentication providers
configured — contact admin" with a disabled Continue) or redirect/guard the
route so users aren’t stuck on step 1.
♻️ Duplicate comments (3)
client/src/app/invitation/components/login.tsx (3)

97-106: Hardcoded isLoading={false} and untranslated "Login with SSO" string

handleSSOLogin is async but its button never reflects a loading state, allowing double-clicks. "Login with SSO" bypasses t(), violating the project's i18n requirement.

As per coding guidelines: "Use next-intl's 'useTranslations()' hook for i18n".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/app/invitation/components/login.tsx` around lines 97 - 106, The
SSO button uses a hardcoded isLoading={false} and a hardcoded label; update the
AuthButton rendering that checks configs?.enabledOIDCProviders to track the
async state of handleSSOLogin and use next-intl translations: add a local
loading boolean (e.g., ssoLoading) that is set true before await in
handleSSOLogin and false in finally, pass isLoading={ssoLoading} to the
AuthButton, and replace the literal "Login with SSO" with t('loginWithSSO')
(using useTranslations() at component top) so the label is localized.

25-37: Redundant handleSSOLoginSocialButtons already handles all OIDC providers

SocialButtons (imported at line 11, rendered at line 71) already maps over enabledOIDCProviders and renders one button per provider. handleSSOLogin hard-codes only the first provider, so when multiple providers exist, the first one gets two buttons (one from SocialButtons and one from the explicit SSO button).

Delete handleSSOLogin (lines 25–37) and the corresponding AuthButton block (lines 97–106) entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/app/invitation/components/login.tsx` around lines 25 - 37, The
handleSSOLogin function duplicates SocialButtons by hardcoding the first OIDC
provider and causing duplicate buttons; remove the handleSSOLogin declaration
and also remove the AuthButton usage that invokes it (the AuthButton block
rendering the SSO button) so SocialButtons is the sole source of provider
buttons; search for the handleSSOLogin identifier and the AuthButton rendering
that references it and delete both to avoid the duplicate-first-provider
behavior.

26-26: Unsafe enabledOIDCProviders.length — missing optional chain on the array

configs?.enabledOIDCProviders.length short-circuits configs to undefined when nullish, but if configs is defined while enabledOIDCProviders is undefined (absent from the API response), the unguarded .length throws a TypeError. Same pattern on line 97.

-    if (!configs?.enabledOIDCProviders.length) return;
+    if (!configs?.enabledOIDCProviders?.length) return;
-        {configs?.enabledOIDCProviders.length ? (
+        {configs?.enabledOIDCProviders?.length ? (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/app/invitation/components/login.tsx` at line 26, The code reads
configs?.enabledOIDCProviders.length which will throw if configs exists but
enabledOIDCProviders is undefined; change these checks to safely handle a
missing array by using either optional chaining on the array (e.g.,
configs?.enabledOIDCProviders?.length) or an explicit array check (e.g.,
Array.isArray(configs.enabledOIDCProviders) &&
configs.enabledOIDCProviders.length) wherever you reference enabledOIDCProviders
(the configs variable and enabledOIDCProviders identifier appear in this file,
including the second occurrence around the later conditional).
🧹 Nitpick comments (1)
client/src/app/login/page.tsx (1)

77-87: React 19 Strict Mode fires this effect twice in development

React 19 Strict Mode simulates mount→unmount→remount for every useEffect. Because authClient.signIn.oauth2() initiates a browser-level OAuth redirect (external navigation), a double-invocation will open the provider's authorization page twice in development, which is disorienting. Add a ref guard or an executed variable to prevent the second call:

🛡️ Proposed guard
+  const redirectInitiated = useRef(false);
   useEffect(() => {
     if (
       !isLoadingConfigs &&
       configs &&
       !configs.internalAuthEnabled &&
-      configs.enabledOIDCProviders?.length === 1
+      configs.enabledOIDCProviders?.length === 1 &&
+      !redirectInitiated.current
     ) {
+      redirectInitiated.current = true;
       const provider = configs.enabledOIDCProviders[0];
       authClient.signIn.oauth2({ providerId: provider.providerId, callbackURL: "/" })
         .catch(err => setError(String(err)));
     }
   }, [configs, isLoadingConfigs]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/app/login/page.tsx` around lines 77 - 87, The useEffect in
page.tsx triggers authClient.signIn.oauth2() twice under React 19 Strict Mode;
add a persistent ref guard (e.g., executedRef) or an `executed` variable to
short-circuit the effect after the first invocation so the redirect is only
initiated once. Update the effect that checks
isLoadingConfigs/configs/internalAuthEnabled/enabledOIDCProviders and, before
calling authClient.signIn.oauth2({ providerId, callbackURL: "/" }), check and
set executedRef.current (or equivalent) so subsequent mount/remounts skip the
oauth2 call; keep the existing catch handler that calls setError(String(err)).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/src/app/login/page.tsx`:
- Around line 77-87: Guard the unsafe access to configs.enabledOIDCProviders by
checking configs.enabledOIDCProviders && configs.enabledOIDCProviders.length (or
use optional chaining configs.enabledOIDCProviders?.length) inside the useEffect
before reading .length to avoid a TypeError; add a new state flag (e.g.,
isAutoRedirecting) that you set to true immediately before calling
authClient.signIn.oauth2(...) and set to false in the catch handler (or finally)
so the UI can show a loading/spinner; update the component render to display a
spinner and "Redirecting to SSO…" message when isAutoRedirecting is true so
users see feedback during the redirect.

In `@client/src/lib/configs.ts`:
- Around line 8-12: Make enabledOIDCProviders and enabledSocialProviders
optional in the configs type (change their declarations to be optional) and
update all consumers to guard against undefined by using double
optional-chaining or nullish coalescing; specifically adjust usages in
login/page.tsx and invitation/components/login.tsx (where .length is accessed)
to use configs?.enabledOIDCProviders?.length or (configs?.enabledOIDCProviders
?? []).length and similarly for enabledSocialProviders so runtime undefined
responses from the HTTP fetch no longer throw.

---

Outside diff comments:
In `@client/src/app/signup/page.tsx`:
- Around line 196-251: The page can become unusable when
configs.internalAuthEnabled is false and SocialButtons renders no providers;
update the signup page render logic to detect "no auth methods available" (check
configs.internalAuthEnabled and whether SocialButtons would render any
providers/OIDC) and show a clear fallback UI or error message instead of the
empty form area; locate the relevant JSX around SocialButtons,
configs?.internalAuthEnabled, and the "Already have an account?" link and either
render a configuration warning/CTA (e.g., "No authentication providers
configured — contact admin" with a disabled Continue) or redirect/guard the
route so users aren’t stuck on step 1.

---

Duplicate comments:
In `@client/src/app/invitation/components/login.tsx`:
- Around line 97-106: The SSO button uses a hardcoded isLoading={false} and a
hardcoded label; update the AuthButton rendering that checks
configs?.enabledOIDCProviders to track the async state of handleSSOLogin and use
next-intl translations: add a local loading boolean (e.g., ssoLoading) that is
set true before await in handleSSOLogin and false in finally, pass
isLoading={ssoLoading} to the AuthButton, and replace the literal "Login with
SSO" with t('loginWithSSO') (using useTranslations() at component top) so the
label is localized.
- Around line 25-37: The handleSSOLogin function duplicates SocialButtons by
hardcoding the first OIDC provider and causing duplicate buttons; remove the
handleSSOLogin declaration and also remove the AuthButton usage that invokes it
(the AuthButton block rendering the SSO button) so SocialButtons is the sole
source of provider buttons; search for the handleSSOLogin identifier and the
AuthButton rendering that references it and delete both to avoid the
duplicate-first-provider behavior.
- Line 26: The code reads configs?.enabledOIDCProviders.length which will throw
if configs exists but enabledOIDCProviders is undefined; change these checks to
safely handle a missing array by using either optional chaining on the array
(e.g., configs?.enabledOIDCProviders?.length) or an explicit array check (e.g.,
Array.isArray(configs.enabledOIDCProviders) &&
configs.enabledOIDCProviders.length) wherever you reference enabledOIDCProviders
(the configs variable and enabledOIDCProviders identifier appear in this file,
including the second occurrence around the later conditional).

---

Nitpick comments:
In `@client/src/app/login/page.tsx`:
- Around line 77-87: The useEffect in page.tsx triggers
authClient.signIn.oauth2() twice under React 19 Strict Mode; add a persistent
ref guard (e.g., executedRef) or an `executed` variable to short-circuit the
effect after the first invocation so the redirect is only initiated once. Update
the effect that checks
isLoadingConfigs/configs/internalAuthEnabled/enabledOIDCProviders and, before
calling authClient.signIn.oauth2({ providerId, callbackURL: "/" }), check and
set executedRef.current (or equivalent) so subsequent mount/remounts skip the
oauth2 call; keep the existing catch handler that calls setError(String(err)).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f8827b2 and 5d701b6.

📒 Files selected for processing (5)
  • client/src/app/invitation/components/login.tsx
  • client/src/app/login/page.tsx
  • client/src/app/signup/page.tsx
  • client/src/lib/configs.ts
  • server/src/api/getConfig.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/src/api/getConfig.ts

Comment on lines +77 to +87
useEffect(() => {
if (!isLoadingConfigs && configs && !configs.internalAuthEnabled && configs.enabledOIDCProviders.length === 1) {
const provider = configs.enabledOIDCProviders[0];
authClient.signIn.oauth2({
providerId: provider.providerId,
callbackURL: "/",
}).catch(err => {
setError(String(err));
});
}
}, [configs, isLoadingConfigs]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Unsafe enabledOIDCProviders.length access and missing loading indicator in auto-redirect effect

Two issues in this useEffect:

  1. Potential TypeError (line 78): Even though configs is confirmed truthy, configs.enabledOIDCProviders can be undefined if the server omits the field (see the Configs interface — the field is declared required but is API-sourced). The unguarded .length will throw at runtime in that case.

  2. No loading/spinner state during redirect: Once the condition is met, authClient.signIn.oauth2() is called silently. The user sees the otherwise-empty login page (only SocialButtons and the signup link are visible) with no indication that a redirect is in progress. This creates a confusing UX gap.

🛡️ Proposed fix
+  const [isAutoRedirecting, setIsAutoRedirecting] = useState(false);

   useEffect(() => {
-    if (!isLoadingConfigs && configs && !configs.internalAuthEnabled && configs.enabledOIDCProviders.length === 1) {
+    if (!isLoadingConfigs && configs && !configs.internalAuthEnabled && configs.enabledOIDCProviders?.length === 1) {
       const provider = configs.enabledOIDCProviders[0];
+      setIsAutoRedirecting(true);
       authClient.signIn.oauth2({
         providerId: provider.providerId,
         callbackURL: "/",
       }).catch(err => {
         setError(String(err));
+        setIsAutoRedirecting(false);
       });
     }
   }, [configs, isLoadingConfigs]);

Then render a loading indicator when isAutoRedirecting is true (e.g., replace the form area with a spinner and a "Redirecting to SSO…" message).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (!isLoadingConfigs && configs && !configs.internalAuthEnabled && configs.enabledOIDCProviders.length === 1) {
const provider = configs.enabledOIDCProviders[0];
authClient.signIn.oauth2({
providerId: provider.providerId,
callbackURL: "/",
}).catch(err => {
setError(String(err));
});
}
}, [configs, isLoadingConfigs]);
const [isAutoRedirecting, setIsAutoRedirecting] = useState(false);
useEffect(() => {
if (!isLoadingConfigs && configs && !configs.internalAuthEnabled && configs.enabledOIDCProviders?.length === 1) {
const provider = configs.enabledOIDCProviders[0];
setIsAutoRedirecting(true);
authClient.signIn.oauth2({
providerId: provider.providerId,
callbackURL: "/",
}).catch(err => {
setError(String(err));
setIsAutoRedirecting(false);
});
}
}, [configs, isLoadingConfigs]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/app/login/page.tsx` around lines 77 - 87, Guard the unsafe access
to configs.enabledOIDCProviders by checking configs.enabledOIDCProviders &&
configs.enabledOIDCProviders.length (or use optional chaining
configs.enabledOIDCProviders?.length) inside the useEffect before reading
.length to avoid a TypeError; add a new state flag (e.g., isAutoRedirecting)
that you set to true immediately before calling authClient.signIn.oauth2(...)
and set to false in the catch handler (or finally) so the UI can show a
loading/spinner; update the component render to display a spinner and
"Redirecting to SSO…" message when isAutoRedirecting is true so users see
feedback during the redirect.

Comment thread client/src/lib/configs.ts
Comment on lines +8 to +12
enabledOIDCProviders: Array<{
providerId: string;
name: string;
}>;
enabledSocialProviders: string[];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

enabledOIDCProviders and enabledSocialProviders should be optional to match the API contract

Both are declared as required non-nullable fields, but they are populated from an HTTP fetch. Any response that omits these fields (e.g., older server, partial response, network truncation) will cause configs.enabledOIDCProviders to be undefined at runtime while TypeScript believes it is a defined array. Consumers in login/page.tsx (line 78) and invitation/components/login.tsx (lines 26, 97) then call .length on the field without the second optional-chain (enabledOIDCProviders?.length), which will throw a TypeError.

🛡️ Proposed fix
 interface Configs {
   disableSignup: boolean;
   internalAuthEnabled: boolean;
   mapboxToken: string;
-  enabledOIDCProviders: Array<{
+  enabledOIDCProviders?: Array<{
     providerId: string;
     name: string;
   }>;
-  enabledSocialProviders: string[];
+  enabledSocialProviders?: string[];
 }

All consumers must then use configs?.enabledOIDCProviders?.length (double optional-chain) or a nullish-coalescing fallback.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
enabledOIDCProviders: Array<{
providerId: string;
name: string;
}>;
enabledSocialProviders: string[];
interface Configs {
disableSignup: boolean;
internalAuthEnabled: boolean;
mapboxToken: string;
enabledOIDCProviders?: Array<{
providerId: string;
name: string;
}>;
enabledSocialProviders?: string[];
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/lib/configs.ts` around lines 8 - 12, Make enabledOIDCProviders and
enabledSocialProviders optional in the configs type (change their declarations
to be optional) and update all consumers to guard against undefined by using
double optional-chaining or nullish coalescing; specifically adjust usages in
login/page.tsx and invitation/components/login.tsx (where .length is accessed)
to use configs?.enabledOIDCProviders?.length or (configs?.enabledOIDCProviders
?? []).length and similarly for enabledSocialProviders so runtime undefined
responses from the HTTP fetch no longer throw.

@acvigue
Copy link
Copy Markdown

acvigue commented Feb 27, 2026

thank you for doing this, it looks great. I will close my PR here in a bit, as it seems there's a common theme of no one ever reviewing anything.

@reazndev
Copy link
Copy Markdown
Author

thank you for doing this, it looks great. I will close my PR here in a bit, as it seems there's a common theme of no one ever reviewing anything.

The maintainer said in one GH issue that he does not plan on adding oauth2 to Rybbit so I doubt it would ever get approved so I also didn't care for writing any docs that would be needed for a merge.

…iple languages, ensuring consistency in terminology for custom events and team management. Added missing translations for various terms and improved user experience by maintaining uniformity in the user interface language.
acvigue and others added 4 commits April 17, 2026 18:24
This commit adds the ability to disable email/password login and
prioritize SSO/OIDC authentication methods for organizations.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

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

⚠️ Outside diff range comments (3)
client/src/components/auth/SocialButtons.tsx (2)

81-85: ⚠️ Potential issue | 🟡 Minor

"Or continue with email" divider renders even when internal auth is disabled.

The divider is unconditionally rendered whenever there is at least one OIDC/social provider, but in client/src/app/login/page.tsx and client/src/app/signup/components/AccountStep.tsx the email/password form is now gated on configs.internalAuthEnabled. When internal auth is disabled, users see an "Or continue with email" separator pointing to empty space below the SSO buttons.

Consider gating the divider on configs.internalAuthEnabled, or lifting it out of SocialButtons entirely and rendering it from the parent only when the email form will be shown.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/auth/SocialButtons.tsx` around lines 81 - 85, The "Or
continue with email" divider inside SocialButtons is always rendered causing an
empty separator when internal auth is disabled; update SocialButtons to only
render that divider when internal auth will actually be shown—either read
configs.internalAuthEnabled inside SocialButtons and gate the divider on that
flag, or add a boolean prop (e.g., showEmailDivider) to SocialButtons and have
the callers in login/page.tsx and AccountStep.tsx pass
configs.internalAuthEnabled so the divider is only rendered when internal auth
is enabled.

31-55: ⚠️ Potential issue | 🟡 Minor

Inconsistent callbackURL handling between OIDC and social flows.

handleOIDCAuth sets callbackURL only when mode !== "signup", while handleSocialAuth sets callbackURL unconditionally (even in signup mode). In practice, for the signup flow (callbackURL="/signup?step=2", mode="signup"):

  • OIDC: only newUserCallbackURL is set → existing users land on the default redirect.
  • Social (Google/GitHub): both callbackURL and newUserCallbackURL are set to /signup?step=2 → existing users also get pushed through the signup step flow.

Please confirm the intended behavior and align the two handlers. If the OIDC version is correct, handleSocialAuth should also skip callbackURL when mode === "signup".

🔧 Proposed alignment
   const handleSocialAuth = async (provider: string) => {
     try {
       await authClient.signIn.social({
         provider,
-        ...(callbackURL ? { callbackURL } : {}),
-        // For signup flow, new users should also be redirected to the callbackURL
+        ...(callbackURL && mode !== "signup" ? { callbackURL } : {}),
+        // For signup flow, new users should be redirected to the same callbackURL
         ...(mode === "signup" && callbackURL ? { newUserCallbackURL: callbackURL } : {}),
       });
     } catch (error) {
       onError(String(error));
     }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/auth/SocialButtons.tsx` around lines 31 - 55,
handleOIDCAuth and handleSocialAuth disagree on passing callbackURL in signup
mode; update handleSocialAuth so it matches handleOIDCAuth by only including
callbackURL when callbackURL exists AND mode !== "signup" (but still include
newUserCallbackURL when mode === "signup"); modify the authClient.signIn.social
call inside handleSocialAuth to conditionally spread { callbackURL } only if
callbackURL && mode !== "signup", keeping the existing newUserCallbackURL
behavior for mode === "signup".
client/src/app/login/page.tsx (1)

99-153: ⚠️ Potential issue | 🟡 Minor

Dead-end UI when internal auth is disabled and no SSO providers are configured.

If configs.internalAuthEnabled === false and configs.enabledOIDCProviders/enabledSocialProviders are both empty, SocialButtons returns null (line 21–29 of SocialButtons.tsx) and the form on lines 103–153 is hidden. The user would be left with a blank page and only the "Sign up" link. Consider surfacing a friendly error/empty state for this misconfiguration, or prevent the server from reporting internalAuthEnabled: false unless at least one SSO provider is registered.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/app/login/page.tsx` around lines 99 - 153, The login page
currently renders nothing when internal auth is disabled and SocialButtons
returns null (because configs.enabledOIDCProviders and
configs.enabledSocialProviders are empty), causing a dead-end; update the login
page (page.tsx) to detect this misconfiguration (configs.internalAuthEnabled ===
false && configs.enabledOIDCProviders?.length === 0 &&
configs.enabledSocialProviders?.length === 0) and render a friendly
empty-state/error panel (e.g., a message with guidance and a contact/admin link)
in place of the form/SocialButtons, or alternatively show a fallback CTA (e.g.,
"No sign-in methods configured") so users are not left on a blank page;
reference SocialButtons, configs.internalAuthEnabled, enabledOIDCProviders,
enabledSocialProviders, and the Login page render logic to implement this check
and UI fallback.
♻️ Duplicate comments (1)
client/src/app/login/page.tsx (1)

76-87: ⚠️ Potential issue | 🟠 Major

Unsafe access to enabledOIDCProviders and missing redirect-in-progress UI.

Two concerns with the new auto-redirect effect:

  1. Potential TypeError at line 78: configs.enabledOIDCProviders.length will throw if the server response omits the field (e.g., during a rolling deploy where the client is newer than the server). Guard with optional chaining: configs.enabledOIDCProviders?.length === 1.
  2. No visual feedback during redirect: Between the effect firing and the browser navigating away, the user sees the bare login UI (which currently renders nothing else when internalAuthEnabled is false). Consider an isAutoRedirecting state that swaps the form area for a spinner/"Redirecting to SSO…" message, and set it back to false in the .catch so errors are actionable.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/app/login/page.tsx` around lines 76 - 87, Guard access to
configs.enabledOIDCProviders with optional chaining and add a redirecting UI
state: update the useEffect that auto-redirects (useEffect) to check
configs.enabledOIDCProviders?.length === 1 instead of accessing .length
directly, and introduce an isAutoRedirecting state (with setter
setIsAutoRedirecting) that you set to true immediately before calling
authClient.signIn.oauth2({ providerId: provider.providerId, callbackURL: "/" })
and set back to false in the .catch handler (and/or .finally) where you already
call setError(String(err)); render a spinner/"Redirecting to SSO…" when
isAutoRedirecting is true so the user sees feedback while the redirect is in
progress.
🧹 Nitpick comments (3)
client/src/app/settings/account/page.tsx (1)

16-21: Using flex on the description <p> may hurt wrapping/RTL behavior.

Turning this paragraph into a flex container makes the translated sentence and the ExternalLink two independent flex items rather than inline text. On narrow viewports or with longer translations (e.g., many non-English locales), the leading text won't wrap around the link the way inline flow would, and items-center gap-1 can produce odd vertical alignment once the sentence wraps internally.

Consider keeping the paragraph as regular inline flow and just adding a space before the link, matching the pattern used in NoData.tsx and TraitsExplorer.tsx.

♻️ Suggested change
-        <p className="text-neutral-500 dark:text-neutral-400 flex items-center gap-1">
-          {t("Manage your personal account settings")}
-          <ExternalLink href="https://www.rybbit.com/docs/account-settings">
-            {t("Learn more")}
-          </ExternalLink>
-        </p>
+        <p className="text-neutral-500 dark:text-neutral-400">
+          {t("Manage your personal account settings")}{" "}
+          <ExternalLink href="https://www.rybbit.com/docs/account-settings">
+            {t("Learn more")}
+          </ExternalLink>
+        </p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/app/settings/account/page.tsx` around lines 16 - 21, The paragraph
currently renders as a flex container which breaks inline wrapping and RTL
behavior; remove the flex-specific classes ("flex items-center gap-1") from the
<p> (leave the text color classes) so it stays in normal inline flow, and ensure
there's an explicit space between the translated sentence and the ExternalLink
(e.g., by adding a literal space or {" "} before <ExternalLink>) to match the
pattern used in NoData.tsx and TraitsExplorer.tsx.
server/src/lib/const.ts (1)

633-653: Simplify env scan — one loop per key instead of four regex matches.

Since every branch does the same thing (add capture group 1 to providerIds), a single combined regex avoids redundant work and is easier to maintain.

♻️ Proposed refactor
-  const oidcClientIdRegex = /^OIDC_([A-Z0-9_]+)_CLIENT_ID$/;
-  const oidcClientSecretRegex = /^OIDC_([A-Z0-9_]+)_CLIENT_SECRET$/;
-  const oidcDiscoveryUrlRegex = /^OIDC_([A-Z0-9_]+)_DISCOVERY_URL$/;
-  const oidcNameRegex = /^OIDC_([A-Z0-9_]+)_NAME$/;
-
-  const providerIds = new Set<string>();
-
-  for (const key in process.env) {
-    const clientIdMatch = key.match(oidcClientIdRegex);
-    if (clientIdMatch && process.env[key]) {
-      providerIds.add(clientIdMatch[1]);
-    }
-
-    const clientSecretMatch = key.match(oidcClientSecretRegex);
-    if (clientSecretMatch && process.env[key]) {
-      providerIds.add(clientSecretMatch[1]);
-    }
-
-    const discoveryUrlMatch = key.match(oidcDiscoveryUrlRegex);
-    if (discoveryUrlMatch && process.env[key]) {
-      providerIds.add(discoveryUrlMatch[1]);
-    }
-
-    const nameMatch = key.match(oidcNameRegex);
-    if (nameMatch && process.env[key]) {
-      providerIds.add(nameMatch[1]);
-    }
-  }
+  const oidcKeyRegex = /^OIDC_([A-Z0-9_]+)_(?:CLIENT_ID|CLIENT_SECRET|DISCOVERY_URL|NAME)$/;
+  const providerIds = new Set<string>();
+  for (const [key, value] of Object.entries(process.env)) {
+    const match = key.match(oidcKeyRegex);
+    if (match && value) providerIds.add(match[1]);
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/lib/const.ts` around lines 633 - 653, The loop that scans
process.env performs four separate regex matches for the same outcome; replace
it with a single combined regex (or sequential test using a single matching
step) that captures the provider id in group 1 to avoid redundant checks. Update
the loop that currently references oidcClientIdRegex, oidcClientSecretRegex,
oidcDiscoveryUrlRegex, and oidcNameRegex to use a combined pattern (or one match
call) and then, if matched and process.env[key] is truthy, add the captured id
to providerIds (preserving the existing providerIds.add(match[1]) behavior).
client/src/components/auth/SocialButtons.tsx (1)

4-4: SiGoogle is imported but never used.

The Google button now uses <Image src="/crawlers/Google.svg" /> instead of SiGoogle. Remove the unused import to keep the module clean.

-import { SiGoogle, SiGithub, SiOpenid } from "@icons-pack/react-simple-icons";
+import { SiGithub, SiOpenid } from "@icons-pack/react-simple-icons";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/auth/SocialButtons.tsx` at line 4, Remove the unused
SiGoogle import from the SocialButtons module: locate the import line importing
SiGoogle, SiGithub, SiOpenid (symbol: SiGoogle) in SocialButtons.tsx and delete
SiGoogle from that import so only used icons (e.g., SiGithub, SiOpenid) remain;
ensure the file still compiles and no other references to SiGoogle exist.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/messages/cs.json`:
- Line 870: The Czech translation key "Ix3wt3" is empty; add an appropriate
Czech string value (for example "Zjisti více o týmech") for the "Ix3wt3" key so
the inline link shown by t("Learn more about teams") in the teams settings
layout renders non-empty; update the cs.json entry for "Ix3wt3" with the chosen
Czech text.

In `@client/messages/de.json`:
- Line 870: The translation key "Ix3wt3" is empty in client/messages/de.json but
is used by t("Learn more about teams") in
client/src/app/settings/teams/layout.tsx; add an appropriate German translation
(e.g., "Erfahren Sie mehr über Teams") as the value for "Ix3wt3" in
client/messages/de.json so the Teams settings UI renders correctly for German
users, then rebuild/localize assets if your workflow requires it.

In `@client/messages/it.json`:
- Line 870: The translation key "Ix3wt3" is empty in the Italian locale and
should mirror the English "Learn more about teams"; update the value for
"Ix3wt3" in the Italian messages to an appropriate Italian string (e.g., "Scopri
di più sui team" or "Ulteriori informazioni sui team") so the teams settings
link displays correctly for Italian users.

In `@client/messages/ja.json`:
- Line 870: The JSON key "Ix3wt3" in the Japanese locale is an empty string
causing the "Learn more about teams" message to render blank; update the ja.json
entry for "Ix3wt3" to a proper Japanese translation of "Learn more about teams"
(or delete the "Ix3wt3" key so the i18n fallback to the default locale is used)
and commit that change so next-intl will display the intended text.

In `@client/messages/pl.json`:
- Line 870: Add the missing Polish translation for the message key "Ix3wt3"
(which corresponds to "Learn more about teams" in en.json and is used as the
visible text for the ExternalLink in settings/teams/layout.tsx) by replacing the
empty string value with an appropriate Polish translation (e.g., "Dowiedz się
więcej o zespołach") so the link is visible to Polish users.

In `@client/src/app/signup/components/AccountStep.tsx`:
- Around line 38-45: AccountStep currently defaults internalAuthEnabled to true
but when configs?.internalAuthEnabled === false and no external providers are
configured the UI shows only the heading and "Log in" link with no signup path;
update the AccountStep component to detect this misconfiguration by checking
configs?.internalAuthEnabled and whether SocialButtons would render any
providers (use the same provider-detection logic as SocialButtons or expose a
prop/utility), and when internal auth is disabled AND no external providers
exist render an informational/misconfiguration message (matching the
style/resolution used in login/page.tsx) that explains signup is unavailable and
directs the user/admin to correct the auth provider configuration or contact
support. Include references to internalAuthEnabled, configs, SocialButtons and
AccountStep to locate and implement the change.

In `@client/src/components/auth/SocialButtons.tsx`:
- Around line 25-29: The code in SocialButtons.tsx assumes
configs.enabledOIDCProviders and configs.enabledSocialProviders are always
arrays and uses .length (and elsewhere may use .includes), which will throw if
the API omits those keys; make the check defensive by treating those fields as
optional arrays and defaulting to an empty array (e.g., use optional chaining
and/or fallback to [] when evaluating hasProviders and any .includes usages) so
the component returns null safely when no providers exist even if the server
omits those properties.

In `@server/src/lib/const.ts`:
- Line 9: Document the default-on behavior of INTERNAL_AUTHENTICATION_ENABLED in
server/.env.example and server/README.md noting that only the exact string
"false" disables it, and add a runtime safeguard in server/src/lib/auth.ts to
prevent complete lockout: check INTERNAL_AUTHENTICATION_ENABLED and the
configured external auth providers and, if INTERNAL_AUTHENTICATION_ENABLED ===
"false" and no OIDC/social providers or other auth methods are configured, throw
an explicit error or revert to a safe default; update the validation to run at
startup (e.g., in the same init path that uses emailOTP and emailAndPassword)
and reference INTERNAL_AUTHENTICATION_ENABLED, emailOTP, and emailAndPassword so
maintainers can find and adjust the logic.

---

Outside diff comments:
In `@client/src/app/login/page.tsx`:
- Around line 99-153: The login page currently renders nothing when internal
auth is disabled and SocialButtons returns null (because
configs.enabledOIDCProviders and configs.enabledSocialProviders are empty),
causing a dead-end; update the login page (page.tsx) to detect this
misconfiguration (configs.internalAuthEnabled === false &&
configs.enabledOIDCProviders?.length === 0 &&
configs.enabledSocialProviders?.length === 0) and render a friendly
empty-state/error panel (e.g., a message with guidance and a contact/admin link)
in place of the form/SocialButtons, or alternatively show a fallback CTA (e.g.,
"No sign-in methods configured") so users are not left on a blank page;
reference SocialButtons, configs.internalAuthEnabled, enabledOIDCProviders,
enabledSocialProviders, and the Login page render logic to implement this check
and UI fallback.

In `@client/src/components/auth/SocialButtons.tsx`:
- Around line 81-85: The "Or continue with email" divider inside SocialButtons
is always rendered causing an empty separator when internal auth is disabled;
update SocialButtons to only render that divider when internal auth will
actually be shown—either read configs.internalAuthEnabled inside SocialButtons
and gate the divider on that flag, or add a boolean prop (e.g.,
showEmailDivider) to SocialButtons and have the callers in login/page.tsx and
AccountStep.tsx pass configs.internalAuthEnabled so the divider is only rendered
when internal auth is enabled.
- Around line 31-55: handleOIDCAuth and handleSocialAuth disagree on passing
callbackURL in signup mode; update handleSocialAuth so it matches handleOIDCAuth
by only including callbackURL when callbackURL exists AND mode !== "signup" (but
still include newUserCallbackURL when mode === "signup"); modify the
authClient.signIn.social call inside handleSocialAuth to conditionally spread {
callbackURL } only if callbackURL && mode !== "signup", keeping the existing
newUserCallbackURL behavior for mode === "signup".

---

Duplicate comments:
In `@client/src/app/login/page.tsx`:
- Around line 76-87: Guard access to configs.enabledOIDCProviders with optional
chaining and add a redirecting UI state: update the useEffect that
auto-redirects (useEffect) to check configs.enabledOIDCProviders?.length === 1
instead of accessing .length directly, and introduce an isAutoRedirecting state
(with setter setIsAutoRedirecting) that you set to true immediately before
calling authClient.signIn.oauth2({ providerId: provider.providerId, callbackURL:
"/" }) and set back to false in the .catch handler (and/or .finally) where you
already call setError(String(err)); render a spinner/"Redirecting to SSO…" when
isAutoRedirecting is true so the user sees feedback while the redirect is in
progress.

---

Nitpick comments:
In `@client/src/app/settings/account/page.tsx`:
- Around line 16-21: The paragraph currently renders as a flex container which
breaks inline wrapping and RTL behavior; remove the flex-specific classes ("flex
items-center gap-1") from the <p> (leave the text color classes) so it stays in
normal inline flow, and ensure there's an explicit space between the translated
sentence and the ExternalLink (e.g., by adding a literal space or {" "} before
<ExternalLink>) to match the pattern used in NoData.tsx and TraitsExplorer.tsx.

In `@client/src/components/auth/SocialButtons.tsx`:
- Line 4: Remove the unused SiGoogle import from the SocialButtons module:
locate the import line importing SiGoogle, SiGithub, SiOpenid (symbol: SiGoogle)
in SocialButtons.tsx and delete SiGoogle from that import so only used icons
(e.g., SiGithub, SiOpenid) remain; ensure the file still compiles and no other
references to SiGoogle exist.

In `@server/src/lib/const.ts`:
- Around line 633-653: The loop that scans process.env performs four separate
regex matches for the same outcome; replace it with a single combined regex (or
sequential test using a single matching step) that captures the provider id in
group 1 to avoid redundant checks. Update the loop that currently references
oidcClientIdRegex, oidcClientSecretRegex, oidcDiscoveryUrlRegex, and
oidcNameRegex to use a combined pattern (or one match call) and then, if matched
and process.env[key] is truthy, add the captured id to providerIds (preserving
the existing providerIds.add(match[1]) behavior).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 694a3006-7372-42ee-9752-c1bd6b7c0f58

📥 Commits

Reviewing files that changed from the base of the PR and between d0c4a87 and 4f3993f.

⛔ Files ignored due to path filters (2)
  • client/package-lock.json is excluded by !**/package-lock.json
  • server/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (31)
  • README.md
  • client/messages/cs.json
  • client/messages/de.json
  • client/messages/en.json
  • client/messages/es.json
  • client/messages/fr.json
  • client/messages/it.json
  • client/messages/ja.json
  • client/messages/ko.json
  • client/messages/pl.json
  • client/messages/pt.json
  • client/messages/zh.json
  • client/src/app/[site]/components/Header/NoData.tsx
  • client/src/app/[site]/funnels/page.tsx
  • client/src/app/[site]/goals/page.tsx
  • client/src/app/[site]/users/components/TraitsExplorer.tsx
  • client/src/app/invitation/components/login.tsx
  • client/src/app/login/page.tsx
  • client/src/app/settings/account/page.tsx
  • client/src/app/settings/teams/layout.tsx
  • client/src/app/signup/components/AccountStep.tsx
  • client/src/app/signup/page.tsx
  • client/src/components/ExternalLink.tsx
  • client/src/components/auth/SocialButtons.tsx
  • client/src/lib/auth.ts
  • client/src/lib/configs.ts
  • docker-compose.cloud.yml
  • docker-compose.yml
  • server/src/api/getConfig.ts
  • server/src/lib/auth.ts
  • server/src/lib/const.ts
✅ Files skipped from review due to trivial changes (13)
  • README.md
  • client/src/app/[site]/goals/page.tsx
  • client/src/app/settings/teams/layout.tsx
  • docker-compose.yml
  • client/messages/ko.json
  • client/messages/zh.json
  • client/messages/es.json
  • client/messages/fr.json
  • client/messages/en.json
  • client/messages/pt.json
  • docker-compose.cloud.yml
  • client/src/lib/configs.ts
  • client/src/app/invitation/components/login.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • client/src/lib/auth.ts
  • server/src/api/getConfig.ts
  • server/src/lib/auth.ts

Comment thread client/messages/cs.json
"+WnNru": "",
"DNeuzb": "",
"f7uwif": "",
"Ix3wt3": "",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Czech translation missing for Ix3wt3.

Like several other keys in this file, Ix3wt3 has no translation and will render as empty in the Teams settings page (used by t("Learn more about teams") in client/src/app/settings/teams/layout.tsx). Consider providing a Czech value, e.g. "Zjisti více o týmech", to avoid a blank inline link label.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/messages/cs.json` at line 870, The Czech translation key "Ix3wt3" is
empty; add an appropriate Czech string value (for example "Zjisti více o
týmech") for the "Ix3wt3" key so the inline link shown by t("Learn more about
teams") in the teams settings layout renders non-empty; update the cs.json entry
for "Ix3wt3" with the chosen Czech text.

Comment thread client/messages/de.json
"+WnNru": "Sind Sie sicher, dass Sie das Team \"{name}\" löschen möchten? Websites, die diesem Team zugewiesen sind, werden nicht mehr zugewiesen und für alle Mitglieder sichtbar.",
"DNeuzb": "Teams",
"f7uwif": "Organisieren Sie Websites in Teams, um zu steuern, welche Mitglieder darauf zugreifen können.",
"Ix3wt3": "",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing German translation for actively used key Ix3wt3.

This key is referenced by t("Learn more about teams") in client/src/app/settings/teams/layout.tsx (line 27) and will render as an empty string in the Teams settings UI for German users, breaking the sentence that introduces the "Learn more about teams" link.

🛠️ Suggested fix
-  "Ix3wt3": "",
+  "Ix3wt3": "Mehr über Teams erfahren",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"Ix3wt3": "",
"Ix3wt3": "Mehr über Teams erfahren",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/messages/de.json` at line 870, The translation key "Ix3wt3" is empty
in client/messages/de.json but is used by t("Learn more about teams") in
client/src/app/settings/teams/layout.tsx; add an appropriate German translation
(e.g., "Erfahren Sie mehr über Teams") as the value for "Ix3wt3" in
client/messages/de.json so the Teams settings UI renders correctly for German
users, then rebuild/localize assets if your workflow requires it.

Comment thread client/messages/it.json
"+WnNru": "Sei sicuro di voler eliminare il team \"{name}\"? I siti assegnati a questo team diventeranno non assegnati e visibili a tutti i membri.",
"DNeuzb": "Team",
"f7uwif": "Organizza i siti in team per controllare quali membri possono accedervi.",
"Ix3wt3": "",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing Italian translation for Ix3wt3.

The key Ix3wt3 maps to "Learn more about teams" in client/messages/en.json, but here it's an empty string. Italian users will see a blank link in the teams settings page.

🌐 Proposed translation
-  "Ix3wt3": "",
+  "Ix3wt3": "Scopri di più sui team",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"Ix3wt3": "",
"Ix3wt3": "Scopri di più sui team",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/messages/it.json` at line 870, The translation key "Ix3wt3" is empty
in the Italian locale and should mirror the English "Learn more about teams";
update the value for "Ix3wt3" in the Italian messages to an appropriate Italian
string (e.g., "Scopri di più sui team" or "Ulteriori informazioni sui team") so
the teams settings link displays correctly for Italian users.

Comment thread client/messages/ja.json
"+WnNru": "チーム\"{name}\"を削除してよろしいですか?このチームに割り当てられたサイトは未割り当てになり、すべてのメンバーに表示されます。",
"DNeuzb": "チーム",
"f7uwif": "サイトをチームに整理して、どのメンバーがアクセスできるかを制御します。",
"Ix3wt3": "",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Empty placeholder for Ix3wt3 — confirm this is intentional.

"Ix3wt3": "" leaves the "Learn more about teams" string untranslated for Japanese users (and will likely render as empty text in the UI rather than falling back to English, depending on how next-intl resolves missing values). If a Japanese translation isn't ready, consider removing the key so the default/fallback kicks in, or adding a translation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/messages/ja.json` at line 870, The JSON key "Ix3wt3" in the Japanese
locale is an empty string causing the "Learn more about teams" message to render
blank; update the ja.json entry for "Ix3wt3" to a proper Japanese translation of
"Learn more about teams" (or delete the "Ix3wt3" key so the i18n fallback to the
default locale is used) and commit that change so next-intl will display the
intended text.

Comment thread client/messages/pl.json
"+WnNru": "Czy na pewno chcesz usunąć zespół \"{name}\"? Witryny przypisane do tego zespołu staną się nieprzypisane i widoczne dla wszystkich członków.",
"DNeuzb": "Zespoły",
"f7uwif": "Organizuj witryny w zespoły, aby kontrolować, którzy członkowie mogą do nich uzyskać dostęp.",
"Ix3wt3": "",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing Polish translation for Ix3wt3 results in empty link text.

The key Ix3wt3 maps to "Learn more about teams" in client/messages/en.json and is rendered as the visible text of the ExternalLink in client/src/app/settings/teams/layout.tsx (line 27). With an empty string here, Polish users will see a blank/invisible link next to the teams description.

🌐 Proposed translation
-  "Ix3wt3": "",
+  "Ix3wt3": "Dowiedz się więcej o zespołach",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"Ix3wt3": "",
"Ix3wt3": "Dowiedz się więcej o zespołach",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/messages/pl.json` at line 870, Add the missing Polish translation for
the message key "Ix3wt3" (which corresponds to "Learn more about teams" in
en.json and is used as the visible text for the ExternalLink in
settings/teams/layout.tsx) by replacing the empty string value with an
appropriate Polish translation (e.g., "Dowiedz się więcej o zespołach") so the
link is visible to Polish users.

Comment on lines +38 to +45
const internalAuthEnabled = configs?.internalAuthEnabled ?? true;

return (
<div>
<h2 className="text-2xl font-semibold mb-4">{t("Signup")}</h2>
<div className="space-y-4">
<SocialButtons onError={setError} callbackURL="/signup?step=2" mode="signup" />
<AuthInput
id="email"
label={t("Email")}
type="email"
placeholder="email@example.com"
required
value={email}
onChange={e => setEmail(e.target.value)}
/>
<AuthInput
id="password"
label={t("Password")}
type="password"
placeholder="••••••••"
required
value={password}
onChange={e => setPassword(e.target.value)}
/>
{IS_CLOUD && (
<Turnstile
onSuccess={token => setTurnstileToken(token)}
onError={() => setTurnstileToken("")}
onExpire={() => setTurnstileToken("")}
className="flex justify-center"
/>
{internalAuthEnabled && (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Default to true is a sensible fallback, but consider the no-provider edge case.

internalAuthEnabled ?? true correctly treats "configs still loading / field missing" as enabled. However, when configs?.internalAuthEnabled === false and no OIDC/social providers are configured, this step renders only the heading and the "Log in" link — no way to sign up. Consider rendering an informational message for that misconfiguration, matching whatever resolution is chosen in login/page.tsx.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/app/signup/components/AccountStep.tsx` around lines 38 - 45,
AccountStep currently defaults internalAuthEnabled to true but when
configs?.internalAuthEnabled === false and no external providers are configured
the UI shows only the heading and "Log in" link with no signup path; update the
AccountStep component to detect this misconfiguration by checking
configs?.internalAuthEnabled and whether SocialButtons would render any
providers (use the same provider-detection logic as SocialButtons or expose a
prop/utility), and when internal auth is disabled AND no external providers
exist render an informational/misconfiguration message (matching the
style/resolution used in login/page.tsx) that explains signup is unavailable and
directs the user/admin to correct the auth provider configuration or contact
support. Include references to internalAuthEnabled, configs, SocialButtons and
AccountStep to locate and implement the change.

Comment on lines +25 to +29
const hasProviders = configs.enabledOIDCProviders.length > 0 || configs.enabledSocialProviders.length > 0;

if (!hasProviders) {
return null;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Defensive null check for API-sourced arrays.

configs.enabledOIDCProviders and configs.enabledSocialProviders come from the server /api/config response. If the backend ever omits them (e.g., older server version against newer client during rolling deploy), .length/.includes will throw. Consider optional chaining or a default.

-  const hasProviders = configs.enabledOIDCProviders.length > 0 || configs.enabledSocialProviders.length > 0;
+  const hasProviders =
+    (configs.enabledOIDCProviders?.length ?? 0) > 0 ||
+    (configs.enabledSocialProviders?.length ?? 0) > 0;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const hasProviders = configs.enabledOIDCProviders.length > 0 || configs.enabledSocialProviders.length > 0;
if (!hasProviders) {
return null;
}
const hasProviders =
(configs.enabledOIDCProviders?.length ?? 0) > 0 ||
(configs.enabledSocialProviders?.length ?? 0) > 0;
if (!hasProviders) {
return null;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/auth/SocialButtons.tsx` around lines 25 - 29, The code
in SocialButtons.tsx assumes configs.enabledOIDCProviders and
configs.enabledSocialProviders are always arrays and uses .length (and elsewhere
may use .includes), which will throw if the API omits those keys; make the check
defensive by treating those fields as optional arrays and defaulting to an empty
array (e.g., use optional chaining and/or fallback to [] when evaluating
hasProviders and any .includes usages) so the component returns null safely when
no providers exist even if the server omits those properties.

Comment thread server/src/lib/const.ts
export const IS_CLOUD = process.env.CLOUD === "true";
export const DISABLE_SIGNUP = process.env.DISABLE_SIGNUP === "true";
export const DISABLE_TELEMETRY = process.env.DISABLE_TELEMETRY === "true";
export const INTERNAL_AUTHENTICATION_ENABLED = process.env.INTERNAL_AUTHENTICATION_ENABLED !== "false";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP -C3 '\bINTERNAL_AUTHENTICATION_ENABLED\b'

Repository: rybbit-io/rybbit

Length of output: 3256


🏁 Script executed:

fd -type f -name '.env*' -o -name 'README*' | head -20

Repository: rybbit-io/rybbit

Length of output: 230


🏁 Script executed:

wc -l server/src/lib/auth.ts

Repository: rybbit-io/rybbit

Length of output: 86


🏁 Script executed:

fd -type file \( -name '.env*' -o -name 'README*' \)

Repository: rybbit-io/rybbit

Length of output: 230


🏁 Script executed:

cat -n server/src/lib/auth.ts

Repository: rybbit-io/rybbit

Length of output: 11474


🏁 Script executed:

find . -maxdepth 2 -type f \( -name '.env*' -o -name 'README*' \)

Repository: rybbit-io/rybbit

Length of output: 218


🏁 Script executed:

rg -n 'getOIDCProviders|getSocialProviders' server/src/lib/const.ts -A 10

Repository: rybbit-io/rybbit

Length of output: 1153


🏁 Script executed:

cat server/.env.example

Repository: rybbit-io/rybbit

Length of output: 837


🏁 Script executed:

cat server/README.md

Repository: rybbit-io/rybbit

Length of output: 1276


Document INTERNAL_AUTHENTICATION_ENABLED default behavior and verify no lockout risk.

The default-on behavior using !== "false" is reasonable for backward compatibility, but needs attention:

  1. Missing documentation: INTERNAL_AUTHENTICATION_ENABLED is not documented in server/.env.example or server/README.md. Add it with clarification that only the exact string "false" disables it.

  2. Lockout risk confirmed: In server/src/lib/auth.ts, when INTERNAL_AUTHENTICATION_ENABLED is false, both emailAndPassword (line 98) and emailOTP (lines 70–75) are disabled. If no OIDC providers or social credentials are configured, operators can lock themselves out with zero authentication methods available. Add a validation or safeguard to prevent this scenario.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/lib/const.ts` at line 9, Document the default-on behavior of
INTERNAL_AUTHENTICATION_ENABLED in server/.env.example and server/README.md
noting that only the exact string "false" disables it, and add a runtime
safeguard in server/src/lib/auth.ts to prevent complete lockout: check
INTERNAL_AUTHENTICATION_ENABLED and the configured external auth providers and,
if INTERNAL_AUTHENTICATION_ENABLED === "false" and no OIDC/social providers or
other auth methods are configured, throw an explicit error or revert to a safe
default; update the validation to run at startup (e.g., in the same init path
that uses emailOTP and emailAndPassword) and reference
INTERNAL_AUTHENTICATION_ENABLED, emailOTP, and emailAndPassword so maintainers
can find and adjust the logic.

@reazndev reazndev closed this Apr 18, 2026
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.

4 participants