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;
|
analyticsEnabled: boolean | null;
|
||||||
analyticsConsentShownAt: number | null;
|
analyticsConsentShownAt: number | null;
|
||||||
analyticsConsentRemindAt: number | null;
|
analyticsConsentRemindAt: number | null;
|
||||||
|
oidcEnabled: boolean;
|
||||||
|
oidcProviderName: string | null;
|
||||||
|
loginMethod: string | null;
|
||||||
|
hasLocalPassword: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const USER_PERMISSIONS = [
|
const USER_PERMISSIONS = [
|
||||||
@@ -33,6 +37,10 @@ export function useAuth() {
|
|||||||
analyticsEnabled: null,
|
analyticsEnabled: null,
|
||||||
analyticsConsentShownAt: null,
|
analyticsConsentShownAt: null,
|
||||||
analyticsConsentRemindAt: null,
|
analyticsConsentRemindAt: null,
|
||||||
|
oidcEnabled: false,
|
||||||
|
oidcProviderName: null,
|
||||||
|
loginMethod: null,
|
||||||
|
hasLocalPassword: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -55,27 +63,16 @@ export function useAuth() {
|
|||||||
analyticsEnabled: null,
|
analyticsEnabled: null,
|
||||||
analyticsConsentShownAt: null,
|
analyticsConsentShownAt: null,
|
||||||
analyticsConsentRemindAt: null,
|
analyticsConsentRemindAt: null,
|
||||||
|
oidcEnabled: false,
|
||||||
|
oidcProviderName: null,
|
||||||
|
loginMethod: null,
|
||||||
|
hasLocalPassword: false,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = localStorage.getItem("snapotter-token");
|
// Always call /api/auth/session -- OIDC users have a session cookie
|
||||||
if (!token) {
|
// (not a localStorage token), so we cannot skip based on token absence.
|
||||||
if (!cancelled)
|
|
||||||
setState({
|
|
||||||
loading: false,
|
|
||||||
authEnabled: true,
|
|
||||||
isAuthenticated: false,
|
|
||||||
mustChangePassword: false,
|
|
||||||
role: null,
|
|
||||||
permissions: [],
|
|
||||||
analyticsEnabled: null,
|
|
||||||
analyticsConsentShownAt: null,
|
|
||||||
analyticsConsentRemindAt: null,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionRes = await fetch("/api/auth/session", {
|
const sessionRes = await fetch("/api/auth/session", {
|
||||||
headers: formatHeaders(),
|
headers: formatHeaders(),
|
||||||
});
|
});
|
||||||
@@ -94,6 +91,10 @@ export function useAuth() {
|
|||||||
analyticsEnabled: session.user?.analyticsEnabled ?? null,
|
analyticsEnabled: session.user?.analyticsEnabled ?? null,
|
||||||
analyticsConsentShownAt: session.user?.analyticsConsentShownAt ?? null,
|
analyticsConsentShownAt: session.user?.analyticsConsentShownAt ?? null,
|
||||||
analyticsConsentRemindAt: session.user?.analyticsConsentRemindAt ?? 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 {
|
} else {
|
||||||
localStorage.removeItem("snapotter-token");
|
localStorage.removeItem("snapotter-token");
|
||||||
@@ -108,6 +109,10 @@ export function useAuth() {
|
|||||||
analyticsEnabled: null,
|
analyticsEnabled: null,
|
||||||
analyticsConsentShownAt: null,
|
analyticsConsentShownAt: null,
|
||||||
analyticsConsentRemindAt: null,
|
analyticsConsentRemindAt: null,
|
||||||
|
oidcEnabled: config.oidcEnabled ?? false,
|
||||||
|
oidcProviderName: config.oidcProviderName ?? null,
|
||||||
|
loginMethod: null,
|
||||||
|
hasLocalPassword: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { type FormEvent, useCallback, useEffect, useState } from "react";
|
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";
|
import { setToken } from "@/lib/api";
|
||||||
|
|
||||||
const phrases = [
|
const phrases = [
|
||||||
@@ -60,11 +62,28 @@ function RotatingPhrase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LoginPage() {
|
export function LoginPage() {
|
||||||
|
const { oidcEnabled, oidcProviderName } = useAuth();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
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) => {
|
const handleSubmit = async (e: FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -149,6 +168,21 @@ export function LoginPage() {
|
|||||||
{loading ? "Logging in..." : "Login"}
|
{loading ? "Logging in..." : "Login"}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</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>
|
</div>
|
||||||
<div className="hidden lg:flex flex-1 bg-primary/90 items-center justify-center p-12 text-white rounded-l-3xl">
|
<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...",
|
loggingIn: "Logging in...",
|
||||||
invalidCredentials: "Invalid username or password",
|
invalidCredentials: "Invalid username or password",
|
||||||
connectionError: "Connection error",
|
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: {
|
pipeline: {
|
||||||
title: "Automate",
|
title: "Automate",
|
||||||
|
|||||||
Reference in New Issue
Block a user