feat(auth): email-based login and setup flows

Login page now requires email + password. Setup page adds email and
optional display name fields. API endpoints updated to pass email
to verifyPassword/setupUser and explicit userId to createSession.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Frederic Cabassut
2026-03-09 01:10:37 +01:00
committed by Fred Cabassut
co-authored by Claude Opus 4.6
parent 59b5e92d91
commit 4dea359362
4 changed files with 63 additions and 19 deletions
+16 -4
View File
@@ -1,4 +1,7 @@
<script lang="ts">
import { goto } from '$app/navigation';
let email = $state('');
let password = $state('');
let errorMsg = $state('');
let loading = $state(false);
@@ -12,14 +15,14 @@
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password })
body: JSON.stringify({ email, password })
});
if (res.ok) {
window.location.href = '/';
goto('/');
} else {
const data = await res.json();
errorMsg = data.message || 'Invalid password';
errorMsg = data.message || 'Invalid email or password';
}
} catch {
errorMsg = 'Connection error';
@@ -37,7 +40,7 @@
<div class="mb-6 text-center">
<img src="/favicon.svg" alt="Crumbs" class="mx-auto h-12 w-12" />
<h1 class="font-['Press_Start_2P'] text-xl text-[var(--primary)]">Crumbs</h1>
<p class="mt-2 text-sm text-[var(--text-muted)]">Enter your password to continue</p>
<p class="mt-2 text-sm text-[var(--text-muted)]">Sign in to continue</p>
</div>
<form onsubmit={handleLogin}>
@@ -47,6 +50,15 @@
</div>
{/if}
<input
type="email"
bind:value={email}
placeholder="Email"
class="mb-3 w-full rounded-sm border border-[var(--border-subtle)] bg-[var(--bg-base)] px-4 py-3 text-sm text-[var(--text)] outline-none placeholder:text-[var(--text-muted)] focus:border-[var(--primary)]"
data-testid="email-input"
required
/>
<input
type="password"
bind:value={password}
+32 -4
View File
@@ -1,4 +1,8 @@
<script lang="ts">
import { goto } from '$app/navigation';
let email = $state('');
let displayName = $state('');
let password = $state('');
let confirmPassword = $state('');
let errorMsg = $state('');
@@ -8,6 +12,11 @@
e.preventDefault();
errorMsg = '';
if (!email) {
errorMsg = 'Email is required';
return;
}
if (password.length < 8) {
errorMsg = 'Password must be at least 8 characters';
return;
@@ -24,11 +33,11 @@
const res = await fetch('/api/auth/setup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password })
body: JSON.stringify({ email, displayName, password })
});
if (res.ok) {
window.location.href = '/';
goto('/');
} else {
const data = await res.json();
errorMsg = data.message || 'Setup failed';
@@ -49,7 +58,7 @@
<div class="mb-6 text-center">
<img src="/favicon.svg" alt="Crumbs" class="mx-auto h-12 w-12" />
<h1 class="font-['Press_Start_2P'] text-xl text-[var(--primary)]">Welcome to Crumbs</h1>
<p class="mt-2 text-sm text-[var(--text-muted)]">Set up your password to get started</p>
<p class="mt-2 text-sm text-[var(--text-muted)]">Create your admin account to get started</p>
</div>
<form onsubmit={handleSetup}>
@@ -59,6 +68,23 @@
</div>
{/if}
<input
type="email"
bind:value={email}
placeholder="Email"
class="mb-3 w-full rounded-sm border border-[var(--border-subtle)] bg-[var(--bg-base)] px-4 py-3 text-sm text-[var(--text)] outline-none placeholder:text-[var(--text-muted)] focus:border-[var(--primary)]"
data-testid="email-input"
required
/>
<input
type="text"
bind:value={displayName}
placeholder="Display name (optional)"
class="mb-3 w-full rounded-sm border border-[var(--border-subtle)] bg-[var(--bg-base)] px-4 py-3 text-sm text-[var(--text)] outline-none placeholder:text-[var(--text-muted)] focus:border-[var(--primary)]"
data-testid="display-name-input"
/>
<input
type="password"
bind:value={password}
@@ -66,6 +92,7 @@
class="mb-3 w-full rounded-sm border border-[var(--border-subtle)] bg-[var(--bg-base)] px-4 py-3 text-sm text-[var(--text)] outline-none placeholder:text-[var(--text-muted)] focus:border-[var(--primary)]"
data-testid="password-input"
required
minlength="8"
/>
<input
@@ -75,6 +102,7 @@
class="mb-4 w-full rounded-sm border border-[var(--border-subtle)] bg-[var(--bg-base)] px-4 py-3 text-sm text-[var(--text)] outline-none placeholder:text-[var(--text-muted)] focus:border-[var(--primary)]"
data-testid="confirm-password-input"
required
minlength="8"
/>
<button
@@ -83,7 +111,7 @@
class="w-full rounded-sm bg-[var(--primary)] py-3 text-sm font-medium text-white transition-colors hover:bg-[var(--primary-hover)] disabled:opacity-50"
data-testid="setup-btn"
>
{loading ? 'Setting up...' : 'Set password & start'}
{loading ? 'Setting up...' : 'Create account & start'}
</button>
</form>
</div>
+7 -7
View File
@@ -3,18 +3,18 @@ import type { RequestHandler } from './$types.js';
import { verifyPassword, createSession } from '$lib/server/auth.js';
export const POST: RequestHandler = async ({ request, cookies }) => {
const { password } = await request.json();
const { email, password } = await request.json();
if (!password) {
throw error(400, 'Password is required');
if (!email || !password) {
throw error(400, 'Email and password are required');
}
const valid = await verifyPassword(password);
if (!valid) {
throw error(401, 'Invalid password');
const user = await verifyPassword(email, password);
if (!user) {
throw error(401, 'Invalid email or password');
}
const token = await createSession();
const token = await createSession(user.id);
cookies.set('session', token, {
path: '/',
httpOnly: true,
+8 -4
View File
@@ -8,18 +8,22 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
throw error(400, 'Setup already completed');
}
const { password } = await request.json();
const { email, displayName, password } = await request.json();
if (!email) {
throw error(400, 'Email is required');
}
if (!password || password.length < 8) {
throw error(400, 'Password must be at least 8 characters');
}
const success = await setupUser(password);
if (!success) {
const user = await setupUser(email, password, displayName);
if (!user) {
throw error(500, 'Failed to create user');
}
const token = await createSession();
const token = await createSession(user.id);
cookies.set('session', token, {
path: '/',
httpOnly: true,