v0.1.1 88fcae1

+server.ts RawHistoryFolder

1import type { RequestHandler } from '@sveltejs/kit';
2import type { LoginResponseSuccess, LoginResponseError } from '$lib/types/payloads';
3import db from '$lib/db/db.server';
4import { users } from '$lib/db/schema';
5import { eq } from 'drizzle-orm';
6import { verifyPassword } from '$lib/utils/password';
7import { generateSessionToken, createSession } from '$lib/api/session.server';
8import { setSessionTokenCookie } from '$lib/api/session.cookie';
9
10// In-memory map to track failed login attempts per IP
11const loginAttempts = new Map<string, { count: number, lastAttempt: number }>();
12const MAX_ATTEMPTS = 3;
13const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
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 loginAttempts.entries()) {
20        if (now - data.lastAttempt > ONE_HOUR) {
21            loginAttempts.delete(ip);
22        }
23    }
24}, 60 * 60 * 1000);
25
26export const POST: RequestHandler = async ({ request, cookies }) => {
27    console.debug("POST /api/session/login called");
28
29    if (request.method !== 'POST') {
30        return new Response(JSON.stringify({ error: 'Method Not Allowed' } as LoginResponseError), { status: 405 });
31    }
32
33    // Get the IP address for rate limiting
34    const ip = request.headers.get('x-forwarded-for') || 'unknown';
35    const now = Date.now();
36    const attemptData = loginAttempts.get(ip) || { count: 0, lastAttempt: 0 };
37
38    // If the user has failed three or more times, apply exponential backoff
39    if (attemptData.count >= MAX_ATTEMPTS) {
40        const delay = Math.pow(2, attemptData.count - MAX_ATTEMPTS + 1) * 1000; // delay in milliseconds
41        console.debug(`Applying exponential backoff delay of ${delay}ms for IP ${ip}`);
42        await sleep(delay);
43    }
44
45    let body: unknown;
46    try {
47        body = await request.json();
48    } catch (err) {
49        console.debug("Invalid JSON", err);
50        return new Response(JSON.stringify({ error: 'Invalid JSON' } as LoginResponseError), { status: 400 });
51    }
52
53    const { username, password } = body as { username: string, password: string };
54
55    if (typeof username !== 'string' || typeof password !== 'string') {
56        console.debug("Missing or invalid input fields", { username, password });
57        return new Response(JSON.stringify({ error: 'Missing or invalid input fields' } as LoginResponseError), { status: 400 });
58    }
59
60    // Find user by username
61    const user = await db.query.users.findFirst({
62        where: eq(users.nickname, username.trim())
63    });
64
65    if (!user) {
66        attemptData.count++;
67        attemptData.lastAttempt = now;
68        loginAttempts.set(ip, attemptData);
69        console.debug(`Login failed for IP ${ip}. Attempt count: ${attemptData.count}`);
70        const remaining = Math.max(0, MAX_ATTEMPTS - attemptData.count);
71        return new Response(JSON.stringify({ 
72            error: `Invalid username or password. ${remaining > 0 ? remaining + " attempt(s) remaining." : "Please wait before trying again."}` 
73        } as LoginResponseError), { status: 401 });
74    }
75
76    // Verify password
77    try {
78        const isValid = await verifyPassword(password.trim(), user.password);
79        if (!isValid) {
80            attemptData.count++;
81            attemptData.lastAttempt = now;
82            loginAttempts.set(ip, attemptData);
83            console.debug(`Login failed for IP ${ip}. Attempt count: ${attemptData.count}`);
84            const remaining = Math.max(0, MAX_ATTEMPTS - attemptData.count);
85            return new Response(JSON.stringify({ 
86                error: `Invalid username or password. ${remaining > 0 ? remaining + " attempt(s) remaining." : "Please wait before trying again."}` 
87            } as LoginResponseError), { status: 401 });
88        }
89    } catch (err) {
90        console.debug("Error verifying password", err);
91        return new Response(JSON.stringify({ error: 'Internal Server Error' } as LoginResponseError), { status: 500 });
92    }
93
94    // Reset login attempt data on success
95    loginAttempts.delete(ip);
96
97    // Generate session token and create session
98    const token = generateSessionToken();
99    const session = await createSession(token, user.id);
100
101    // Update user status to online
102    await db.update(users)
103        .set({ 
104            status: 'online',
105            lastSeen: Date.now()
106        })
107        .where(eq(users.id, user.id));
108
109    // Set session cookie
110    setSessionTokenCookie({ cookies }, token, session.expiresAt);
111
112    return new Response(JSON.stringify({ 
113        success: true,
114        user: {
115            id: user.id,
116            nickname: user.nickname,
117            status: 'online',
118            avatarUrl: user.avatarUrl,
119            lastSeen: Date.now()
120        }
121    } as LoginResponseSuccess), { 
122        status: 200 
123    });
124}; 

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