import { fail, redirect } from '@sveltejs/kit'; import type { Actions, PageServerLoad } from './$types'; import { loginUser, createUser, mergeSessionProgress, createUserSession } from '$lib/server/pocketbase'; import { createAuthToken } from '../../hooks.server'; import { log } from '$lib/server/logger'; import { randomBytes } from 'node:crypto'; const AUTH_COOKIE = 'libnovel_auth'; const ONE_YEAR = 60 * 60 * 24 * 365; export const load: PageServerLoad = async ({ locals }) => { // Already logged in — send to home if (locals.user) { redirect(302, '/'); } return {}; }; export const actions: Actions = { login: async ({ request, cookies, locals }) => { const data = await request.formData(); const username = (data.get('username') as string | null)?.trim() ?? ''; const password = (data.get('password') as string | null) ?? ''; if (!username || !password) { return fail(400, { action: 'login', error: 'Username and password are required.' }); } let user; try { user = await loginUser(username, password); } catch (err) { log.error('auth', 'login unexpected error', { username, err: String(err) }); return fail(500, { action: 'login', error: 'An error occurred. Please try again.' }); } if (!user) { return fail(401, { action: 'login', error: 'Invalid username or password.' }); } // Merge any anonymous session progress into the user's account so that // chapters read before logging in are preserved and portable across devices. mergeSessionProgress(locals.sessionId, user.id).catch((err) => log.warn('auth', 'login: mergeSessionProgress failed (non-fatal)', { err: String(err) }) ); // Create a unique auth session ID for this login const authSessionId = randomBytes(16).toString('hex'); // Record the session in PocketBase (best-effort, non-fatal) const userAgent = request.headers.get('user-agent') ?? ''; const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? request.headers.get('x-real-ip') ?? ''; createUserSession(user.id, authSessionId, userAgent, ip).catch((err) => log.warn('auth', 'login: createUserSession failed (non-fatal)', { err: String(err) }) ); const token = createAuthToken(user.id, user.username, user.role ?? 'user', authSessionId); cookies.set(AUTH_COOKIE, token, { path: '/', httpOnly: true, sameSite: 'lax', maxAge: ONE_YEAR }); redirect(302, '/'); }, register: async ({ request, cookies, locals }) => { const data = await request.formData(); const username = (data.get('username') as string | null)?.trim() ?? ''; const password = (data.get('password') as string | null) ?? ''; const confirm = (data.get('confirm') as string | null) ?? ''; if (!username || !password) { return fail(400, { action: 'register', error: 'Username and password are required.' }); } if (username.length < 3 || username.length > 32) { return fail(400, { action: 'register', error: 'Username must be between 3 and 32 characters.' }); } if (!/^[a-zA-Z0-9_-]+$/.test(username)) { return fail(400, { action: 'register', error: 'Username may only contain letters, numbers, underscores and hyphens.' }); } if (password.length < 8) { return fail(400, { action: 'register', error: 'Password must be at least 8 characters.' }); } if (password !== confirm) { return fail(400, { action: 'register', error: 'Passwords do not match.' }); } let user; try { user = await createUser(username, password); } catch (err: unknown) { const msg = err instanceof Error ? err.message : 'Registration failed.'; if (msg.includes('Username already taken')) { return fail(409, { action: 'register', error: 'That username is already taken.' }); } log.error('auth', 'register unexpected error', { username, err: String(err) }); return fail(500, { action: 'register', error: 'An error occurred. Please try again.' }); } // Merge any anonymous session progress into the newly created account. mergeSessionProgress(locals.sessionId, user.id).catch((err) => log.warn('auth', 'register: mergeSessionProgress failed (non-fatal)', { err: String(err) }) ); // Create a unique auth session ID for this registration const authSessionId = randomBytes(16).toString('hex'); // Record the session in PocketBase (best-effort, non-fatal) const userAgent = request.headers.get('user-agent') ?? ''; const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? request.headers.get('x-real-ip') ?? ''; createUserSession(user.id, authSessionId, userAgent, ip).catch((err) => log.warn('auth', 'register: createUserSession failed (non-fatal)', { err: String(err) }) ); const token = createAuthToken(user.id, user.username, user.role ?? 'user', authSessionId); cookies.set(AUTH_COOKIE, token, { path: '/', httpOnly: true, sameSite: 'lax', maxAge: ONE_YEAR }); redirect(302, '/'); } };