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.debug("POST /api/register called");
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 (err) {
23 console.debug("Invalid JSON", err);
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.debug("Missing or invalid input fields", { suUsername, suPassword, suConfirmPassword, captchaAnswer });
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 now = Date.now();
70 const attemptData = captchaAttempts.get(ip) || { count: 0, lastAttempt: 0 };
71 // If the user has failed three or more times, apply aggressive exponential backoff.
72 if (attemptData.count >= 3) {
73 const delay = Math.pow(2, attemptData.count - 3 + 1) * 1000; // delay in milliseconds
74 console.debug(`Applying exponential backoff delay of ${delay}ms for IP ${ip}`);
75 await sleep(delay);
76 }
77 if (captcha !== expectedCaptcha) {
78 attemptData.count++;
79 attemptData.lastAttempt = now;
80 captchaAttempts.set(ip, attemptData);
81 console.debug(`Captcha failed for IP ${ip}. Attempt count: ${attemptData.count}`);
82 const remaining = Math.max(0, 3 - attemptData.count);
83 return new Response(JSON.stringify({ error: `Incorrect captcha answer. ${remaining > 0 ? remaining + " attempt(s) remaining." : "Please wait before trying again."}` } as RegisterResponseError), { status: 400 });
84 }
85 // Reset captcha attempt data on success.
86 if (captcha === expectedCaptcha) {
87 captchaAttempts.delete(ip);
88 }
89
90 // Securely hash the user's password.
91 let hashedPassword: string;
92 try {
93 hashedPassword = await hashPassword(password);
94 } catch (err) {
95 console.debug("Error hashing password", err);
96 return new Response(JSON.stringify({ error: 'Internal Server Error' } as RegisterResponseError), { status: 500 });
97 }
98 // Insert the new user into the database.
99 try {
100 await db.insert(users).values({
101 password: hashedPassword,
102 nickname: username,
103 createdAt: Date.now()
104 });
105 console.debug(`User ${username} registered successfully`);
106 } catch (err) {
107 console.debug("Database insertion error", err);
108 // Assume a duplicate user error if the email (or derived unique field) already exists.
109 return new Response(JSON.stringify({ error: 'User registration failed. Possibly user already exists.' } as RegisterResponseError), { status: 409 });
110 }
111
112 return new Response(JSON.stringify({ success: true } as RegisterResponseSuccess), { status: 201 });
113};