import { Challenge } from '$lib/server/challenge.js'; import { Changesets } from '$lib/server/changesets.js'; import { Users } from '$lib/server/users/index.js'; import { TimeOTP } from '$lib/server/users/totp.js'; import { error, fail, redirect, type Actions } from '@sveltejs/kit'; import { RateLimiter } from 'sveltekit-rate-limiter/server'; interface LoginParams { email: string; password: string; challenge: string; otpCode: string; } interface LoginChallenge { email: string; } const limiter = new RateLimiter({ IP: [6, '45s'] }); export const actions = { default: async (event) => { const { request, locals, url } = event; if (await limiter.isLimited(event)) throw error(429); // Redirect const redirectUrl = url.searchParams.has('redirectTo') ? (url.searchParams.get('redirectTo') as string) : '/'; // Already logged in if (locals.session.data?.user) { return redirect(303, redirectUrl); } // TODO: Audit log failed attempts const body = await request.formData(); const { email, password, challenge, otpCode } = Changesets.take( ['email', 'password', 'challenge', 'otpCode'], body ); if (!email) { return fail(400, { incorrect: true }); } if (!password && (!challenge || !otpCode)) { return fail(400, { incorrect: true }); } // Find existing active user const loginUser = await Users.getByLogin(email); if (!loginUser) { return fail(400, { email, incorrect: true }); } // Check OTP challenge if (challenge && otpCode) { const userOtp = await TimeOTP.getUserOtp(loginUser); if (!userOtp) { return fail(400, { email, incorrect: true }); } try { const valid = await Challenge.verifyChallenge( challenge, loginUser.uuid, otpCode, userOtp.token ); if (email !== valid.email) { return fail(400, { email, incorrect: true }); } } catch { return fail(400, { email, incorrect: true }); } } else { // Compare user password if (!loginUser || !password || !(await Users.validatePassword(loginUser, password))) { return fail(400, { email, incorrect: true }); } } // Issue two-factor challenge if (!challenge) { const isOtp = await TimeOTP.isUserOtp(loginUser); if (isOtp) { const issued = await Challenge.issueChallenge({ email }, loginUser.uuid); return { otpRequired: issued, email }; } } // Create session data for user const sessionUser = await Users.toSession(loginUser); await locals.session.set({ user: sessionUser }); return redirect(303, redirectUrl); } } as Actions; export const load = async ({ locals, url }) => { if (locals.session.data?.user) { return redirect(301, url.searchParams.get('redirectTo') || '/'); } // Activation routine if (url.searchParams.has('activate')) { const activationInfo = await Users.getActivationToken( url.searchParams.get('activate') as string ); if (!activationInfo?.user) { return { activated: false }; } await Users.activateUserBy(activationInfo.token, activationInfo.user); return { activated: true }; } return { activated: null }; };