fix issue with new auth providers not being shown in login page

This commit is contained in:
msukkari 2025-05-30 11:12:00 -07:00
parent 97a2a3efac
commit a2e06266db
8 changed files with 177 additions and 46 deletions

View file

@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Fixed
- Fixed issue where new oauth providers weren't being display in the login page
## [4.0.1] - 2025-05-28 ## [4.0.1] - 2025-05-28
### Fixed ### Fixed

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="167.117" height="150.658" viewBox="0 0 44.216 39.861"><path d="m88.61 138.456 5.716-9.865 23.018-.004 5.686 9.965.007 19.932-5.691 9.957-23.012.008-5.782-9.965z" style="display:inline;fill:#4d4d4d;fill-opacity:1;stroke-width:.264583" transform="translate(-82.815 -128.588)"/><path d="M88.552 158.481h10.375l-5.699-10.041 4.634-9.982-9.252-.002-5.795 10.065" style="fill:#ededed;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729" transform="translate(-82.815 -128.588)"/><path d="M102.073 158.481h7.582l6.706-9.773-6.589-10.156h-8.921l-5.373 9.814z" style="fill:#e0e0e0;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729" transform="translate(-82.815 -128.588)"/><path d="m82.815 148.52 5.738 9.964h10.374l-5.636-9.93z" style="fill:#acacac;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729" transform="translate(-82.815 -128.588)"/><path d="m95.589 148.522 6.484 9.963h7.582l6.601-9.959z" style="fill:#9e9e9e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729" transform="translate(-82.815 -128.588)"/><path d="m98.157 148.529-1.958.569-1.877-.572 7.667-13.288 1.918 3.316" style="fill:#00b8e3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729" transform="translate(-82.815 -128.588)"/><path d="m103.9 158.482-1.909 3.332-5.093-5.487-2.58-7.797v-.004h3.838" style="fill:#33c6e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729" transform="translate(-82.815 -128.588)"/><path d="M94.322 148.526h-.003v.003l-1.918 3.322-1.925-3.307 1.952-3.386 5.728-9.92h3.834" style="fill:#008aaa;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729" transform="translate(-82.815 -128.588)"/><path d="M115.42 158.481h11.611l-.007-19.93h-11.605z" style="fill:#d4d4d4;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729" transform="translate(-82.815 -128.588)"/><path d="M115.42 148.554v9.93h11.59v-9.93z" style="fill:#919191;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729" transform="translate(-82.815 -128.588)"/><path d="M101.992 161.817h-3.836l-5.755-9.966 1.918-3.321z" style="fill:#00b8e3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729" transform="translate(-82.815 -128.588)"/><path d="m117.333 148.526-7.669 13.289c-.705-1.036-1.913-3.331-1.913-3.331l5.753-9.959z" style="fill:#008aaa;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729" transform="translate(-82.815 -128.588)"/><path d="m113.495 161.815-3.831-.001 7.67-13.288 1.917-3.317 1.921 3.34m-3.839-.023h-3.828l-5.755-9.973 1.905-3.314 4.658 5.922z" style="fill:#00b8e3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729" transform="translate(-82.815 -128.588)"/><path d="M119.25 145.205v.003l-1.917 3.318-7.677-13.286 3.841.002z" style="fill:#33c6e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729" transform="translate(-82.815 -128.588)"/></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="uuid-f8d4d392-7c12-4bd9-baff-66fbf7814b91" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
<path d="m3.802,14.032c.388.242,1.033.511,1.715.511.621,0,1.198-.18,1.676-.487,0,0,.001,0,.002-.001l1.805-1.128v4.073c-.286,0-.574-.078-.824-.234l-4.374-2.734Z" fill="#225086"/>
<path d="m7.853,1.507L.353,9.967c-.579.654-.428,1.642.323,2.111,0,0,2.776,1.735,3.126,1.954.388.242,1.033.511,1.715.511.621,0,1.198-.18,1.676-.487,0,0,.001,0,.002-.001l1.805-1.128-4.364-2.728,4.365-4.924V1s0,0,0,0c-.424,0-.847.169-1.147.507Z" fill="#6df"/>
<polygon points="4.636 10.199 4.688 10.231 9 12.927 9.001 12.927 9.001 12.927 9.001 5.276 9 5.275 4.636 10.199" fill="#cbf8ff"/>
<path d="m17.324,12.078c.751-.469.902-1.457.323-2.111l-4.921-5.551c-.397-.185-.842-.291-1.313-.291-.925,0-1.752.399-2.302,1.026l-.109.123h0s4.364,4.924,4.364,4.924h0s0,0,0,0l-4.365,2.728v4.073c.287,0,.573-.078.823-.234l7.5-4.688Z" fill="#074793"/>
<path d="m9.001,1v4.275s.109-.123.109-.123c.55-.627,1.377-1.026,2.302-1.026.472,0,.916.107,1.313.291l-2.579-2.909c-.299-.338-.723-.507-1.146-.507Z" fill="#0294e4"/>
<polygon points="13.365 10.199 13.365 10.199 13.365 10.199 9.001 5.276 9.001 12.926 13.365 10.199" fill="#96bcc2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36">
<path d="M19.8,.26l-.74,9.12c-.35-.04-.7-.06-1.06-.06-.45,0-.89,.03-1.32,.1l-.42-4.42c-.01-.14,.1-.26,.24-.26h.75l-.36-4.47c-.01-.14,.1-.26,.23-.26h2.45c.14,0,.25,.12,.23,.26h0Zm-6.18,.45c-.04-.13-.18-.21-.31-.16l-2.3,.84c-.13,.05-.19,.2-.13,.32l1.87,4.08-.71,.26c-.13,.05-.19,.2-.13,.32l1.91,4.01c.69-.38,1.44-.67,2.23-.85L13.63,.71ZM7.98,3.25l5.29,7.46c-.67,.44-1.28,.96-1.8,1.56l-3.17-3.12c-.1-.1-.09-.26,.01-.35l.58-.48-3.15-3.19c-.1-.1-.09-.26,.02-.35l1.87-1.57c.11-.09,.26-.07,.34,.04ZM3.54,7.57c-.11-.08-.27-.04-.34,.08l-1.22,2.12c-.07,.12-.02,.27,.1,.33l4.06,1.92-.38,.65c-.07,.12-.02,.28,.11,.33l4.04,1.85c.29-.75,.68-1.45,1.16-2.08L3.54,7.57ZM.55,13.33c.02-.14,.16-.22,.29-.19l8.85,2.31c-.23,.75-.36,1.54-.38,2.36l-4.43-.36c-.14-.01-.24-.14-.21-.28l.13-.74-4.47-.42c-.14-.01-.23-.14-.21-.28l.42-2.41h0Zm-.33,5.98c-.14,.01-.23,.14-.21,.28l.43,2.41c.02,.14,.16,.22,.29,.19l4.34-1.13,.13,.74c.02,.14,.16,.22,.29,.19l4.28-1.18c-.25-.74-.41-1.53-.45-2.34L.21,19.31Zm1.42,6.34c-.07-.12-.02-.27,.1-.33l8.26-3.92c.31,.74,.73,1.43,1.23,2.05l-3.62,2.58c-.11,.08-.27,.05-.34-.07l-.38-.66-3.69,2.55c-.11,.08-.27,.04-.34-.08l-1.23-2.12Zm10.01-1.72l-6.43,6.51c-.1,.1-.09,.26,.02,.35l1.88,1.57c.11,.09,.26,.07,.34-.04l2.6-3.66,.58,.49c.11,.09,.27,.07,.35-.05l2.52-3.66c-.68-.42-1.31-.93-1.85-1.51Zm-1.27,10.45c-.13-.05-.19-.2-.13-.32l3.81-8.32c.7,.36,1.46,.63,2.25,.78l-1.12,4.3c-.03,.13-.18,.21-.31,.16l-.71-.26-1.19,4.33c-.04,.13-.18,.21-.31,.16l-2.3-.84h0Zm6.56-7.75l-.74,9.12c-.01,.14,.1,.26,.23,.26h2.45c.14,0,.25-.12,.23-.26l-.36-4.47h.75c.14,0,.25-.12,.24-.26l-.42-4.42c-.43,.07-.87,.1-1.32,.1-.36,0-.71-.02-1.06-.07h0ZM25.76,1.94c.06-.13,0-.27-.13-.32l-2.3-.84c-.13-.05-.27,.03-.31,.16l-1.19,4.33-.71-.26c-.13-.05-.27,.03-.31,.16l-1.12,4.3c.8,.16,1.55,.43,2.25,.78L25.76,1.94h0Zm5.02,3.63l-6.43,6.51c-.54-.58-1.16-1.09-1.85-1.51l2.52-3.66c.08-.11,.24-.14,.35-.05l.58,.49,2.6-3.66c.08-.11,.24-.13,.34-.04l1.88,1.57c.11,.09,.11,.25,.02,.35Zm3.48,5.12c.13-.06,.17-.21,.1-.33l-1.23-2.12c-.07-.12-.23-.15-.34-.08l-3.69,2.55-.38-.65c-.07-.12-.23-.16-.34-.07l-3.62,2.58c.5,.62,.91,1.31,1.23,2.05l8.26-3.92Zm1.3,3.32l.42,2.41c.02,.14-.07,.26-.21,.28l-9.11,.85c-.04-.82-.2-1.6-.45-2.34l4.28-1.18c.13-.04,.27,.05,.29,.19l.13,.74,4.34-1.13c.13-.03,.27,.05,.29,.19h0Zm-.41,8.85c.13,.03,.27-.05,.29-.19l.42-2.41c.02-.14-.07-.26-.21-.28l-4.47-.42,.13-.74c.02-.14-.07-.26-.21-.28l-4.43-.36c-.02,.82-.15,1.61-.38,2.36l8.85,2.31h0Zm-2.36,5.5c-.07,.12-.23,.15-.34,.08l-7.53-5.2c.48-.63,.87-1.33,1.16-2.08l4.04,1.85c.13,.06,.18,.21,.11,.33l-.38,.65,4.06,1.92c.12,.06,.17,.21,.1,.33l-1.22,2.12h0Zm-10.07-3.07l5.29,7.46c.08,.11,.24,.13,.34,.04l1.87-1.57c.11-.09,.11-.25,.02-.35l-3.15-3.19,.58-.48c.11-.09,.11-.25,.01-.35l-3.17-3.12c-.53,.6-1.13,1.13-1.8,1.56h0Zm-.05,10.16c-.13,.05-.27-.03-.31-.16l-2.42-8.82c.79-.18,1.54-.47,2.23-.85l1.91,4.01c.06,.13,0,.28-.13,.32l-.71,.26,1.87,4.08c.06,.13,0,.27-.13,.32l-2.3,.84h0Z" fill="#191919" fill-rule="evenodd"></path>
</svg>

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -1,12 +1,11 @@
'use client'; 'use client';
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import googleLogo from "@/public/google.svg";
import Image from "next/image"; import Image from "next/image";
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
import { Fragment, useCallback, useMemo } from "react"; import { Fragment, useCallback, useMemo } from "react";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { cn, getCodeHostIcon } from "@/lib/utils"; import { cn, getAuthProviderInfo } from "@/lib/utils";
import { MagicLinkForm } from "./magicLinkForm"; import { MagicLinkForm } from "./magicLinkForm";
import { CredentialsForm } from "./credentialsForm"; import { CredentialsForm } from "./credentialsForm";
import { SourcebotLogo } from "@/app/components/sourcebotLogo"; import { SourcebotLogo } from "@/app/components/sourcebotLogo";
@ -22,15 +21,10 @@ const PRIVACY_POLICY_URL = "https://sourcebot.dev/privacy";
interface LoginFormProps { interface LoginFormProps {
callbackUrl?: string; callbackUrl?: string;
error?: string; error?: string;
enabledMethods: { providers: Array<{ id: string; name: string }>;
github: boolean;
google: boolean;
magicLink: boolean;
credentials: boolean;
}
} }
export const LoginForm = ({ callbackUrl, error, enabledMethods }: LoginFormProps) => { export const LoginForm = ({ callbackUrl, error, providers }: LoginFormProps) => {
const captureEvent = useCaptureEvent(); const captureEvent = useCaptureEvent();
const onSignInWithOauth = useCallback((provider: string) => { const onSignInWithOauth = useCallback((provider: string) => {
signIn(provider, { redirectTo: callbackUrl ?? "/" }); signIn(provider, { redirectTo: callbackUrl ?? "/" });
@ -50,6 +44,33 @@ export const LoginForm = ({ callbackUrl, error, enabledMethods }: LoginFormProps
} }
}, [error]); }, [error]);
// Separate OAuth providers from special auth methods
const oauthProviders = providers.filter(p =>
!["credentials", "nodemailer"].includes(p.id)
);
const hasCredentials = providers.some(p => p.id === "credentials");
const hasMagicLink = providers.some(p => p.id === "nodemailer");
// Helper function to get the correct analytics event name
const getLoginEventName = (providerId: string) => {
switch (providerId) {
case "github":
return "wa_login_with_github" as const;
case "google":
return "wa_login_with_google" as const;
case "gitlab":
return "wa_login_with_gitlab" as const;
case "okta":
return "wa_login_with_okta" as const;
case "keycloak":
return "wa_login_with_keycloak" as const;
case "microsoft-entra-id":
return "wa_login_with_microsoft_entra_id" as const;
default:
return "wa_login_with_github" as const; // fallback
}
};
return ( return (
<div className="flex flex-col items-center justify-center w-full"> <div className="flex flex-col items-center justify-center w-full">
<div className="mb-6 flex flex-col items-center"> <div className="mb-6 flex flex-col items-center">
@ -71,36 +92,28 @@ export const LoginForm = ({ callbackUrl, error, enabledMethods }: LoginFormProps
)} )}
<DividerSet <DividerSet
elements={[ elements={[
...(enabledMethods.github || enabledMethods.google ? [ ...(oauthProviders.length > 0 ? [
<> <div key="oauth-providers" className="w-full space-y-3">
{enabledMethods.github && ( {oauthProviders.map((provider) => {
<ProviderButton const providerInfo = getAuthProviderInfo(provider.id);
key="github" return (
name="GitHub" <ProviderButton
logo={getCodeHostIcon("github")!} key={provider.id}
onClick={() => { name={providerInfo.displayName}
captureEvent("wa_login_with_github", {}); logo={providerInfo.icon}
onSignInWithOauth("github") onClick={() => {
}} captureEvent(getLoginEventName(provider.id), {});
/> onSignInWithOauth(provider.id);
)} }}
{enabledMethods.google && ( />
<ProviderButton );
key="google" })}
name="Google" </div>
logo={{ src: googleLogo }}
onClick={() => {
captureEvent("wa_login_with_google", {});
onSignInWithOauth("google")
}}
/>
)}
</>
] : []), ] : []),
...(enabledMethods.magicLink ? [ ...(hasMagicLink ? [
<MagicLinkForm key="magic-link" callbackUrl={callbackUrl} /> <MagicLinkForm key="magic-link" callbackUrl={callbackUrl} />
] : []), ] : []),
...(enabledMethods.credentials ? [ ...(hasCredentials ? [
<CredentialsForm key="credentials" callbackUrl={callbackUrl} /> <CredentialsForm key="credentials" callbackUrl={callbackUrl} />
] : []) ] : [])
]} ]}
@ -120,7 +133,7 @@ const ProviderButton = ({
className, className,
}: { }: {
name: string; name: string;
logo: { src: string, className?: string }; logo: { src: string, className?: string } | null;
onClick: () => void; onClick: () => void;
className?: string; className?: string;
}) => { }) => {

View file

@ -20,11 +20,11 @@ export default async function Login({ searchParams }: LoginProps) {
} }
const providers = getProviders(); const providers = getProviders();
const providerMap = providers const providerData = providers
.map((provider) => { .map((provider) => {
if (typeof provider === "function") { if (typeof provider === "function") {
const providerData = provider() const providerInfo = provider()
return { id: providerData.id, name: providerData.name } return { id: providerInfo.id, name: providerInfo.name }
} else { } else {
return { id: provider.id, name: provider.name } return { id: provider.id, name: provider.name }
} }
@ -36,12 +36,7 @@ export default async function Login({ searchParams }: LoginProps) {
<LoginForm <LoginForm
callbackUrl={searchParams.callbackUrl} callbackUrl={searchParams.callbackUrl}
error={searchParams.error} error={searchParams.error}
enabledMethods={{ providers={providerData}
github: providerMap.some(provider => provider.id === "github"),
google: providerMap.some(provider => provider.id === "google"),
magicLink: providerMap.some(provider => provider.id === "nodemailer"),
credentials: providerMap.some(provider => provider.id === "credentials"),
}}
/> />
</div> </div>
<Footer /> <Footer />

View file

@ -216,6 +216,10 @@ export type PosthogEventMap = {
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
wa_login_with_github: {}, wa_login_with_github: {},
wa_login_with_google: {}, wa_login_with_google: {},
wa_login_with_gitlab: {},
wa_login_with_okta: {},
wa_login_with_keycloak: {},
wa_login_with_microsoft_entra_id: {},
wa_login_with_magic_link: {}, wa_login_with_magic_link: {},
wa_login_with_credentials: {}, wa_login_with_credentials: {},
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////

View file

@ -6,6 +6,10 @@ import giteaLogo from "@/public/gitea.svg";
import gerritLogo from "@/public/gerrit.svg"; import gerritLogo from "@/public/gerrit.svg";
import bitbucketLogo from "@/public/bitbucket.svg"; import bitbucketLogo from "@/public/bitbucket.svg";
import gitLogo from "@/public/git.svg"; import gitLogo from "@/public/git.svg";
import googleLogo from "@/public/google.svg";
import oktaLogo from "@/public/okta.svg";
import keycloakLogo from "@/public/keycloak.svg";
import microsoftLogo from "@/public/microsoft_entra.svg";
import { ServiceError } from "./serviceError"; import { ServiceError } from "./serviceError";
import { StatusCodes } from "http-status-codes"; import { StatusCodes } from "http-status-codes";
import { ErrorCode } from "./errorCodes"; import { ErrorCode } from "./errorCodes";
@ -44,6 +48,105 @@ export type CodeHostType =
"bitbucket-server" | "bitbucket-server" |
"generic-git-host"; "generic-git-host";
export type AuthProviderType =
"github" |
"gitlab" |
"google" |
"okta" |
"keycloak" |
"microsoft-entra-id" |
"credentials" |
"nodemailer";
type AuthProviderInfo = {
id: string;
name: string;
displayName: string;
icon: { src: string; className?: string } | null;
}
export const getAuthProviderInfo = (providerId: string): AuthProviderInfo => {
switch (providerId) {
case "github":
return {
id: "github",
name: "GitHub",
displayName: "GitHub",
icon: {
src: githubLogo,
className: "dark:invert",
},
};
case "gitlab":
return {
id: "gitlab",
name: "GitLab",
displayName: "GitLab",
icon: {
src: gitlabLogo,
},
};
case "google":
return {
id: "google",
name: "Google",
displayName: "Google",
icon: {
src: googleLogo,
},
};
case "okta":
return {
id: "okta",
name: "Okta",
displayName: "Okta",
icon: {
src: oktaLogo,
className: "dark:invert",
},
};
case "keycloak":
return {
id: "keycloak",
name: "Keycloak",
displayName: "Keycloak",
icon: {
src: keycloakLogo,
},
};
case "microsoft-entra-id":
return {
id: "microsoft-entra-id",
name: "Microsoft Entra ID",
displayName: "Microsoft Entra ID",
icon: {
src: microsoftLogo,
},
};
case "credentials":
return {
id: "credentials",
name: "Credentials",
displayName: "Email & Password",
icon: null, // No icon needed for credentials
};
case "nodemailer":
return {
id: "nodemailer",
name: "Email",
displayName: "Email Code",
icon: null, // No icon needed for email
};
default:
return {
id: providerId,
name: providerId,
displayName: providerId.charAt(0).toUpperCase() + providerId.slice(1),
icon: null,
};
}
};
type CodeHostInfo = { type CodeHostInfo = {
type: CodeHostType; type: CodeHostType;
displayName: string; displayName: string;