diff --git a/docker-compose.yml b/docker-compose.yml index dd81221..d4c89df 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -290,6 +290,7 @@ services: POCKETBASE_ADMIN_EMAIL: "${POCKETBASE_ADMIN_EMAIL}" POCKETBASE_ADMIN_PASSWORD: "${POCKETBASE_ADMIN_PASSWORD}" AUTH_SECRET: "${AUTH_SECRET}" + DEBUG_LOGIN_TOKEN: "${DEBUG_LOGIN_TOKEN}" PUBLIC_MINIO_PUBLIC_URL: "${MINIO_PUBLIC_ENDPOINT}" # Valkey VALKEY_ADDR: "valkey:6379" diff --git a/ui/src/routes/api/auth/debug-login/+server.ts b/ui/src/routes/api/auth/debug-login/+server.ts new file mode 100644 index 0000000..e332402 --- /dev/null +++ b/ui/src/routes/api/auth/debug-login/+server.ts @@ -0,0 +1,73 @@ +import { redirect, error } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { getUserByUsername, createUserSession } from '$lib/server/pocketbase'; +import { createAuthToken } from '../../../../hooks.server'; +import { env } from '$env/dynamic/private'; +import { log } from '$lib/server/logger'; +import { randomBytes } from 'node:crypto'; + +const AUTH_COOKIE = 'libnovel_auth'; +const ONE_YEAR = 60 * 60 * 24 * 365; + +/** + * GET /api/auth/debug-login?token=&username= + * + * One-shot debug bypass: verifies a shared secret token, then mints a real + * auth cookie for the given user (defaults to the first admin account) and + * redirects to /. + * + * Requires DEBUG_LOGIN_TOKEN env var to be set. Disabled (404) when the var + * is absent or empty. + */ +export const GET: RequestHandler = async ({ url, cookies, request }) => { + const debugToken = env.DEBUG_LOGIN_TOKEN ?? ''; + if (!debugToken) { + error(404, 'Not found'); + } + + const provided = url.searchParams.get('token') ?? ''; + // Constant-time comparison to prevent timing attacks + if (provided.length !== debugToken.length) { + log.warn('api/auth/debug-login', 'bad token attempt'); + error(401, 'Invalid token'); + } + let diff = 0; + for (let i = 0; i < debugToken.length; i++) { + diff |= provided.charCodeAt(i) ^ debugToken.charCodeAt(i); + } + if (diff !== 0) { + log.warn('api/auth/debug-login', 'bad token attempt'); + error(401, 'Invalid token'); + } + + const username = url.searchParams.get('username') ?? 'kamil_alekber_2e99'; + const user = await getUserByUsername(username); + if (!user) { + error(404, `User '${username}' not found`); + } + + const authSessionId = randomBytes(16).toString('hex'); + const userAgent = request.headers.get('user-agent') ?? ''; + const ip = + request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? + request.headers.get('x-real-ip') ?? + 'debug'; + + createUserSession(user.id, authSessionId, userAgent, ip).catch((e) => + log.warn('api/auth/debug-login', 'createUserSession failed (non-fatal)', { err: String(e) }) + ); + + const token = createAuthToken(user.id, user.username, user.role ?? 'user', authSessionId); + + cookies.set(AUTH_COOKIE, token, { + path: '/', + httpOnly: true, + sameSite: 'lax', + maxAge: ONE_YEAR + }); + + log.info('api/auth/debug-login', 'debug login used', { username: user.username, ip }); + + const next = url.searchParams.get('next') ?? '/'; + redirect(302, next); +};