feat(oidc): add OIDC login button and auth hook updates

This commit is contained in:
SnapOtter
2026-05-13 19:04:06 +08:00
parent aaebd14c60
commit 44f42c6866
3 changed files with 69 additions and 17 deletions
+22 -17
View File
@@ -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 {
+34
View File
@@ -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<string, string> = {
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"}
</button>
</form>
{oidcEnabled && (
<>
<div className="flex items-center gap-3 my-4">
<div className="flex-1 border-t border-border" />
<span className="text-sm text-muted-foreground">or</span>
<div className="flex-1 border-t border-border" />
</div>
<a
href="/api/auth/oidc/login"
className="w-full py-3 rounded-lg bg-secondary text-secondary-foreground font-medium hover:bg-secondary/80 transition-colors flex items-center justify-center gap-2"
>
Sign in with {oidcProviderName || "SSO"}
</a>
</>
)}
</div>
</div>
<div className="hidden lg:flex flex-1 bg-primary/90 items-center justify-center p-12 text-white rounded-l-3xl">
+13
View File
@@ -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",