"use client" import { useState, useEffect, useRef } from "react" import { Button } from "./ui/button" import { Dialog, DialogContent, DialogTitle } from "./ui/dialog" import { Input } from "./ui/input" import { Label } from "./ui/label" import { Shield, Lock, User, AlertCircle, Eye, EyeOff, Upload, Trash2 } from "lucide-react" import { getApiUrl } from "../lib/api-config" interface AuthSetupProps { onComplete: () => void } export function AuthSetup({ onComplete }: AuthSetupProps) { const [open, setOpen] = useState(false) const [step, setStep] = useState<"choice" | "setup">("choice") const [username, setUsername] = useState("") const [password, setPassword] = useState("") const [confirmPassword, setConfirmPassword] = useState("") const [error, setError] = useState("") const [loading, setLoading] = useState(false) const [showPassword, setShowPassword] = useState(false) const [showConfirmPassword, setShowConfirmPassword] = useState(false) // Profile (Fase 2 — v1.2.2). Both optional decorations on top of the // mandatory username + password. Persisted via PUT /api/auth/profile // and POST /api/auth/profile/avatar after the user lands a successful // /api/auth/setup so we don't change the setup endpoint's contract. const [displayName, setDisplayName] = useState("") const [avatarFile, setAvatarFile] = useState(null) const [avatarPreviewUrl, setAvatarPreviewUrl] = useState(null) const fileInputRef = useRef(null) useEffect(() => { const checkOnboardingStatus = async () => { try { const response = await fetch(getApiUrl("/api/auth/status")) // Check if response is valid JSON before parsing if (!response.ok) { // API not available - don't show modal in preview return } const contentType = response.headers.get("content-type") if (!contentType || !contentType.includes("application/json")) { return } const data = await response.json() // Show modal if auth is not configured and not declined if (!data.auth_configured) { setTimeout(() => setOpen(true), 500) } } catch { // API not available (preview environment) - don't show modal } } checkOnboardingStatus() }, []) const handleSkipAuth = async () => { setLoading(true) setError("") try { const response = await fetch(getApiUrl("/api/auth/skip"), { method: "POST", headers: { "Content-Type": "application/json" }, }) const data = await response.json() if (!response.ok) { throw new Error(data.error || "Failed to skip authentication") } if (data.auth_declined) { } localStorage.setItem("proxmenux-auth-declined", "true") localStorage.removeItem("proxmenux-auth-token") // Remove any old token setOpen(false) onComplete() } catch (err) { console.error("[v0] Auth skip error:", err) setError(err instanceof Error ? err.message : "Failed to save preference") } finally { setLoading(false) } } const handleAvatarPick = () => fileInputRef.current?.click() const handleAvatarChange = (file: File | null) => { // Revoke the previous local preview so we don't leak blob URLs while // the user picks another file before submitting. if (avatarPreviewUrl) { URL.revokeObjectURL(avatarPreviewUrl) } setAvatarFile(file) setAvatarPreviewUrl(file ? URL.createObjectURL(file) : null) } const handleSetupAuth = async () => { setError("") if (!username || !password) { setError("Please fill in all fields") return } if (password !== confirmPassword) { setError("Passwords do not match") return } if (password.length < 6) { setError("Password must be at least 6 characters") return } setLoading(true) try { const response = await fetch(getApiUrl("/api/auth/setup"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password, }), }) const data = await response.json() if (!response.ok) { throw new Error(data.error || "Failed to setup authentication") } if (data.token) { localStorage.setItem("proxmenux-auth-token", data.token) localStorage.removeItem("proxmenux-auth-declined") } // Profile decorations (Fase 2). Sent as a follow-up to the setup // call so the /api/auth/setup endpoint stays minimal (username + // password only) — these calls reuse the existing profile // endpoints and the JWT we just received. Failures here are // non-fatal: the user is already authenticated and can finish // configuring the profile from the /profile page. const token = data.token if (token) { const trimmedDisplayName = displayName.trim() if (trimmedDisplayName) { try { await fetch(getApiUrl("/api/auth/profile"), { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ display_name: trimmedDisplayName }), }) } catch (e) { console.warn("[auth-setup] failed to save display_name:", e) } } if (avatarFile) { try { await fetch(getApiUrl("/api/auth/profile/avatar"), { method: "POST", headers: { "Content-Type": avatarFile.type, Authorization: `Bearer ${token}`, }, body: avatarFile, }) } catch (e) { console.warn("[auth-setup] failed to upload avatar:", e) } } } // Release the local preview blob now that the file has been // uploaded (or skipped). The header avatar pulls a fresh copy // from the backend. if (avatarPreviewUrl) { URL.revokeObjectURL(avatarPreviewUrl) setAvatarPreviewUrl(null) } // Notify the header AvatarMenu (mounted on dashboard load with // auth_enabled=false) to re-fetch its status + profile so the // avatar appears immediately after first-time setup instead of // requiring a page refresh. if (typeof window !== "undefined") { window.dispatchEvent(new CustomEvent("proxmenux:profile-changed")) } setOpen(false) onComplete() } catch (err) { console.error("[v0] Auth setup error:", err) setError(err instanceof Error ? err.message : "Failed to setup authentication") } finally { setLoading(false) } } return ( {step === "choice" ? "Setup Dashboard Protection" : "Create Password"} {step === "choice" ? (

Protect Your Dashboard?

Add an extra layer of security to protect your Proxmox data when accessing from non-private networks.

You can always enable this later in Settings

) : (

Setup Authentication

Create a username and password to protect your dashboard

{error && (

{error}

)}
setUsername(e.target.value)} className="pl-10 text-base" disabled={loading} autoComplete="username" />
setPassword(e.target.value)} className="pl-10 text-base" disabled={loading} autoComplete="new-password" />
setConfirmPassword(e.target.value)} className="pl-10 text-base" disabled={loading} autoComplete="new-password" />
{/* Optional profile decorations (Fase 2). Visually separated from the mandatory credential fields by a divider + a small heading so the operator understands they can skip everything below and still complete the setup. Both are saved with follow-up calls after the setup endpoint returns the JWT. */}

Profile · optional

setDisplayName(e.target.value)} maxLength={64} className="pl-10 text-base" disabled={loading} />

Leave empty to render the username itself. Up to 64 characters.

{avatarPreviewUrl ? ( // eslint-disable-next-line @next/next/no-img-element ) : ( {(displayName || username || "U").trim().charAt(0).toUpperCase() || "U"} )}
{ const file = e.target.files?.[0] || null handleAvatarChange(file) if (fileInputRef.current) fileInputRef.current.value = "" }} />
{avatarFile && ( )}

PNG, JPEG, WebP or GIF · up to 2 MB · pre-crop square for best results.

)}
) }