v0.1.3 ff61cc9

+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';
6import { validateTurnstileToken } from '$lib/utils/turnstile.server';
7import { createLogger } from '$lib/utils/logger.server';
8import { eq } from 'drizzle-orm';
9
10const log = createLogger('register-server');
11
12// In-memory map to track failed captcha attempts per IP
13const captchaAttempts = new Map<string, { count: number, lastAttempt: number }>();
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 captchaAttempts.entries()) {
20		if (now - data.lastAttempt > ONE_HOUR) {
21			captchaAttempts.delete(ip);
22		}
23	}
24}, 60 * 60 * 1000);
25
26export const POST: RequestHandler = async ({ request }) => {
27	log.debug('New registration attempt received');
28
29	if (request.method !== 'POST') {
30		log.warn('Invalid method used', { method: request.method });
31		return new Response(JSON.stringify({ error: 'Method Not Allowed' } as RegisterResponseError), { status: 405 });
32	}
33
34	// Get the IP address for rate limiting and Turnstile validation
35	const ip = request.headers.get('x-forwarded-for') || 'unknown';
36	const maskedIp = ip.split('.').map((octet, idx) => idx < 3 ? 'xxx' : octet).join('.');
37	const now = Date.now();
38	const attemptData = captchaAttempts.get(ip) || { count: 0, lastAttempt: 0 };
39
40	let body: unknown;
41	try {
42		body = await request.json();
43	} catch {
44		log.warn('Invalid JSON payload received');
45		return new Response(JSON.stringify({ error: 'Invalid JSON' } as RegisterResponseError), { status: 400 });
46	}
47
48	const { suUsername, suPassword, suConfirmPassword, captchaAnswer, turnstileToken } = body as { 
49		suUsername: string, 
50		suPassword: string, 
51		suConfirmPassword: string, 
52		captchaAnswer: string,
53		turnstileToken: string 
54	};
55
56	if (!suUsername || !suPassword || !suConfirmPassword || !captchaAnswer || !turnstileToken) {
57		log.warn('Missing required fields', { 
58			hasUsername: Boolean(suUsername),
59			hasPassword: Boolean(suPassword),
60			hasConfirmPassword: Boolean(suConfirmPassword),
61			hasCaptcha: Boolean(captchaAnswer),
62			hasTurnstileToken: Boolean(turnstileToken)
63		});
64		return new Response(JSON.stringify({ error: 'All fields are required' } as RegisterResponseError), { status: 400 });
65	}
66
67	// Validate Turnstile token
68	const isValidTurnstile = await validateTurnstileToken(turnstileToken, ip);
69	if (!isValidTurnstile) {
70		log.warn('Invalid Turnstile token', { maskedIp });
71		return new Response(JSON.stringify({ error: 'Security check failed. Please try again.' } as RegisterResponseError), { status: 400 });
72	}
73
74	// Validate PDR captcha
75	const normalizedAnswer = captchaAnswer.trim().toLowerCase();
76	if (normalizedAnswer !== 'poudlard' && normalizedAnswer !== 'poudlard rp') {
77		attemptData.count++;
78		attemptData.lastAttempt = now;
79		captchaAttempts.set(ip, attemptData);
80		log.warn('Invalid captcha answer', { maskedIp, attemptCount: attemptData.count });
81		return new Response(JSON.stringify({ error: 'Invalid answer to the PDR question' } as RegisterResponseError), { status: 400 });
82	}
83
84	// Reset captcha attempts on success
85	captchaAttempts.delete(ip);
86
87	// Destructure and validate required fields from the payload.
88	if (typeof suUsername !== 'string' || typeof suPassword !== 'string' ||
89	    typeof suConfirmPassword !== 'string') {
90		log.warn('Missing or invalid input fields');
91		return new Response(JSON.stringify({ error: 'Missing or invalid input fields' } as RegisterResponseError), { status: 400 });
92	}
93
94	// Trim input values.
95	const username = suUsername.trim();
96	const password = suPassword.trim();
97	const confirmPassword = suConfirmPassword.trim();
98
99	// Basic validations.
100	if (!username || !password || !confirmPassword) {
101		log.warn('All fields must be filled');
102		return new Response(JSON.stringify({ error: 'All fields must be filled' } as RegisterResponseError), { status: 400 });
103	}
104
105	if (username.length > 32) {
106		log.warn('Username must be at most 32 characters');
107		return new Response(JSON.stringify({ error: 'Username must be at most 32 characters' } as RegisterResponseError), { status: 400 });
108	}
109	// Only allow letters, numbers, underscores, and dashes in the username.
110	const usernameRegex = /^[a-zA-Z0-9_-]+$/;
111	if (!usernameRegex.test(username)) {
112		log.warn('Invalid username. Only letters, numbers, underscores, and dashes are allowed.');
113		return new Response(JSON.stringify({ error: 'Invalid username. Only letters, numbers, underscores, and dashes are allowed.' } as RegisterResponseError), { status: 400 });
114	}
115
116	if (password !== confirmPassword) {
117		log.warn('Passwords do not match');
118		return new Response(JSON.stringify({ error: 'Passwords do not match' } as RegisterResponseError), { status: 400 });
119	}
120
121	if (password.length < 8 || password.length > 64) {
122		log.warn('Password must be between 8 and 64 characters');
123		return new Response(JSON.stringify({ error: 'Password must be between 8 and 64 characters' } as RegisterResponseError), { status: 400 });
124	}
125
126	// Check if nickname already exists
127	const existingUser = await db.select().from(users).where(eq(users.nickname, username));
128	if (existingUser.length > 0) {
129		log.warn('Username already exists', { username });
130		return new Response(JSON.stringify({ error: 'This username is already taken' } as RegisterResponseError), { status: 409 });
131	}
132
133	// Securely hash the user's password.
134	let hashedPassword: string;
135	try {
136		hashedPassword = await hashPassword(password);
137	} catch {
138		log.error('Error hashing password');
139		return new Response(JSON.stringify({ error: 'Internal Server Error' } as RegisterResponseError), { status: 500 });
140	}
141	// Insert the new user into the database.
142	try {
143		await db.insert(users).values({
144			nickname: username,
145			password: hashedPassword,
146			createdAt: Date.now()
147		});
148		log.info(`User ${username} registered successfully`);
149	} catch (err: unknown) {
150		log.error('Database insertion error', { error: err as object });
151		// Assume a duplicate user error if the email (or derived unique field) already exists.
152		return new Response(JSON.stringify({ error: 'User registration failed. Possibly user already exists.' } as RegisterResponseError), { status: 409 });
153	}
154
155	return new Response(JSON.stringify({ success: true } as RegisterResponseSuccess), { status: 201 });
156};

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