231 lines
9.3 KiB
TypeScript
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 },
|
|
}
|
|
});
|
|
}
|