Files
dumpsterChat/web/src/components/LoginForm.tsx
T
hobokenchicken 9f138deb9d feat: login with username or email + change password
Backend:
- Login handler accepts username OR email via the same field
- New PUT /auth/me/password endpoint (verifies current password,
  requires min 8 chars for new password)

Frontend:
- Login form shows EMAIL OR USERNAME field (single field for both)
- Auth store updated with changePassword function
- UserSettings has new CHANGE PASSWORD section
2026-06-29 14:00:19 -04:00

180 lines
5.9 KiB
TypeScript

import { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useAuthStore } from "../stores/auth.ts";
export function LoginForm() {
const [isRegister, setIsRegister] = useState(false);
const [email, setEmail] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const { login, register, isLoading, error, clearError } = useAuthStore();
const navigate = useNavigate();
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
clearError();
if (isRegister) {
if (password !== confirmPassword) return;
await register(email, username, password);
} else {
await login(email, password);
}
navigate("/");
};
return (
<div className="h-full w-full flex items-center justify-center bg-gb-bg p-4">
<div className="terminal-border bg-gb-bg-h p-6 w-full max-w-md">
<pre className="text-gb-orange font-mono text-lg mb-6">
{"┌─ DUMPSTER ─┐"}
</pre>
<h2 className="text-gb-fg-s mb-4">
{isRegister ? "[REGISTER]" : "[LOGIN]"}
</h2>
<form onSubmit={handleSubmit} className="space-y-4">
{isRegister ? (
<>
<div>
<label className="block text-gb-fg-t mb-1">EMAIL:</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="terminal-input"
required
autoComplete="email"
/>
</div>
<div>
<label className="block text-gb-fg-t mb-1">USERNAME:</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="terminal-input"
required
autoComplete="username"
/>
</div>
</>
) : (
<div>
<label className="block text-gb-fg-t mb-1">
EMAIL OR USERNAME:
</label>
<input
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="terminal-input"
required
autoComplete="username"
placeholder="email or username"
/>
</div>
)}
<div>
<label className="block text-gb-fg-t mb-1">PASSWORD:</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="terminal-input"
required
autoComplete={
isRegister ? "new-password" : "current-password"
}
/>
</div>
{isRegister && (
<div>
<label className="block text-gb-fg-t mb-1">
CONFIRM PASSWORD:
</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="terminal-input"
required
/>
</div>
)}
{error && <p className="text-gb-red text-sm">ERR: {error}</p>}
<button
type="submit"
className="terminal-button w-full"
disabled={isLoading}
>
{isLoading
? "[PROCESSING...]"
: isRegister
? "[REGISTER]"
: "[LOGIN]"}
</button>
</form>
{!isRegister && (
<div className="mt-3">
<button
type="button"
onClick={async () => {
try {
const resp = await fetch(
"/api/v1/auth/webauthn/login/begin",
{ method: "POST", credentials: "include" },
);
if (!resp.ok)
throw new Error("Passkey login not available");
const options = await resp.json();
const credential = await navigator.credentials.get({
publicKey: options,
});
if (!credential) return;
const finishResp = await fetch(
"/api/v1/auth/webauthn/login/finish",
{
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(credential),
},
);
if (finishResp.ok) navigate("/");
} catch (err) {
console.error("Passkey login failed:", err);
}
}}
className="terminal-button w-full border-gb-aqua text-gb-aqua"
>
[SIGN IN WITH PASSKEY]
</button>
</div>
)}
<div className="mt-4 text-center">
<button
type="button"
onClick={() => {
setIsRegister(!isRegister);
clearError();
}}
className="text-gb-blue hover:text-gb-aqua text-sm"
>
{isRegister ? "[BACK TO LOGIN]" : "[CREATE ACCOUNT]"}
</button>
</div>
{!isRegister && (
<p className="text-gb-fg-f text-xs mt-4 text-center">
Use Link component:{" "}
<Link to="/" className="text-gb-blue hover:text-gb-aqua">
[HOME]
</Link>
</p>
)}
</div>
</div>
);
}