1import type { RequestHandler } from '@sveltejs/kit';
2import type { RegisterResponseSuccess, RegisterResponseError } from '$lib/types/payloads';
3import db from '$lib/db/db.server';
4import { users } from '$lib/db/schema';
5import { hashPassword } from '$lib/utils/password';
6
7// In-memory map to track failed captcha attempts per IP
8const captchaAttempts = new Map<string, { count: number, lastAttempt: number }>();
9const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
10
11export const POST: RequestHandler = async ({ request }) => {
12 console.log("[Register] New registration attempt received");
13
14 // Only allow POST
15 if (request.method !== 'POST') {
16 return new Response(JSON.stringify({ error: 'Method Not Allowed' } as RegisterResponseError), { status: 405 });
17 }
18
19 let body: unknown;
20 try {
21 body = await request.json();
22 } catch {
23 console.log("[Register] Invalid JSON payload received");
24 return new Response(JSON.stringify({ error: 'Invalid JSON' } as RegisterResponseError), { status: 400 });
25 }
26
27 const { suUsername, suPassword, suConfirmPassword, captchaAnswer } = body as { suUsername: string, suPassword: string, suConfirmPassword: string, captchaAnswer: string };
28
29 // Destructure and validate required fields from the payload.
30 if (typeof suUsername !== 'string' || typeof suPassword !== 'string' ||
31 typeof suConfirmPassword !== 'string' || typeof captchaAnswer !== 'string') {
32 console.log("[Register] Missing or invalid input fields");
33 return new Response(JSON.stringify({ error: 'Missing or invalid input fields' } as RegisterResponseError), { status: 400 });
34 }
35
36 // Trim input values.
37 const username = suUsername.trim();
38 const password = suPassword.trim();
39 const confirmPassword = suConfirmPassword.trim();
40 const captcha = captchaAnswer.trim().toLowerCase();
41
42 // Basic validations.
43 if (!username || !password || !confirmPassword || !captcha) {
44 return new Response(JSON.stringify({ error: 'All fields must be filled' } as RegisterResponseError), { status: 400 });
45 }
46
47 if (username.length > 32) {
48 return new Response(JSON.stringify({ error: 'Username must be at most 32 characters' } as RegisterResponseError), { status: 400 });
49 }
50 // Only allow letters, numbers, underscores, and dashes in the username.
51 const usernameRegex = /^[a-zA-Z0-9_-]+$/;
52 if (!usernameRegex.test(username)) {
53 return new Response(JSON.stringify({ error: 'Invalid username. Only letters, numbers, underscores, and dashes are allowed.' } as RegisterResponseError), { status: 400 });
54 }
55
56 if (password !== confirmPassword) {
57 return new Response(JSON.stringify({ error: 'Passwords do not match' } as RegisterResponseError), { status: 400 });
58 }
59
60 if (password.length < 8 || password.length > 64) {
61 return new Response(JSON.stringify({ error: 'Password must be between 8 and 64 characters' } as RegisterResponseError), { status: 400 });
62 }
63
64 // Captcha validation logic.
65 // Expected captcha answer.
66 const expectedCaptcha = "point de rencontre";
67 // Get the IP address for rate limiting. In production, a more reliable method should be used.
68 const ip = request.headers.get('x-forwarded-for') || 'unknown';
69 const maskedIp = ip.split('.').map((octet, idx) => idx < 3 ? 'xxx' : octet).join('.');
70 const now = Date.now();
71 const attemptData = captchaAttempts.get(ip) || { count: 0, lastAttempt: 0 };
72 // If the user has failed three or more times, apply aggressive exponential backoff.
73 if (attemptData.count >= 3) {
74 const delay = Math.pow(2, attemptData.count - 3 + 1) * 1000; // delay in milliseconds
75 console.log(`[Register] Rate limit exceeded for IP ${maskedIp} - applying ${delay}ms delay`);
76 await sleep(delay);
77 }
78 if (captcha !== expectedCaptcha) {
79 attemptData.count++;
80 attemptData.lastAttempt = now;
81 captchaAttempts.set(ip, attemptData);
82 console.log(`[Register] Captcha failed for IP ${maskedIp}. Attempt count: ${attemptData.count}`);
83 const remaining = Math.max(0, 3 - attemptData.count);
84 return new Response(JSON.stringify({ error: `Incorrect captcha answer. ${remaining > 0 ? remaining + " attempt(s) remaining." : "Please wait before trying again."}` } as RegisterResponseError), { status: 400 });
85 }
86 // Reset captcha attempt data on success.
87 if (captcha === expectedCaptcha) {
88 captchaAttempts.delete(ip);
89 }
90
91 // Securely hash the user's password.
92 let hashedPassword: string;
93 try {
94 hashedPassword = await hashPassword(password);
95 } catch {
96 console.log("[Register] Error hashing password");
97 return new Response(JSON.stringify({ error: 'Internal Server Error' } as RegisterResponseError), { status: 500 });
98 }
99 // Insert the new user into the database.
100 try {
101 await db.insert(users).values({
102 password: hashedPassword,
103 nickname: username,
104 createdAt: Date.now()
105 });
106 console.log(`[Register] User ${username} registered successfully`);
107 } catch (err) {
108 console.log("Database insertion error", err);
109 // Assume a duplicate user error if the email (or derived unique field) already exists.
110 return new Response(JSON.stringify({ error: 'User registration failed. Possibly user already exists.' } as RegisterResponseError), { status: 409 });
111 }
112
113 return new Response(JSON.stringify({ success: true } as RegisterResponseSuccess), { status: 201 });
114};