v0.1.2 944de91

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

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