import React, {useState, useEffect} from "react"; import {cn} from "@/lib/utils"; import {Button} from "@/components/ui/button"; import {Input} from "@/components/ui/input"; import {Label} from "@/components/ui/label"; import {Alert, AlertTitle, AlertDescription} from "@/components/ui/alert"; import {Separator} from "@/components/ui/separator"; 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; } export function HomepageAuth({ className, setLoggedIn, setIsAdmin, setUsername, setUserId, loggedIn, authLoading, dbError, setDbError, ...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); 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 resetPassword() { } 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); 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"))) && (
Logged in! You are logged in! Use the sidebar to access all available tools. To get started, create an SSH Host in the SSH Manager tab. Once created, you can connect to that host using the other apps in the sidebar.
)} {(!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} />
setConfirmPassword(e.target.value)} disabled={resetLoading} />
)} )}
) : (
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} )}
); }