feat: initial commit — Curio mastery tutor
Full Next.js App Router application with: - AI-graded lesson checkpoints (BKT/Elo mastery tracking) - Auth.js v5: credentials, Google, GitHub, generic OIDC - Anonymous-session-first with migrate-on-signin - Admin panel: users, blueprints, reports, site settings - Password reset + email verification (nodemailer/SMTP) - Site config: require_auth + signups_enabled flags - server-only guards on all DB/generation/verification modules - PostgreSQL 16 + pgvector, Redis cache, Drizzle ORM - 271 unit tests (Vitest), golden-eval harness, Playwright e2e stubs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
const params = useSearchParams();
|
||||
const token = params.get('token') ?? '';
|
||||
const router = useRouter();
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirm, setConfirm] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
if (!token) {
|
||||
return (
|
||||
<main style={s.page}>
|
||||
<p style={{ color: 'var(--color-misconception)' }}>Invalid reset link.</p>
|
||||
<Link href="/auth/forgot-password" style={s.link}>Request a new one</Link>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (password !== confirm) { setError('Passwords do not match.'); return; }
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const res = await fetch('/api/auth/reset-password', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token, password }),
|
||||
});
|
||||
setLoading(false);
|
||||
if (res.ok) {
|
||||
router.push('/auth/login?reset=1');
|
||||
} else {
|
||||
const d = (await res.json()) as { error?: string };
|
||||
setError(d.error ?? 'Failed to reset password.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main style={s.page}>
|
||||
<h1 style={s.heading}>Set new password</h1>
|
||||
<form onSubmit={handleSubmit} style={s.form} noValidate>
|
||||
<label style={s.label} htmlFor="password">New password</label>
|
||||
<input id="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} required minLength={8} autoComplete="new-password" style={s.input} />
|
||||
<label style={s.label} htmlFor="confirm">Confirm password</label>
|
||||
<input id="confirm" type="password" value={confirm} onChange={(e) => setConfirm(e.target.value)} required autoComplete="new-password" style={s.input} />
|
||||
{error && <p role="alert" style={s.error}>{error}</p>}
|
||||
<button type="submit" disabled={!password || !confirm || loading} style={s.btn}>
|
||||
{loading ? 'Saving…' : 'Set password'}
|
||||
</button>
|
||||
</form>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const s = {
|
||||
page: { display: 'flex', flexDirection: 'column' as const, alignItems: 'center', minHeight: '100svh', padding: 'var(--space-8)', paddingTop: 'var(--space-16)', gap: 'var(--space-4)' },
|
||||
heading: { fontFamily: 'var(--font-display)', fontSize: '1.5rem', fontWeight: 500, color: 'var(--color-ink)', marginBottom: 'var(--space-4)' },
|
||||
form: { width: '100%', maxWidth: '26rem', display: 'flex', flexDirection: 'column' as const, gap: 'var(--space-2)' },
|
||||
label: { fontFamily: 'var(--font-display)', fontSize: '0.875rem', fontWeight: 500, color: 'var(--color-ink)', marginTop: 'var(--space-3)' },
|
||||
input: { padding: 'var(--space-3) var(--space-4)', background: 'var(--color-surface)', border: '1px solid var(--color-border)', borderRadius: '4px', fontFamily: 'var(--font-body)', fontSize: '1rem', color: 'var(--color-ink)', outline: 'none' },
|
||||
error: { color: 'var(--color-misconception)', fontSize: '0.875rem', margin: 0 },
|
||||
btn: { marginTop: 'var(--space-4)', padding: 'var(--space-3) var(--space-6)', background: 'var(--color-accent)', color: '#fff', border: 'none', borderRadius: '4px', fontFamily: 'var(--font-display)', fontSize: '0.9375rem', fontWeight: 500, cursor: 'pointer' },
|
||||
link: { color: 'var(--color-accent)', fontFamily: 'var(--font-display)', fontSize: '0.875rem', textDecoration: 'none' },
|
||||
} as const;
|
||||
Reference in New Issue
Block a user