feat: add auth pages (login, logout, register, change-password)
Login uses loginLimiter, authenticates via authenticateUser, creates session cookie, and redirects foster role to /foster, others to /admin. Logout destroys session and redirects to /. Register uses registrationLimiter, validates input, creates inactive user, and sends registration notification. Change-password verifies current password, enforces 8-char minimum, updates hash, clears mustChangePassword flag. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db';
|
||||
import { users } from '$lib/server/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { hashPassword } from '$lib/server/auth-utils';
|
||||
import { sendRegistrationNotification } from '$lib/server/email';
|
||||
import { registrationLimiter } from '$lib/server/rate-limit';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
if (locals.user) {
|
||||
throw redirect(303, locals.user.role === 'foster' ? '/foster' : '/admin');
|
||||
}
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request, getClientAddress, locals }) => {
|
||||
const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? getClientAddress();
|
||||
|
||||
if (!registrationLimiter.check(ip)) {
|
||||
return fail(429, { error: 'Too many registration attempts. Please try again later.' });
|
||||
}
|
||||
|
||||
const data = await request.formData();
|
||||
const name = data.get('name')?.toString().trim();
|
||||
const email = data.get('email')?.toString().trim().toLowerCase();
|
||||
const password = data.get('password')?.toString();
|
||||
const confirmPassword = data.get('confirmPassword')?.toString();
|
||||
const reason = data.get('reason')?.toString().trim();
|
||||
|
||||
if (!name || !email || !password || !confirmPassword || !reason) {
|
||||
return fail(400, { error: 'All fields are required', name, email, reason });
|
||||
}
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
return fail(400, { error: 'Please enter a valid email address', name, email, reason });
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
return fail(400, { error: 'Password must be at least 8 characters', name, email, reason });
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
return fail(400, { error: 'Passwords do not match', name, email, reason });
|
||||
}
|
||||
|
||||
const [existing] = await db.select({ id: users.id }).from(users).where(eq(users.email, email)).limit(1);
|
||||
if (existing) {
|
||||
return fail(400, { error: 'An account with this email already exists', name, email, reason });
|
||||
}
|
||||
|
||||
const passwordHash = await hashPassword(password);
|
||||
|
||||
await db.insert(users).values({
|
||||
name,
|
||||
email,
|
||||
passwordHash,
|
||||
role: 'viewer',
|
||||
active: false,
|
||||
mustChangePassword: false
|
||||
});
|
||||
|
||||
const orgConfig = locals.orgConfig;
|
||||
if (orgConfig.orgInfo.email) {
|
||||
try {
|
||||
await sendRegistrationNotification(
|
||||
orgConfig.orgInfo.name,
|
||||
orgConfig.orgInfo.email,
|
||||
name,
|
||||
email,
|
||||
reason
|
||||
);
|
||||
} catch {
|
||||
// Email failure shouldn't block registration
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user