wol-dash/src/routes/login/+page.server.ts

111 lines
3.0 KiB
TypeScript

import { env } from '$env/dynamic/private';
import { INTERNAL_ERR, PARSE_ERROR } from '$lib/server/commonResponses';
import { users } from '$lib/server/db';
import { createSession } from '$lib/server/sessions';
import { fail, redirect } from '@sveltejs/kit';
import bcrypt from 'bcryptjs';
import humanizeDuration from 'humanize-duration';
import { z } from 'zod';
import type { Actions } from './$types';
type RateLimitData = { attempts: number; blockedUntil: number; timeout: NodeJS.Timeout };
const rateLimits = new Map<string, RateLimitData>();
const delays = [
60, //
60 * 2,
60 * 5,
60 * 15,
60 * 30,
60 * 60,
];
const freeAttempts = 2;
function getBackoff(n: number) {
if (n < freeAttempts) return 0;
return delays[Math.min(n - freeAttempts, delays.length) - 1] * 1000;
}
export const actions = {
default: async ({ cookies, request, locals: { guard }, getClientAddress }) => {
const ip =
env.USE_REVERSE_PROXY != 'false'
? request.headers.get(env.REAL_IP_HEADER)
: getClientAddress();
if (!ip) {
console.error(
'Could not get client IP -- check .env, are the USE_REVERSE_PROXY and REAL_IP_HEADER set correctly?',
);
return INTERNAL_ERR;
}
const limit = rateLimits.get(ip);
if (limit && Date.now() < limit.blockedUntil) {
return fail(429, {
error:
'Too many attempts. Try again in ' +
humanizeDuration(limit.blockedUntil - Date.now(), { round: true }),
});
}
if (!guard.requiresAuth().isFailed()) {
redirect(302, '/dash');
}
const schema = z.object({
username: z.string({ message: 'Username is required.' }).trim(),
password: z.string({ message: 'Password is required.' }),
});
const data = await request.formData();
const parsed = schema.safeParse(Object.fromEntries(data.entries()));
if (!parsed.success) {
return PARSE_ERROR(parsed.error);
}
const user = await users.getByName(parsed.data.username);
if (!user || !bcrypt.compareSync(parsed.data.password, user.password)) {
let attempts = limit?.attempts != undefined ? limit.attempts + 1 : 1;
let blockedUntil = Date.now() + getBackoff(attempts);
clearTimeout(limit?.timeout);
rateLimits.set(ip, {
attempts,
blockedUntil,
timeout: setTimeout(() => rateLimits.delete(ip), 86_400_000), // 24h after last attempt, forget about it
});
return fail(403, {
error: 'Could not sign in. Please verify your username/password are correct.',
blockedUntil,
});
}
cookies.set(
'session',
createSession({
userAgent: request.headers.get('user-agent') ?? 'UNKNOWN',
userId: user.id,
ip,
}),
{
path: '/',
httpOnly: true,
secure: env.USE_SECURE != 'false',
sameSite: true,
maxAge: parseInt(env.SESSION_LIFETIME) || 86400,
},
);
clearTimeout(limit?.timeout);
rateLimits.delete(ip);
redirect(302, '/dash');
},
} satisfies Actions;