"use client" import type React from "react" import { useState, useEffect } from "react" import { Button } from "./ui/button" import { Input } from "./ui/input" import { Label } from "./ui/label" import { Checkbox } from "./ui/checkbox" import { Lock, User, AlertCircle, Server, Shield, Eye, EyeOff } from "lucide-react" import { getApiUrl } from "../lib/api-config" import { useT } from "../lib/i18n/provider" import Image from "next/image" interface LoginProps { onLogin: () => void } export function Login({ onLogin }: LoginProps) { const t = useT() const [username, setUsername] = useState("") const [password, setPassword] = useState("") const [totpCode, setTotpCode] = useState("") const [requiresTotp, setRequiresTotp] = useState(false) const [rememberMe, setRememberMe] = useState(false) const [showPassword, setShowPassword] = useState(false) const [error, setError] = useState("") const [loading, setLoading] = useState(false) useEffect(() => { // The Login screen is, by construction, the recovery path from any // 401 cascade (the api-config wrapper redirects here when an // expired/invalid JWT is detected). Clear the cascade-prevention // flag on mount so a successful login can subsequently fire a fresh // reload if a NEW 401 ever occurs. Without this clear, any 401 set // earlier in the session sticks around forever and the next 401 // (e.g. mid-2FA, or right after a successful login if the token was // briefly stale) is silently swallowed by the de-dup — the user // sees a blank/stuck dashboard. try { sessionStorage.removeItem("proxmenux-auth-401-handled") } catch { // private browsing — best-effort } const savedUsername = localStorage.getItem("proxmenux-saved-username") const savedPassword = localStorage.getItem("proxmenux-saved-password") if (savedUsername && savedPassword) { setUsername(savedUsername) setPassword(savedPassword) setRememberMe(true) } }, []) const handleLogin = async (e: React.FormEvent) => { e.preventDefault() setError("") if (!username || !password) { setError(t("login.missingCredentials")) return } if (requiresTotp && !totpCode) { setError(t("login.missingTotp")) return } setLoading(true) try { const response = await fetch(getApiUrl("/api/auth/login"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password, totp_token: totpCode || undefined, // Include 2FA code if provided }), }) const data = await response.json() if (response.ok && data.requires_totp) { setRequiresTotp(true) setLoading(false) return } if (!response.ok) { if (response.status === 429) { throw new Error(t("login.tooManyAttempts")) } if (response.status === 401) { throw new Error(data.requires_totp ? t("login.invalidTotp") : t("login.invalidCredentials")) } throw new Error(t("login.loginFailed")) } localStorage.setItem("proxmenux-auth-token", data.token) try { sessionStorage.removeItem("proxmenux-auth-401-handled") } catch { // ignore } if (rememberMe) { localStorage.setItem("proxmenux-saved-username", username) localStorage.setItem("proxmenux-saved-password", password) } else { localStorage.removeItem("proxmenux-saved-username") localStorage.removeItem("proxmenux-saved-password") } onLogin() } catch (err) { setError(err instanceof Error ? err.message : t("login.loginFailed")) } finally { setLoading(false) } } return (
ProxMenux Logo { const target = e.target as HTMLImageElement target.style.display = "none" const fallback = target.parentElement?.querySelector(".fallback-icon") if (fallback) { fallback.classList.remove("hidden") } }} />

{t("app.title")}

{t("login.subtitle")}

{error && (

{error}

)} {!requiresTotp ? ( <>
setUsername(e.target.value)} className="pl-10 text-base" disabled={loading} autoComplete="username" />
setPassword(e.target.value)} className="pl-10 pr-10 text-base" disabled={loading} autoComplete="current-password" />
setRememberMe(checked as boolean)} disabled={loading} />
) : (

{t("login.twoFactorTitle")}

{t("login.twoFactorDescription")}

setTotpCode(e.target.value.replace(/\D/g, "").slice(0, 6))} className="text-center text-lg tracking-widest font-mono text-base" maxLength={6} disabled={loading} autoComplete="one-time-code" autoFocus />

{t("login.backupCodeHint")}

)}

{t("login.version")}

) }