import { randomBytes } from "crypto"; import bcrypt from "bcryptjs"; import { Cookie } from "rjweb-server"; import { prisma } from "../lib/db"; import { makeResponse } from "../lib/response"; import { ERROR_MESSAGES } from "../lib/errors"; import { createLogger } from "../lib/logger"; const log = createLogger("AUTH_ROUTE"); const COOKIE_NAME = "pp_session"; const COOKIE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; export async function loginHandler(ctr: any) { let body: any; try { body = await ctr.body(); } catch { return makeResponse({ ctr, content: { code: 400, message: "Invalid body" } }); } const { username, password } = body || {}; if (!username || !password) { return makeResponse({ ctr, content: { code: 400, message: "Username and password required" } }); } const user = await prisma.user.findUnique({ where: { username } }); if (!user) { return makeResponse({ ctr, content: { code: 401, message: "Invalid credentials" } }); } const valid = await bcrypt.compare(password, user.passwordHash); if (!valid) { return makeResponse({ ctr, content: { code: 401, message: "Invalid credentials" } }); } const sessionHash = randomBytes(32).toString("hex"); await prisma.session.create({ data: { hash: sessionHash, userId: user.id } }); ctr.cookies.set( COOKIE_NAME, new Cookie(sessionHash, { httpOnly: true, expires: new Date(Date.now() + COOKIE_MAX_AGE_MS), path: "/", sameSite: "lax", }), ); return makeResponse({ ctr, content: { code: 200, data: { id: user.id, username: user.username, isAdmin: user.isAdmin } } }); } export async function logoutHandler(ctr: any) { const auth = ctr.getAuth?.(); if (auth?.success) { await prisma.session.delete({ where: { id: auth.sessionId } }).catch(() => {}); } ctr.cookies.set(COOKIE_NAME, new Cookie("", { expires: new Date(0), path: "/" })); return makeResponse({ ctr, content: { code: 200, message: "Logged out" } }); } export async function meHandler(ctr: any) { const auth = ctr.getAuth?.(); if (!auth?.success) { return makeResponse({ ctr, content: { code: ERROR_MESSAGES.UNAUTHORIZED.code, message: ERROR_MESSAGES.UNAUTHORIZED.message } }); } const user = await prisma.user.findUnique({ where: { id: auth.user.id } }); if (!user) return makeResponse({ ctr, content: { code: 404, message: "User not found" } }); return makeResponse({ ctr, content: { code: 200, data: { id: user.id, username: user.username, isAdmin: user.isAdmin, isFounder: user.isFounder, giteaUsername: user.giteaUsername, giteaInstanceUrl: user.giteaInstanceUrl, giteaPatSet: !!user.giteaPAT, awsAccessKeyId: user.awsAccessKeyId ? "****" : null, awsRegion: user.awsRegion, awsConfigured: !!(user.awsAccessKeyId && user.awsSecretAccessKey && user.awsRegion), setupComplete: !!(user.giteaInstanceUrl && user.giteaPAT && user.awsAccessKeyId && user.awsSecretAccessKey && user.awsRegion), }, }, }); } export async function setupStatusHandler(ctr: any) { const count = await prisma.user.count(); return makeResponse({ ctr, content: { code: 200, data: { needsSetup: count === 0 } } }); } export async function firstUserHandler(ctr: any) { const count = await prisma.user.count(); if (count > 0) { return makeResponse({ ctr, content: { code: 403, message: "Setup already completed. Contact an administrator to create your account." } }); } let body: any; try { body = await ctr.body(); } catch { return makeResponse({ ctr, content: { code: 400, message: "Invalid body" } }); } const { username, password } = body || {}; if (!username || !password || password.length < 8) { return makeResponse({ ctr, content: { code: 400, message: "Username and password (min 8 chars) required" } }); } const hash = await bcrypt.hash(password, 12); const user = await prisma.user.create({ data: { username, passwordHash: hash, isAdmin: true, isFounder: true }, }); // Auto-create a webhook token so it's immediately available in the setup wizard const webhookSecret = randomBytes(32).toString("hex"); await prisma.webhookToken.create({ data: { userId: user.id, token: webhookSecret } }); const sessionHash = randomBytes(32).toString("hex"); await prisma.session.create({ data: { hash: sessionHash, userId: user.id } }); ctr.cookies.set( COOKIE_NAME, new Cookie(sessionHash, { httpOnly: true, expires: new Date(Date.now() + COOKIE_MAX_AGE_MS), path: "/", sameSite: "lax", }), ); log.info({ username }, "First user (founder) created"); return makeResponse({ ctr, content: { code: 201, data: { id: user.id, username: user.username, isAdmin: true, isFounder: true } } }); }