mirror of
https://github.com/bretzel-app/crumbs.git
synced 2026-10-12 20:06:57 +02:00
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:
committed by
Fred Cabassut
co-authored by
Claude Opus 4.6
parent
59b5e92d91
commit
4dea359362
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
Reference in New Issue
Block a user