Files
pr-preview/backend/src/routes/api/user.ts
T
space a1c7ad7120
Deploy / Build (pull_request) Successful in 33s
Deploy / Build and Push Docker Image (pull_request) Has been skipped
feat(setup): add ability to skip setup wizard and update user model
2026-07-26 17:06:05 +02:00

231 lines
9.3 KiB
TypeScript

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 },
}
});
}