v0.1 64c03e0

+server.ts RawHistoryFolder

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};

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 8

↑↓ moveenter openesc close