1import type { RequestHandler } from '@sveltejs/kit';
2import type { LoginResponseSuccess, LoginResponseError } from '$lib/types/payloads';
3import db from '$lib/db/db.server';
4import { users } from '$lib/db/schema';
5import { eq } from 'drizzle-orm';
6import { verifyPassword } from '$lib/utils/password';
7import { generateSessionToken, createSession } from '$lib/api/session.server';
8import { setSessionTokenCookie } from '$lib/api/session.cookie';
9
10// In-memory map to track failed login attempts per IP
11const loginAttempts = new Map<string, { count: number, lastAttempt: number }>();
12const MAX_ATTEMPTS = 3;
13const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
14
15// Cleanup old entries every hour
16setInterval(() => {
17 const now = Date.now();
18 const ONE_HOUR = 60 * 60 * 1000;
19 for (const [ip, data] of loginAttempts.entries()) {
20 if (now - data.lastAttempt > ONE_HOUR) {
21 loginAttempts.delete(ip);
22 }
23 }
24}, 60 * 60 * 1000);
25
26export const POST: RequestHandler = async ({ request, cookies }) => {
27 console.debug("POST /api/session/login called");
28
29 if (request.method !== 'POST') {
30 return new Response(JSON.stringify({ error: 'Method Not Allowed' } as LoginResponseError), { status: 405 });
31 }
32
33 // Get the IP address for rate limiting
34 const ip = request.headers.get('x-forwarded-for') || 'unknown';
35 const now = Date.now();
36 const attemptData = loginAttempts.get(ip) || { count: 0, lastAttempt: 0 };
37
38 // If the user has failed three or more times, apply exponential backoff
39 if (attemptData.count >= MAX_ATTEMPTS) {
40 const delay = Math.pow(2, attemptData.count - MAX_ATTEMPTS + 1) * 1000; // delay in milliseconds
41 console.debug(`Applying exponential backoff delay of ${delay}ms for IP ${ip}`);
42 await sleep(delay);
43 }
44
45 let body: unknown;
46 try {
47 body = await request.json();
48 } catch (err) {
49 console.debug("Invalid JSON", err);
50 return new Response(JSON.stringify({ error: 'Invalid JSON' } as LoginResponseError), { status: 400 });
51 }
52
53 const { username, password } = body as { username: string, password: string };
54
55 if (typeof username !== 'string' || typeof password !== 'string') {
56 console.debug("Missing or invalid input fields", { username, password });
57 return new Response(JSON.stringify({ error: 'Missing or invalid input fields' } as LoginResponseError), { status: 400 });
58 }
59
60 // Find user by username
61 const user = await db.query.users.findFirst({
62 where: eq(users.nickname, username.trim())
63 });
64
65 if (!user) {
66 attemptData.count++;
67 attemptData.lastAttempt = now;
68 loginAttempts.set(ip, attemptData);
69 console.debug(`Login failed for IP ${ip}. Attempt count: ${attemptData.count}`);
70 const remaining = Math.max(0, MAX_ATTEMPTS - attemptData.count);
71 return new Response(JSON.stringify({
72 error: `Invalid username or password. ${remaining > 0 ? remaining + " attempt(s) remaining." : "Please wait before trying again."}`
73 } as LoginResponseError), { status: 401 });
74 }
75
76 // Verify password
77 try {
78 const isValid = await verifyPassword(password.trim(), user.password);
79 if (!isValid) {
80 attemptData.count++;
81 attemptData.lastAttempt = now;
82 loginAttempts.set(ip, attemptData);
83 console.debug(`Login failed for IP ${ip}. Attempt count: ${attemptData.count}`);
84 const remaining = Math.max(0, MAX_ATTEMPTS - attemptData.count);
85 return new Response(JSON.stringify({
86 error: `Invalid username or password. ${remaining > 0 ? remaining + " attempt(s) remaining." : "Please wait before trying again."}`
87 } as LoginResponseError), { status: 401 });
88 }
89 } catch (err) {
90 console.debug("Error verifying password", err);
91 return new Response(JSON.stringify({ error: 'Internal Server Error' } as LoginResponseError), { status: 500 });
92 }
93
94 // Reset login attempt data on success
95 loginAttempts.delete(ip);
96
97 // Generate session token and create session
98 const token = generateSessionToken();
99 const session = await createSession(token, user.id);
100
101 // Update user status to online
102 await db.update(users)
103 .set({
104 status: 'online',
105 lastSeen: Date.now()
106 })
107 .where(eq(users.id, user.id));
108
109 // Set session cookie
110 setSessionTokenCookie({ cookies }, token, session.expiresAt);
111
112 return new Response(JSON.stringify({
113 success: true,
114 user: {
115 id: user.id,
116 nickname: user.nickname,
117 status: 'online',
118 avatarUrl: user.avatarUrl,
119 lastSeen: Date.now()
120 }
121 } as LoginResponseSuccess), {
122 status: 200
123 });
124};