import React, {useState, useEffect} from "react"; import {cn} from "@/lib/utils.ts"; import {Button} from "@/components/ui/button.tsx"; import {Input} from "@/components/ui/input.tsx"; import {Label} from "@/components/ui/label.tsx"; import {Alert, AlertTitle, AlertDescription} from "@/components/ui/alert.tsx"; import axios from "axios"; function setCookie(name: string, value: string, days = 7) { const expires = new Date(Date.now() + days * 864e5).toUTCString(); document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/`; } function getCookie(name: string) { return document.cookie.split('; ').reduce((r, v) => { const parts = v.split('='); return parts[0] === name ? decodeURIComponent(parts[1]) : r; }, ""); } const apiBase = import.meta.env.DEV ? "http://localhost:8081/users" : "/users"; const API = axios.create({ baseURL: apiBase, }); interface HomepageAuthProps extends React.ComponentProps<"div"> { setLoggedIn: (loggedIn: boolean) => void; setIsAdmin: (isAdmin: boolean) => void; setUsername: (username: string | null) => void; setUserId: (userId: string | null) => void; loggedIn: boolean; authLoading: boolean; dbError: string | null; setDbError: (error: string | null) => void; onAuthSuccess: (authData: { isAdmin: boolean; username: string | null; userId: string | null }) => void; } export function HomepageAuth({ className, setLoggedIn, setIsAdmin, setUsername, setUserId, loggedIn, authLoading, dbError, setDbError, onAuthSuccess, ...props }: HomepageAuthProps) { const [tab, setTab] = useState<"login" | "signup" | "external" | "reset">("login"); const [localUsername, setLocalUsername] = useState(""); const [password, setPassword] = useState(""); const [signupConfirmPassword, setSignupConfirmPassword] = useState(""); const [loading, setLoading] = useState(false); const [oidcLoading, setOidcLoading] = useState(false); const [error, setError] = useState(null); const [internalLoggedIn, setInternalLoggedIn] = useState(false); const [firstUser, setFirstUser] = useState(false); const [registrationAllowed, setRegistrationAllowed] = useState(true); const [oidcConfigured, setOidcConfigured] = useState(false); const [resetStep, setResetStep] = useState<"initiate" | "verify" | "newPassword">("initiate"); const [resetCode, setResetCode] = useState(""); const [newPassword, setNewPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [tempToken, setTempToken] = useState(""); const [resetLoading, setResetLoading] = useState(false); const [resetSuccess, setResetSuccess] = useState(false); useEffect(() => { setInternalLoggedIn(loggedIn); }, [loggedIn]); useEffect(() => { API.get("/registration-allowed").then(res => { setRegistrationAllowed(res.data.allowed); }); }, []); useEffect(() => { API.get("/oidc-config").then((response) => { if (response.data) { setOidcConfigured(true); } else { setOidcConfigured(false); } }).catch((error) => { if (error.response?.status === 404) { setOidcConfigured(false); } else { setOidcConfigured(false); } }); }, []); useEffect(() => { API.get("/count").then(res => { if (res.data.count === 0) { setFirstUser(true); setTab("signup"); } else { setFirstUser(false); } setDbError(null); }).catch(() => { setDbError("Could not connect to the database. Please try again later."); }); }, [setDbError]); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setError(null); setLoading(true); if (!localUsername.trim()) { setError("Username is required"); setLoading(false); return; } try { let res, meRes; if (tab === "login") { res = await API.post("/login", {username: localUsername, password}); } else { if (password !== signupConfirmPassword) { setError("Passwords do not match"); setLoading(false); return; } if (password.length < 6) { setError("Password must be at least 6 characters long"); setLoading(false); return; } await API.post("/create", {username: localUsername, password}); res = await API.post("/login", {username: localUsername, password}); } setCookie("jwt", res.data.token); [meRes] = await Promise.all([ API.get("/me", {headers: {Authorization: `Bearer ${res.data.token}`}}), API.get("/db-health") ]); setInternalLoggedIn(true); setLoggedIn(true); setIsAdmin(!!meRes.data.is_admin); setUsername(meRes.data.username || null); setUserId(meRes.data.id || null); setDbError(null); onAuthSuccess({ isAdmin: !!meRes.data.is_admin, username: meRes.data.username || null, userId: meRes.data.id || null }); setInternalLoggedIn(true); if (tab === "signup") { setSignupConfirmPassword(""); } } catch (err: any) { setError(err?.response?.data?.error || "Unknown error"); setInternalLoggedIn(false); setLoggedIn(false); setIsAdmin(false); setUsername(null); setUserId(null); setCookie("jwt", "", -1); if (err?.response?.data?.error?.includes("Database")) { setDbError("Could not connect to the database. Please try again later."); } else { setDbError(null); } } finally { setLoading(false); } } async function initiatePasswordReset() { setError(null); setResetLoading(true); try { await API.post("/initiate-reset", {username: localUsername}); setResetStep("verify"); setError(null); } catch (err: any) { setError(err?.response?.data?.error || "Failed to initiate password reset"); } finally { setResetLoading(false); } } async function verifyResetCode() { setError(null); setResetLoading(true); try { const response = await API.post("/verify-reset-code", { username: localUsername, resetCode: resetCode }); setTempToken(response.data.tempToken); setResetStep("newPassword"); setError(null); } catch (err: any) { setError(err?.response?.data?.error || "Failed to verify reset code"); } finally { setResetLoading(false); } } async function completePasswordReset() { setError(null); setResetLoading(true); if (newPassword !== confirmPassword) { setError("Passwords do not match"); setResetLoading(false); return; } if (newPassword.length < 6) { setError("Password must be at least 6 characters long"); setResetLoading(false); return; } try { await API.post("/complete-reset", { username: localUsername, tempToken: tempToken, newPassword: newPassword }); setResetStep("initiate"); setResetCode(""); setNewPassword(""); setConfirmPassword(""); setTempToken(""); setError(null); setResetSuccess(true); } catch (err: any) { setError(err?.response?.data?.error || "Failed to complete password reset"); } finally { setResetLoading(false); } } function resetPasswordState() { setResetStep("initiate"); setResetCode(""); setNewPassword(""); setConfirmPassword(""); setTempToken(""); setError(null); setResetSuccess(false); setSignupConfirmPassword(""); } function clearFormFields() { setPassword(""); setSignupConfirmPassword(""); setError(null); } async function handleOIDCLogin() { setError(null); setOidcLoading(true); try { const authResponse = await API.get("/oidc/authorize"); const {auth_url: authUrl} = authResponse.data; if (!authUrl || authUrl === 'undefined') { throw new Error('Invalid authorization URL received from backend'); } window.location.replace(authUrl); } catch (err: any) { setError(err?.response?.data?.error || err?.message || "Failed to start OIDC login"); setOidcLoading(false); } } useEffect(() => { const urlParams = new URLSearchParams(window.location.search); const success = urlParams.get('success'); const token = urlParams.get('token'); const error = urlParams.get('error'); if (error) { setError(`OIDC authentication failed: ${error}`); setOidcLoading(false); window.history.replaceState({}, document.title, window.location.pathname); return; } if (success && token) { setOidcLoading(true); setError(null); setCookie("jwt", token); API.get("/me", {headers: {Authorization: `Bearer ${token}`}}) .then(meRes => { setInternalLoggedIn(true); setLoggedIn(true); setIsAdmin(!!meRes.data.is_admin); setUsername(meRes.data.username || null); setUserId(meRes.data.id || null); setDbError(null); onAuthSuccess({ isAdmin: !!meRes.data.is_admin, username: meRes.data.username || null, userId: meRes.data.id || null }); setInternalLoggedIn(true); window.history.replaceState({}, document.title, window.location.pathname); }) .catch(err => { setError("Failed to get user info after OIDC login"); setInternalLoggedIn(false); setLoggedIn(false); setIsAdmin(false); setUsername(null); setUserId(null); setCookie("jwt", "", -1); window.history.replaceState({}, document.title, window.location.pathname); }) .finally(() => { setOidcLoading(false); }); } }, []); const Spinner = ( ); return (
{dbError && ( Error {dbError} )} {firstUser && !dbError && !internalLoggedIn && ( First User You are the first user and will be made an admin. You can view admin settings in the sidebar user dropdown. If you think this is a mistake, check the docker logs, or create a{" "} GitHub issue . )} {!registrationAllowed && !internalLoggedIn && ( Registration Disabled New account registration is currently disabled by an admin. Please log in or contact an administrator. )} {(!internalLoggedIn && (!authLoading || !getCookie("jwt"))) && ( <>
{oidcConfigured && ( )}

{tab === "login" ? "Login to your account" : tab === "signup" ? "Create a new account" : tab === "external" ? "Login with external provider" : "Reset your password"}

{tab === "external" || tab === "reset" ? (
{tab === "external" && ( <>

Login using your configured external identity provider

)} {tab === "reset" && ( <> {resetStep === "initiate" && ( <>

Enter your username to receive a password reset code. The code will be logged in the docker container logs.

setLocalUsername(e.target.value)} disabled={resetLoading} />
)} {resetStep === "verify" && ( <>

Enter the 6-digit code from the docker container logs for user: {localUsername}

setResetCode(e.target.value.replace(/\D/g, ''))} disabled={resetLoading} placeholder="000000" />
)} {resetSuccess && ( <> Success! Your password has been successfully reset! You can now log in with your new password. )} {resetStep === "newPassword" && !resetSuccess && ( <>

Enter your new password for user: {localUsername}

setNewPassword(e.target.value)} disabled={resetLoading} autoComplete="new-password" />
setConfirmPassword(e.target.value)} disabled={resetLoading} autoComplete="new-password" />
)} )}
) : (
setLocalUsername(e.target.value)} disabled={loading || internalLoggedIn} />
setPassword(e.target.value)} disabled={loading || internalLoggedIn}/>
{tab === "signup" && (
setSignupConfirmPassword(e.target.value)} disabled={loading || internalLoggedIn}/>
)} {tab === "login" && ( )}
)} )} {error && ( Error {error} )}
); }