import { useEffect, useState, type FormEvent } from "react"; import { Eye, EyeOff, LogIn, ShieldCheck } from "lucide-react"; import { useSession } from "@/store/session"; import { ApiError } from "@/jmap/client"; import { DEFAULT_SOURCE_URL } from "@/lib/source"; export function LoginPage() { const login = useSession((s) => s.login); // The AGPL's offer has to reach everyone who interacts with the app over the // network, and that includes whoever is looking at this form. The server says // where its own source lives, so a modified deployment points at its own. const [sourceUrl, setSourceUrl] = useState(DEFAULT_SOURCE_URL); useEffect(() => { let live = true; fetch("/api/config") .then((r) => (r.ok ? r.json() : null)) .then((c) => { if (live && c?.sourceUrl) setSourceUrl(c.sourceUrl as string); }) .catch(() => { /* the default stands */ }); return () => { live = false; }; }, []); const [username, setUsername] = useState(() => localStorage.getItem("ihasmail:lastUser") ?? ""); const [password, setPassword] = useState(""); const [totp, setTotp] = useState(""); const [showTotp, setShowTotp] = useState(false); const [showPw, setShowPw] = useState(false); const [remember, setRemember] = useState(true); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const submit = async (e: FormEvent) => { e.preventDefault(); if (!username || !password) return; setBusy(true); setError(null); try { await login(username.trim(), password, totp.trim(), remember); localStorage.setItem("ihasmail:lastUser", username.trim()); } catch (err) { if (err instanceof ApiError) { if (err.code === "invalid_credentials") { setError(showTotp ? "Invalid credentials or verification code." : "Invalid username or password."); if (!showTotp && password) setShowTotp(true); } else if (err.code === "rate_limited") setError("Too many attempts. Please wait a few minutes and try again."); else setError(err.message || "Could not sign in."); } else setError("Network error. Please check your connection."); } finally { setBusy(false); } }; return (

Fast, friendly webmail. Your mailbox, your way.

{error && (
{error}
)}
setUsername(e.target.value)} autoFocus={!username} required />
setPassword(e.target.value)} autoFocus={Boolean(username)} required style={{ paddingRight: 40 }} />
{showTotp ? (
setTotp(e.target.value)} autoFocus /> Enter the code from your authenticator app if your account uses 2FA.
) : ( )}

ihasmail by linuxexpert.org {" · "} AGPL-3.0 source

); }