diff --git a/apps/web/src/hooks/use-auth.ts b/apps/web/src/hooks/use-auth.ts index 344a8e9b..c9d01bbc 100644 --- a/apps/web/src/hooks/use-auth.ts +++ b/apps/web/src/hooks/use-auth.ts @@ -12,6 +12,10 @@ interface AuthState { analyticsEnabled: boolean | null; analyticsConsentShownAt: number | null; analyticsConsentRemindAt: number | null; + oidcEnabled: boolean; + oidcProviderName: string | null; + loginMethod: string | null; + hasLocalPassword: boolean; } const USER_PERMISSIONS = [ @@ -33,6 +37,10 @@ export function useAuth() { analyticsEnabled: null, analyticsConsentShownAt: null, analyticsConsentRemindAt: null, + oidcEnabled: false, + oidcProviderName: null, + loginMethod: null, + hasLocalPassword: false, }); useEffect(() => { @@ -55,27 +63,16 @@ export function useAuth() { analyticsEnabled: null, analyticsConsentShownAt: null, analyticsConsentRemindAt: null, + oidcEnabled: false, + oidcProviderName: null, + loginMethod: null, + hasLocalPassword: false, }); return; } - const token = localStorage.getItem("snapotter-token"); - if (!token) { - if (!cancelled) - setState({ - loading: false, - authEnabled: true, - isAuthenticated: false, - mustChangePassword: false, - role: null, - permissions: [], - analyticsEnabled: null, - analyticsConsentShownAt: null, - analyticsConsentRemindAt: null, - }); - return; - } - + // Always call /api/auth/session -- OIDC users have a session cookie + // (not a localStorage token), so we cannot skip based on token absence. const sessionRes = await fetch("/api/auth/session", { headers: formatHeaders(), }); @@ -94,6 +91,10 @@ export function useAuth() { analyticsEnabled: session.user?.analyticsEnabled ?? null, analyticsConsentShownAt: session.user?.analyticsConsentShownAt ?? null, analyticsConsentRemindAt: session.user?.analyticsConsentRemindAt ?? null, + oidcEnabled: config.oidcEnabled ?? false, + oidcProviderName: config.oidcProviderName ?? null, + loginMethod: session.user?.loginMethod ?? null, + hasLocalPassword: session.user?.hasLocalPassword ?? false, }); } else { localStorage.removeItem("snapotter-token"); @@ -108,6 +109,10 @@ export function useAuth() { analyticsEnabled: null, analyticsConsentShownAt: null, analyticsConsentRemindAt: null, + oidcEnabled: config.oidcEnabled ?? false, + oidcProviderName: config.oidcProviderName ?? null, + loginMethod: null, + hasLocalPassword: false, }); } } catch { diff --git a/apps/web/src/pages/login-page.tsx b/apps/web/src/pages/login-page.tsx index a1f0467b..d5a18365 100644 --- a/apps/web/src/pages/login-page.tsx +++ b/apps/web/src/pages/login-page.tsx @@ -1,4 +1,6 @@ import { type FormEvent, useCallback, useEffect, useState } from "react"; +import { useSearchParams } from "react-router-dom"; +import { useAuth } from "@/hooks/use-auth"; import { setToken } from "@/lib/api"; const phrases = [ @@ -60,11 +62,28 @@ function RotatingPhrase() { } export function LoginPage() { + const { oidcEnabled, oidcProviderName } = useAuth(); + const [searchParams] = useSearchParams(); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); const [loading, setLoading] = useState(false); + useEffect(() => { + const oidcError = searchParams.get("error"); + if (oidcError) { + const errorMessages: Record = { + oidc_auth_failed: "Authentication failed. Please try again.", + oidc_provider_unreachable: "Could not reach the identity provider. Please try again later.", + oidc_session_expired: "Login session expired. Please try again.", + oidc_user_not_authorized: + "Your account is not authorized to access this application. Contact your administrator.", + oidc_user_limit_reached: "User limit reached. Contact your administrator.", + }; + setError(errorMessages[oidcError] || "Authentication error. Please try again."); + } + }, [searchParams]); + const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setLoading(true); @@ -149,6 +168,21 @@ export function LoginPage() { {loading ? "Logging in..." : "Login"} + {oidcEnabled && ( + <> +
+
+ or +
+
+ + Sign in with {oidcProviderName || "SSO"} + + + )}
diff --git a/packages/shared/src/i18n/en.ts b/packages/shared/src/i18n/en.ts index cb895f03..b6d2a749 100644 --- a/packages/shared/src/i18n/en.ts +++ b/packages/shared/src/i18n/en.ts @@ -300,6 +300,19 @@ export const en = { loggingIn: "Logging in...", invalidCredentials: "Invalid username or password", connectionError: "Connection error", + signInWith: "Sign in with {provider}", + signInWithSso: "Sign in with SSO", + or: "or", + methodLocal: "Local", + methodOidc: "OIDC", + methodBoth: "Local + OIDC", + oidcAuthFailed: "Authentication failed. Please try again.", + oidcProviderUnreachable: "Could not reach the identity provider. Please try again later.", + oidcSessionExpired: "Login session expired. Please try again.", + oidcUserNotAuthorized: + "Your account is not authorized to access this application. Contact your administrator.", + oidcUserLimitReached: "User limit reached. Contact your administrator.", + passwordManagedByProvider: "Password changes are managed by your identity provider.", }, pipeline: { title: "Automate",