import bcrypt from "bcryptjs"; import { randomBytes } from "crypto"; import { prisma } from "../../lib/db"; import { makeResponse } from "../../lib/response"; import { ERROR_MESSAGES } from "../../lib/errors"; import { encrypt, decrypt } from "../../lib/encryption"; import { updateWebhookSecret, validateGiteaUrl, validateGiteaToken } from "../../services/gitea"; import { validateAwsCredentials } from "../../services/ec2"; import { env } from "../../lib/env"; function requireAuth(ctr: any) { const auth = ctr.getAuth?.(); if (!auth?.success) return null; return auth.user; } async function getOrCreateWebhookToken(userId: number) { const existing = await prisma.webhookToken.findUnique({ where: { userId } }); if (existing) return existing; const secret = randomBytes(32).toString("hex"); return prisma.webhookToken.create({ data: { userId, token: secret } }); } async function syncRegisteredRepoWebhooks(userId: number, fullUser: any, secret: string) { if (!fullUser?.giteaInstanceUrl || !fullUser?.giteaPAT) { return { updated: 0, errors: [] as string[] }; } const configs = await prisma.repoConfig.findMany({ where: { userId, giteaWebhookId: { not: null } }, }); const webhookUrl = `${env.PP_BASE_URL}/webhook/${userId}`; const errors: string[] = []; let updated = 0; for (const config of configs) { try { await updateWebhookSecret(fullUser, config.repoOwner, config.repoName, config.giteaWebhookId!, webhookUrl, secret); updated++; } catch (e: any) { errors.push(`${config.repoOwner}/${config.repoName}: ${e.message}`); } } return { updated, errors }; } export async function getUserSettings(ctr: any) { const user = requireAuth(ctr); if (!user) return makeResponse({ ctr, content: { code: ERROR_MESSAGES.UNAUTHORIZED.code, message: ERROR_MESSAGES.UNAUTHORIZED.message } }); const webhookToken = await prisma.webhookToken.findUnique({ where: { userId: user.id } }); const fullUser = await prisma.user.findUnique({ where: { id: user.id } }); return makeResponse({ ctr, content: { code: 200, data: { id: user.id, username: user.username, giteaUsername: fullUser?.giteaUsername, giteaInstanceUrl: fullUser?.giteaInstanceUrl, giteaPatSet: !!fullUser?.giteaPAT, awsAccessKeyId: fullUser?.awsAccessKeyId ? "****" : null, awsRegion: fullUser?.awsRegion, webhookUrl: `${env.PP_BASE_URL}/webhook/${user.id}`, webhookSecret: webhookToken?.token ? "••••••••" : null, webhookTokenExists: !!webhookToken, } } }); } export async function updateUsername(ctr: any) { const user = requireAuth(ctr); if (!user) return makeResponse({ ctr, content: { code: 401, message: ERROR_MESSAGES.UNAUTHORIZED.message } }); const body = await ctr.body(); const { username } = body || {}; if (!username || typeof username !== "string") return makeResponse({ ctr, content: { code: 400, message: "Username required" } }); const existing = await prisma.user.findFirst({ where: { username, id: { not: user.id } } }); if (existing) return makeResponse({ ctr, content: { code: 409, message: "Username already taken" } }); await prisma.user.update({ where: { id: user.id }, data: { username } }); return makeResponse({ ctr, content: { code: 200, message: "Username updated" } }); } export async function updatePassword(ctr: any) { const user = requireAuth(ctr); if (!user) return makeResponse({ ctr, content: { code: 401, message: ERROR_MESSAGES.UNAUTHORIZED.message } }); const body = await ctr.body(); const { currentPassword, newPassword } = body || {}; if (!currentPassword || !newPassword) return makeResponse({ ctr, content: { code: 400, message: "Current and new passwords required" } }); const fullUser = await prisma.user.findUnique({ where: { id: user.id } }); if (!fullUser) return makeResponse({ ctr, content: { code: 404, message: "User not found" } }); const valid = await bcrypt.compare(currentPassword, fullUser.passwordHash); if (!valid) return makeResponse({ ctr, content: { code: 401, message: "Current password incorrect" } }); const hash = await bcrypt.hash(newPassword, 12); await prisma.user.update({ where: { id: user.id }, data: { passwordHash: hash } }); return makeResponse({ ctr, content: { code: 200, message: "Password updated" } }); } export async function updateGitea(ctr: any) { const user = requireAuth(ctr); if (!user) return makeResponse({ ctr, content: { code: 401, message: ERROR_MESSAGES.UNAUTHORIZED.message } }); const body = await ctr.body(); const { giteaInstanceUrl, giteaUsername, giteaPAT } = body || {}; if (!giteaInstanceUrl || !giteaUsername) { return makeResponse({ ctr, content: { code: 400, message: "Gitea URL and username required" } }); } const cleanUrl = giteaInstanceUrl.replace(/\/+$/, ""); const validation = await validateGiteaUrl(cleanUrl); if (!validation.success) { return makeResponse({ ctr, content: { code: 400, message: `Gitea validation failed: ${validation.error}` } }); } const data: any = { giteaInstanceUrl: cleanUrl, giteaUsername }; if (giteaPAT) { const tokenCheck = await validateGiteaToken(cleanUrl, giteaPAT, giteaUsername); if (!tokenCheck.success) { return makeResponse({ ctr, content: { code: 400, message: `Gitea token rejected: ${tokenCheck.error}` } }); } data.giteaPAT = encrypt(giteaPAT); } const updatedUser = await prisma.user.update({ where: { id: user.id }, data }); const webhookToken = await getOrCreateWebhookToken(user.id); const hookSync = await syncRegisteredRepoWebhooks(user.id, updatedUser, webhookToken.token); return makeResponse({ ctr, content: { code: 200, message: hookSync.errors.length ? `Connected to Gitea ${validation.version}; ${hookSync.updated} webhooks updated, ${hookSync.errors.length} failed` : hookSync.updated > 0 ? `Connected to Gitea ${validation.version}; ${hookSync.updated} webhooks updated` : `Connected to Gitea ${validation.version}`, data: { version: validation.version, hookSync }, } }); } export async function updateAws(ctr: any) { const user = requireAuth(ctr); if (!user) return makeResponse({ ctr, content: { code: 401, message: ERROR_MESSAGES.UNAUTHORIZED.message } }); const body = await ctr.body(); const awsAccessKeyId = typeof body?.awsAccessKeyId === "string" ? body.awsAccessKeyId.trim() : body?.awsAccessKeyId; const awsSecretAccessKey = typeof body?.awsSecretAccessKey === "string" ? body.awsSecretAccessKey.trim() : body?.awsSecretAccessKey; const awsRegion = typeof body?.awsRegion === "string" ? body.awsRegion.trim() : body?.awsRegion; if (!awsAccessKeyId || !awsSecretAccessKey || !awsRegion) { return makeResponse({ ctr, content: { code: 400, message: "AWS credentials and region required" } }); } const tempUser = { ...user, awsAccessKeyId: encrypt(awsAccessKeyId), awsSecretAccessKey: encrypt(awsSecretAccessKey), awsRegion, }; const validation = await validateAwsCredentials(tempUser as any); if (!validation.success) { return makeResponse({ ctr, content: { code: 400, message: `AWS validation failed: ${validation.error}` } }); } await prisma.user.update({ where: { id: user.id }, data: { awsAccessKeyId: encrypt(awsAccessKeyId), awsSecretAccessKey: encrypt(awsSecretAccessKey), awsRegion, }, }); return makeResponse({ ctr, content: { code: 200, message: `Connected as ${validation.arn}`, data: { arn: validation.arn } } }); } export async function skipSetupWizard(ctr: any) { const user = requireAuth(ctr); if (!user) return makeResponse({ ctr, content: { code: 401, message: ERROR_MESSAGES.UNAUTHORIZED.message } }); await prisma.user.update({ where: { id: user.id }, data: { setupSkipped: true }, }); return makeResponse({ ctr, content: { code: 200, message: "Setup wizard skipped" } }); } export async function getWebhookSecret(ctr: any) { const user = requireAuth(ctr); if (!user) return makeResponse({ ctr, content: { code: 401, message: ERROR_MESSAGES.UNAUTHORIZED.message } }); const token = await getOrCreateWebhookToken(user.id); return makeResponse({ ctr, content: { code: 200, data: { token: token.token, webhookUrl: `${env.PP_BASE_URL}/webhook/${user.id}` } } }); } export async function regenerateWebhookSecret(ctr: any) { const user = requireAuth(ctr); if (!user) return makeResponse({ ctr, content: { code: 401, message: ERROR_MESSAGES.UNAUTHORIZED.message } }); const newSecret = randomBytes(32).toString("hex"); await prisma.webhookToken.upsert({ where: { userId: user.id }, update: { token: newSecret }, create: { userId: user.id, token: newSecret }, }); const fullUser = await prisma.user.findUnique({ where: { id: user.id } }); if (!fullUser?.giteaInstanceUrl) { return makeResponse({ ctr, content: { code: 200, message: "Secret regenerated (no Gitea hooks to update)", data: { token: newSecret } } }); } const hookSync = await syncRegisteredRepoWebhooks(user.id, fullUser, newSecret); return makeResponse({ ctr, content: { code: 200, message: hookSync.errors.length ? `Secret regenerated with ${hookSync.errors.length} hook update errors` : "Secret regenerated and all hooks updated", data: { token: newSecret, errors: hookSync.errors, hookSync }, } }); }