"use client"; import { useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { RECOVERY_RE } from "@spinza/shared"; import { api } from "@/lib/api"; import { useSession } from "@/lib/store"; import { Button, Input } from "@/components/ui"; import { RecoveryCodePanel } from "@/components/shell/recovery-code"; /** Normalise user input to SPZ-XXXX-XXXX-XXXX while typing. */ function formatRecoveryCode(raw: string): string { const clean = raw.toUpperCase().replace(/[^A-Z0-9]/g, ""); const body = clean.startsWith("SPZ") ? clean.slice(3) : clean; const groups = body.slice(0, 12).match(/.{1,4}/g) ?? []; return ["SPZ", ...groups].join("-"); } interface RecoverResponse { user: { id: string; username: string }; recoveryCode: string; notice: string; } export default function RecoverPage() { const router = useRouter(); const refresh = useSession((s) => s.refresh); const [username, setUsername] = useState(""); const [code, setCode] = useState("SPZ-"); const [password, setPassword] = useState(""); const [confirm, setConfirm] = useState(""); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); const [result, setResult] = useState(null); const [continuing, setContinuing] = useState(false); const codeOk = RECOVERY_RE.test(code); const pwError = password && password.length < 8 ? "At least 8 characters." : null; const confirmError = confirm && confirm !== password ? "Passwords do not match." : null; const canSubmit = username.trim().length >= 3 && codeOk && password.length >= 8 && confirm === password && !busy; const submit = async (e: React.FormEvent) => { e.preventDefault(); if (!canSubmit) return; setBusy(true); setError(null); try { const res = await api("/api/auth/recover", { json: { username: username.trim().toLowerCase(), recoveryCode: code, newPassword: password } }); setResult(res); } catch (err) { setError(err instanceof Error ? err.message : "Invalid username or recovery code."); } finally { setBusy(false); } }; const finish = async () => { setContinuing(true); await refresh(); router.push("/"); router.refresh(); }; if (result) { return (
Password updated. You are signed in as @{result.user.username}. All other sessions were signed out.
); } return (
Account recovery

Reset your password.

Enter the recovery code you saved when you created your account. A new code will be issued afterwards.

setUsername(e.target.value)} autoComplete="username" autoCapitalize="none" autoCorrect="off" spellCheck={false} placeholder="your username" required /> setCode(formatRecoveryCode(e.target.value))} onFocus={(e) => e.target.select()} autoComplete="one-time-code" autoCapitalize="characters" autoCorrect="off" spellCheck={false} placeholder="SPZ-XXXX-XXXX-XXXX" className="font-mono uppercase tracking-[0.08em]" error={code.length > 4 && code.length >= 18 && !codeOk ? "Format: SPZ-XXXX-XXXX-XXXX" : null} hint="Format: SPZ-XXXX-XXXX-XXXX" /> setPassword(e.target.value)} autoComplete="new-password" placeholder="At least 8 characters" error={pwError} /> setConfirm(e.target.value)} autoComplete="new-password" placeholder="Repeat your new password" error={confirmError} /> {error ? (

{error}

) : null}

Remembered it?{" "} Back to sign in

Spinza never stores an email address or phone number, so there is no reset link to send. Without the recovery code, the account cannot be recovered — you can always start fresh with a new username.

); }