mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(oidc): add OIDC login button and auth hook updates
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user