feat: initial scaffold - backend, frontend, Prisma schema, Docker

- Prisma schema: User, Session, RepoConfig, Preview, Job, WebhookToken, NoConfigComment, AdminSettings
- Backend: auth (login/logout/me/first-user setup), webhook handler with HMAC verification, EC2 service, SSH service, deploy pipeline, job queue worker, cron workers
- Frontend: Login with first-user detection, Dashboard, PreviewDetail with live log streaming, Settings, Repos config, Admin panel, SetupWizard, Privacy page
- Docker Compose and Dockerfile for self-hosted deployment
- Uses bcryptjs for Node 24 compatibility

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 00:18:08 +02:00
parent ca2efdadea
commit 40d484bede
108 changed files with 13393 additions and 0 deletions
+192
View File
@@ -0,0 +1,192 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var admin_exports = {};
__export(admin_exports, {
adminListPreviews: () => adminListPreviews,
adminStopPreview: () => adminStopPreview,
createUser: () => createUser,
deleteUser: () => deleteUser,
getSettings: () => getSettings,
listUsers: () => listUsers,
updateSettings: () => updateSettings,
updateUser: () => updateUser
});
module.exports = __toCommonJS(admin_exports);
var import_bcryptjs = __toESM(require("bcryptjs"));
var import_db = require("../../lib/db");
var import_response = require("../../lib/response");
var import_errors = require("../../lib/errors");
var import_adminSettings = require("../../lib/adminSettings");
function requireAdmin(ctr) {
const auth = ctr.getAuth?.();
if (!auth?.success) return null;
if (!auth.user.isAdmin) return null;
return auth.user;
}
async function listUsers(ctr) {
const admin = requireAdmin(ctr);
if (!admin) return (0, import_response.makeResponse)({ ctr, content: { code: 403, message: import_errors.ERROR_MESSAGES.FORBIDDEN.message } });
const users = await import_db.prisma.user.findMany({
select: { id: true, username: true, isAdmin: true, isFounder: true, createdAt: true, giteaInstanceUrl: true },
orderBy: { createdAt: "asc" }
});
return (0, import_response.makeResponse)({ ctr, content: { code: 200, data: users } });
}
async function createUser(ctr) {
const admin = requireAdmin(ctr);
if (!admin) return (0, import_response.makeResponse)({ ctr, content: { code: 403, message: import_errors.ERROR_MESSAGES.FORBIDDEN.message } });
const body = await ctr.body();
const { username, password } = body || {};
if (!username || !password) return (0, import_response.makeResponse)({ ctr, content: { code: 400, message: "Username and password required" } });
const existing = await import_db.prisma.user.findUnique({ where: { username } });
if (existing) return (0, import_response.makeResponse)({ ctr, content: { code: 409, message: "Username already taken" } });
const userCount = await import_db.prisma.user.count();
const isFirst = userCount === 0;
const hash = await import_bcryptjs.default.hash(password, 12);
const user = await import_db.prisma.user.create({
data: { username, passwordHash: hash, isAdmin: isFirst, isFounder: isFirst },
select: { id: true, username: true, isAdmin: true, isFounder: true }
});
return (0, import_response.makeResponse)({ ctr, content: { code: 201, data: user } });
}
async function updateUser(ctr) {
const admin = requireAdmin(ctr);
if (!admin) return (0, import_response.makeResponse)({ ctr, content: { code: 403, message: import_errors.ERROR_MESSAGES.FORBIDDEN.message } });
const id = parseInt(ctr.params.get("id") || "0", 10);
const target = await import_db.prisma.user.findUnique({ where: { id } });
if (!target) return (0, import_response.makeResponse)({ ctr, content: { code: 404, message: import_errors.ERROR_MESSAGES.NOT_FOUND.message } });
const body = await ctr.body();
const { username, password, isAdmin } = body || {};
const data = {};
if (username) {
const existing = await import_db.prisma.user.findFirst({ where: { username, id: { not: id } } });
if (existing) return (0, import_response.makeResponse)({ ctr, content: { code: 409, message: "Username taken" } });
data.username = username;
}
if (password) data.passwordHash = await import_bcryptjs.default.hash(password, 12);
if (isAdmin !== void 0 && !target.isFounder) data.isAdmin = Boolean(isAdmin);
await import_db.prisma.user.update({ where: { id }, data });
return (0, import_response.makeResponse)({ ctr, content: { code: 200, message: "User updated" } });
}
async function deleteUser(ctr) {
const admin = requireAdmin(ctr);
if (!admin) return (0, import_response.makeResponse)({ ctr, content: { code: 403, message: import_errors.ERROR_MESSAGES.FORBIDDEN.message } });
const id = parseInt(ctr.params.get("id") || "0", 10);
const target = await import_db.prisma.user.findUnique({ where: { id } });
if (!target) return (0, import_response.makeResponse)({ ctr, content: { code: 404, message: import_errors.ERROR_MESSAGES.NOT_FOUND.message } });
if (target.isFounder) return (0, import_response.makeResponse)({ ctr, content: { code: 403, message: "Cannot delete founder" } });
if (id === admin.id) return (0, import_response.makeResponse)({ ctr, content: { code: 403, message: "Cannot delete yourself" } });
await import_db.prisma.user.delete({ where: { id } });
return (0, import_response.makeResponse)({ ctr, content: { code: 200, message: "User deleted" } });
}
async function getSettings(ctr) {
const admin = requireAdmin(ctr);
if (!admin) return (0, import_response.makeResponse)({ ctr, content: { code: 403, message: import_errors.ERROR_MESSAGES.FORBIDDEN.message } });
const settings = await (0, import_adminSettings.getAdminSettings)();
return (0, import_response.makeResponse)({ ctr, content: { code: 200, data: settings } });
}
async function updateSettings(ctr) {
const admin = requireAdmin(ctr);
if (!admin) return (0, import_response.makeResponse)({ ctr, content: { code: 403, message: import_errors.ERROR_MESSAGES.FORBIDDEN.message } });
const body = await ctr.body();
const {
defaultInstanceType,
maxConcurrentInstancesPerUser,
logSizeLimitBytes,
previewRetentionDays,
webhookRateLimitPerMinute,
contactEmail
} = body || {};
const data = {};
if (defaultInstanceType) data.defaultInstanceType = defaultInstanceType;
if (maxConcurrentInstancesPerUser) data.maxConcurrentInstancesPerUser = Number(maxConcurrentInstancesPerUser);
if (logSizeLimitBytes) data.logSizeLimitBytes = Number(logSizeLimitBytes);
if (previewRetentionDays) data.previewRetentionDays = Number(previewRetentionDays);
if (webhookRateLimitPerMinute) data.webhookRateLimitPerMinute = Number(webhookRateLimitPerMinute);
if (contactEmail !== void 0) data.contactEmail = contactEmail;
await import_db.prisma.adminSettings.upsert({
where: { id: 1 },
update: data,
create: { id: 1, ...data }
});
return (0, import_response.makeResponse)({ ctr, content: { code: 200, message: "Settings updated" } });
}
async function adminListPreviews(ctr) {
const admin = requireAdmin(ctr);
if (!admin) return (0, import_response.makeResponse)({ ctr, content: { code: 403, message: import_errors.ERROR_MESSAGES.FORBIDDEN.message } });
const previews = await import_db.prisma.preview.findMany({
include: {
repoConfig: { include: { user: { select: { id: true, username: true } } } }
},
orderBy: { updatedAt: "desc" },
take: 200
});
return (0, import_response.makeResponse)({
ctr,
content: {
code: 200,
data: previews.map((p) => ({
id: p.id,
prNumber: p.prNumber,
prTitle: p.prTitle,
status: p.status,
instanceIp: p.instanceIp,
port: p.port,
createdAt: p.createdAt,
updatedAt: p.updatedAt,
repoOwner: p.repoConfig.repoOwner,
repoName: p.repoConfig.repoName,
user: p.repoConfig.user
}))
}
});
}
async function adminStopPreview(ctr) {
const admin = requireAdmin(ctr);
if (!admin) return (0, import_response.makeResponse)({ ctr, content: { code: 403, message: import_errors.ERROR_MESSAGES.FORBIDDEN.message } });
const id = parseInt(ctr.params.get("id") || "0", 10);
const preview = await import_db.prisma.preview.findUnique({ where: { id } });
if (!preview) return (0, import_response.makeResponse)({ ctr, content: { code: 404, message: import_errors.ERROR_MESSAGES.NOT_FOUND.message } });
await import_db.prisma.job.create({
data: { previewId: id, type: "STOP", status: "PENDING", payload: { reason: "Admin stop" } }
});
return (0, import_response.makeResponse)({ ctr, content: { code: 200, message: "Stop job enqueued" } });
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
adminListPreviews,
adminStopPreview,
createUser,
deleteUser,
getSettings,
listUsers,
updateSettings,
updateUser
});
//# sourceMappingURL=admin.js.map
File diff suppressed because one or more lines are too long
+153
View File
@@ -0,0 +1,153 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var previews_exports = {};
__export(previews_exports, {
getPreview: () => getPreview,
listPreviews: () => listPreviews,
previewLogsWs: () => previewLogsWs,
stopPreviewRoute: () => stopPreviewRoute
});
module.exports = __toCommonJS(previews_exports);
var import_db = require("../../lib/db");
var import_response = require("../../lib/response");
var import_errors = require("../../lib/errors");
var import_deploy = require("../../services/deploy");
function requireAuth(ctr) {
const auth = ctr.getAuth?.();
if (!auth?.success) return null;
return auth.user;
}
async function listPreviews(ctr) {
const user = requireAuth(ctr);
if (!user) return (0, import_response.makeResponse)({ ctr, content: { code: 401, message: import_errors.ERROR_MESSAGES.UNAUTHORIZED.message } });
const previews = await import_db.prisma.preview.findMany({
where: { repoConfig: { userId: user.id } },
include: { repoConfig: { select: { repoOwner: true, repoName: true } } },
orderBy: { updatedAt: "desc" }
});
return (0, import_response.makeResponse)({
ctr,
content: {
code: 200,
data: previews.map((p) => ({
id: p.id,
prNumber: p.prNumber,
prTitle: p.prTitle,
commitSha: p.commitSha,
status: p.status,
instanceIp: p.instanceIp,
port: p.port,
createdAt: p.createdAt,
updatedAt: p.updatedAt,
lastActivityAt: p.lastActivityAt,
repoOwner: p.repoConfig.repoOwner,
repoName: p.repoConfig.repoName
}))
}
});
}
async function getPreview(ctr) {
const user = requireAuth(ctr);
if (!user) return (0, import_response.makeResponse)({ ctr, content: { code: 401, message: import_errors.ERROR_MESSAGES.UNAUTHORIZED.message } });
const id = parseInt(ctr.params.get("id") || "0", 10);
const preview = await import_db.prisma.preview.findFirst({
where: { id, repoConfig: { userId: user.id } },
include: {
repoConfig: { select: { repoOwner: true, repoName: true } },
jobs: { orderBy: { createdAt: "desc" }, take: 10 }
}
});
if (!preview) return (0, import_response.makeResponse)({ ctr, content: { code: 404, message: import_errors.ERROR_MESSAGES.NOT_FOUND.message } });
return (0, import_response.makeResponse)({
ctr,
content: {
code: 200,
data: {
id: preview.id,
prNumber: preview.prNumber,
prTitle: preview.prTitle,
commitSha: preview.commitSha,
status: preview.status,
instanceIp: preview.instanceIp,
port: preview.port,
logs: preview.logs,
createdAt: preview.createdAt,
updatedAt: preview.updatedAt,
stoppedAt: preview.stoppedAt,
lastActivityAt: preview.lastActivityAt,
repoOwner: preview.repoConfig.repoOwner,
repoName: preview.repoConfig.repoName,
jobs: preview.jobs.map((j) => ({
id: j.id,
type: j.type,
status: j.status,
createdAt: j.createdAt,
startedAt: j.startedAt,
finishedAt: j.finishedAt,
error: j.error
}))
}
}
});
}
async function stopPreviewRoute(ctr) {
const user = requireAuth(ctr);
if (!user) return (0, import_response.makeResponse)({ ctr, content: { code: 401, message: import_errors.ERROR_MESSAGES.UNAUTHORIZED.message } });
const id = parseInt(ctr.params.get("id") || "0", 10);
const preview = await import_db.prisma.preview.findFirst({
where: { id, repoConfig: { userId: user.id } }
});
if (!preview) return (0, import_response.makeResponse)({ ctr, content: { code: 404, message: import_errors.ERROR_MESSAGES.NOT_FOUND.message } });
if (preview.status === "STOPPED") return (0, import_response.makeResponse)({ ctr, content: { code: 400, message: "Already stopped" } });
await import_db.prisma.job.create({
data: { previewId: id, type: "STOP", status: "PENDING", payload: { reason: "Manual stop via UI" } }
});
return (0, import_response.makeResponse)({ ctr, content: { code: 200, message: "Stop job enqueued" } });
}
async function previewLogsWs(ctr) {
const auth = ctr.getAuth?.();
if (!auth?.success) {
ctr.close(1008, "Unauthorized");
return;
}
const id = parseInt(ctr.params.get("id") || "0", 10);
const preview = await import_db.prisma.preview.findFirst({
where: { id, repoConfig: { userId: auth.user.id } }
});
if (!preview) {
ctr.close(1008, "Not found");
return;
}
await ctr.print(JSON.stringify({ type: "init", logs: preview.logs }));
const unsub = (0, import_deploy.subscribeToLogs)(id, (text) => {
try {
ctr.print(JSON.stringify({ type: "append", text }));
} catch {
}
});
ctr.$abort(unsub);
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
getPreview,
listPreviews,
previewLogsWs,
stopPreviewRoute
});
//# sourceMappingURL=previews.js.map
+7
View File
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../../src/routes/api/previews.ts"],
"sourcesContent": ["import { prisma } from \"../../lib/db\";\nimport { makeResponse } from \"../../lib/response\";\nimport { ERROR_MESSAGES } from \"../../lib/errors\";\nimport { subscribeToLogs } from \"../../services/deploy\";\n\nfunction requireAuth(ctr: any) {\n const auth = ctr.getAuth?.();\n if (!auth?.success) return null;\n return auth.user;\n}\n\nexport async function listPreviews(ctr: any) {\n const user = requireAuth(ctr);\n if (!user) return makeResponse({ ctr, content: { code: 401, message: ERROR_MESSAGES.UNAUTHORIZED.message } });\n\n const previews = await prisma.preview.findMany({\n where: { repoConfig: { userId: user.id } },\n include: { repoConfig: { select: { repoOwner: true, repoName: true } } },\n orderBy: { updatedAt: \"desc\" },\n });\n\n return makeResponse({\n ctr, content: {\n code: 200, data: previews.map(p => ({\n id: p.id,\n prNumber: p.prNumber,\n prTitle: p.prTitle,\n commitSha: p.commitSha,\n status: p.status,\n instanceIp: p.instanceIp,\n port: p.port,\n createdAt: p.createdAt,\n updatedAt: p.updatedAt,\n lastActivityAt: p.lastActivityAt,\n repoOwner: p.repoConfig.repoOwner,\n repoName: p.repoConfig.repoName,\n }))\n }\n });\n}\n\nexport async function getPreview(ctr: any) {\n const user = requireAuth(ctr);\n if (!user) return makeResponse({ ctr, content: { code: 401, message: ERROR_MESSAGES.UNAUTHORIZED.message } });\n\n const id = parseInt(ctr.params.get(\"id\") || \"0\", 10);\n const preview = await prisma.preview.findFirst({\n where: { id, repoConfig: { userId: user.id } },\n include: {\n repoConfig: { select: { repoOwner: true, repoName: true } },\n jobs: { orderBy: { createdAt: \"desc\" }, take: 10 },\n },\n });\n\n if (!preview) return makeResponse({ ctr, content: { code: 404, message: ERROR_MESSAGES.NOT_FOUND.message } });\n\n return makeResponse({\n ctr, content: {\n code: 200, data: {\n id: preview.id,\n prNumber: preview.prNumber,\n prTitle: preview.prTitle,\n commitSha: preview.commitSha,\n status: preview.status,\n instanceIp: preview.instanceIp,\n port: preview.port,\n logs: preview.logs,\n createdAt: preview.createdAt,\n updatedAt: preview.updatedAt,\n stoppedAt: preview.stoppedAt,\n lastActivityAt: preview.lastActivityAt,\n repoOwner: preview.repoConfig.repoOwner,\n repoName: preview.repoConfig.repoName,\n jobs: preview.jobs.map(j => ({\n id: j.id,\n type: j.type,\n status: j.status,\n createdAt: j.createdAt,\n startedAt: j.startedAt,\n finishedAt: j.finishedAt,\n error: j.error,\n })),\n }\n }\n });\n}\n\nexport async function stopPreviewRoute(ctr: any) {\n const user = requireAuth(ctr);\n if (!user) return makeResponse({ ctr, content: { code: 401, message: ERROR_MESSAGES.UNAUTHORIZED.message } });\n\n const id = parseInt(ctr.params.get(\"id\") || \"0\", 10);\n const preview = await prisma.preview.findFirst({\n where: { id, repoConfig: { userId: user.id } },\n });\n\n if (!preview) return makeResponse({ ctr, content: { code: 404, message: ERROR_MESSAGES.NOT_FOUND.message } });\n if (preview.status === \"STOPPED\") return makeResponse({ ctr, content: { code: 400, message: \"Already stopped\" } });\n\n await prisma.job.create({\n data: { previewId: id, type: \"STOP\", status: \"PENDING\", payload: { reason: \"Manual stop via UI\" } },\n });\n\n return makeResponse({ ctr, content: { code: 200, message: \"Stop job enqueued\" } });\n}\n\nexport async function previewLogsWs(ctr: any) {\n const auth = ctr.getAuth?.();\n if (!auth?.success) {\n ctr.close(1008, \"Unauthorized\");\n return;\n }\n\n const id = parseInt(ctr.params.get(\"id\") || \"0\", 10);\n const preview = await prisma.preview.findFirst({\n where: { id, repoConfig: { userId: auth.user.id } },\n });\n\n if (!preview) {\n ctr.close(1008, \"Not found\");\n return;\n }\n\n await ctr.print(JSON.stringify({ type: \"init\", logs: preview.logs }));\n\n const unsub = subscribeToLogs(id, (text) => {\n try {\n ctr.print(JSON.stringify({ type: \"append\", text }));\n } catch {}\n });\n\n ctr.$abort(unsub);\n}\n"],
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAAuB;AACvB,sBAA6B;AAC7B,oBAA+B;AAC/B,oBAAgC;AAEhC,SAAS,YAAY,KAAU;AAC7B,QAAM,OAAO,IAAI,UAAU;AAC3B,MAAI,CAAC,MAAM,QAAS,QAAO;AAC3B,SAAO,KAAK;AACd;AAEA,eAAsB,aAAa,KAAU;AAC3C,QAAM,OAAO,YAAY,GAAG;AAC5B,MAAI,CAAC,KAAM,YAAO,8BAAa,EAAE,KAAK,SAAS,EAAE,MAAM,KAAK,SAAS,6BAAe,aAAa,QAAQ,EAAE,CAAC;AAE5G,QAAM,WAAW,MAAM,iBAAO,QAAQ,SAAS;AAAA,IAC7C,OAAO,EAAE,YAAY,EAAE,QAAQ,KAAK,GAAG,EAAE;AAAA,IACzC,SAAS,EAAE,YAAY,EAAE,QAAQ,EAAE,WAAW,MAAM,UAAU,KAAK,EAAE,EAAE;AAAA,IACvE,SAAS,EAAE,WAAW,OAAO;AAAA,EAC/B,CAAC;AAED,aAAO,8BAAa;AAAA,IAClB;AAAA,IAAK,SAAS;AAAA,MACZ,MAAM;AAAA,MAAK,MAAM,SAAS,IAAI,QAAM;AAAA,QAClC,IAAI,EAAE;AAAA,QACN,UAAU,EAAE;AAAA,QACZ,SAAS,EAAE;AAAA,QACX,WAAW,EAAE;AAAA,QACb,QAAQ,EAAE;AAAA,QACV,YAAY,EAAE;AAAA,QACd,MAAM,EAAE;AAAA,QACR,WAAW,EAAE;AAAA,QACb,WAAW,EAAE;AAAA,QACb,gBAAgB,EAAE;AAAA,QAClB,WAAW,EAAE,WAAW;AAAA,QACxB,UAAU,EAAE,WAAW;AAAA,MACzB,EAAE;AAAA,IACJ;AAAA,EACF,CAAC;AACH;AAEA,eAAsB,WAAW,KAAU;AACzC,QAAM,OAAO,YAAY,GAAG;AAC5B,MAAI,CAAC,KAAM,YAAO,8BAAa,EAAE,KAAK,SAAS,EAAE,MAAM,KAAK,SAAS,6BAAe,aAAa,QAAQ,EAAE,CAAC;AAE5G,QAAM,KAAK,SAAS,IAAI,OAAO,IAAI,IAAI,KAAK,KAAK,EAAE;AACnD,QAAM,UAAU,MAAM,iBAAO,QAAQ,UAAU;AAAA,IAC7C,OAAO,EAAE,IAAI,YAAY,EAAE,QAAQ,KAAK,GAAG,EAAE;AAAA,IAC7C,SAAS;AAAA,MACP,YAAY,EAAE,QAAQ,EAAE,WAAW,MAAM,UAAU,KAAK,EAAE;AAAA,MAC1D,MAAM,EAAE,SAAS,EAAE,WAAW,OAAO,GAAG,MAAM,GAAG;AAAA,IACnD;AAAA,EACF,CAAC;AAED,MAAI,CAAC,QAAS,YAAO,8BAAa,EAAE,KAAK,SAAS,EAAE,MAAM,KAAK,SAAS,6BAAe,UAAU,QAAQ,EAAE,CAAC;AAE5G,aAAO,8BAAa;AAAA,IAClB;AAAA,IAAK,SAAS;AAAA,MACZ,MAAM;AAAA,MAAK,MAAM;AAAA,QACf,IAAI,QAAQ;AAAA,QACZ,UAAU,QAAQ;AAAA,QAClB,SAAS,QAAQ;AAAA,QACjB,WAAW,QAAQ;AAAA,QACnB,QAAQ,QAAQ;AAAA,QAChB,YAAY,QAAQ;AAAA,QACpB,MAAM,QAAQ;AAAA,QACd,MAAM,QAAQ;AAAA,QACd,WAAW,QAAQ;AAAA,QACnB,WAAW,QAAQ;AAAA,QACnB,WAAW,QAAQ;AAAA,QACnB,gBAAgB,QAAQ;AAAA,QACxB,WAAW,QAAQ,WAAW;AAAA,QAC9B,UAAU,QAAQ,WAAW;AAAA,QAC7B,MAAM,QAAQ,KAAK,IAAI,QAAM;AAAA,UAC3B,IAAI,EAAE;AAAA,UACN,MAAM,EAAE;AAAA,UACR,QAAQ,EAAE;AAAA,UACV,WAAW,EAAE;AAAA,UACb,WAAW,EAAE;AAAA,UACb,YAAY,EAAE;AAAA,UACd,OAAO,EAAE;AAAA,QACX,EAAE;AAAA,MACJ;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAEA,eAAsB,iBAAiB,KAAU;AAC/C,QAAM,OAAO,YAAY,GAAG;AAC5B,MAAI,CAAC,KAAM,YAAO,8BAAa,EAAE,KAAK,SAAS,EAAE,MAAM,KAAK,SAAS,6BAAe,aAAa,QAAQ,EAAE,CAAC;AAE5G,QAAM,KAAK,SAAS,IAAI,OAAO,IAAI,IAAI,KAAK,KAAK,EAAE;AACnD,QAAM,UAAU,MAAM,iBAAO,QAAQ,UAAU;AAAA,IAC7C,OAAO,EAAE,IAAI,YAAY,EAAE,QAAQ,KAAK,GAAG,EAAE;AAAA,EAC/C,CAAC;AAED,MAAI,CAAC,QAAS,YAAO,8BAAa,EAAE,KAAK,SAAS,EAAE,MAAM,KAAK,SAAS,6BAAe,UAAU,QAAQ,EAAE,CAAC;AAC5G,MAAI,QAAQ,WAAW,UAAW,YAAO,8BAAa,EAAE,KAAK,SAAS,EAAE,MAAM,KAAK,SAAS,kBAAkB,EAAE,CAAC;AAEjH,QAAM,iBAAO,IAAI,OAAO;AAAA,IACtB,MAAM,EAAE,WAAW,IAAI,MAAM,QAAQ,QAAQ,WAAW,SAAS,EAAE,QAAQ,qBAAqB,EAAE;AAAA,EACpG,CAAC;AAED,aAAO,8BAAa,EAAE,KAAK,SAAS,EAAE,MAAM,KAAK,SAAS,oBAAoB,EAAE,CAAC;AACnF;AAEA,eAAsB,cAAc,KAAU;AAC5C,QAAM,OAAO,IAAI,UAAU;AAC3B,MAAI,CAAC,MAAM,SAAS;AAClB,QAAI,MAAM,MAAM,cAAc;AAC9B;AAAA,EACF;AAEA,QAAM,KAAK,SAAS,IAAI,OAAO,IAAI,IAAI,KAAK,KAAK,EAAE;AACnD,QAAM,UAAU,MAAM,iBAAO,QAAQ,UAAU;AAAA,IAC7C,OAAO,EAAE,IAAI,YAAY,EAAE,QAAQ,KAAK,KAAK,GAAG,EAAE;AAAA,EACpD,CAAC;AAED,MAAI,CAAC,SAAS;AACZ,QAAI,MAAM,MAAM,WAAW;AAC3B;AAAA,EACF;AAEA,QAAM,IAAI,MAAM,KAAK,UAAU,EAAE,MAAM,QAAQ,MAAM,QAAQ,KAAK,CAAC,CAAC;AAEpE,QAAM,YAAQ,+BAAgB,IAAI,CAAC,SAAS;AAC1C,QAAI;AACF,UAAI,MAAM,KAAK,UAAU,EAAE,MAAM,UAAU,KAAK,CAAC,CAAC;AAAA,IACpD,QAAQ;AAAA,IAAC;AAAA,EACX,CAAC;AAED,MAAI,OAAO,KAAK;AAClB;",
"names": []
}
+180
View File
@@ -0,0 +1,180 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var repos_exports = {};
__export(repos_exports, {
getRepoConfig: () => getRepoConfig,
listRepos: () => listRepos,
saveRepoConfig: () => saveRepoConfig,
toggleRepoEnabled: () => toggleRepoEnabled
});
module.exports = __toCommonJS(repos_exports);
var import_db = require("../../lib/db");
var import_response = require("../../lib/response");
var import_errors = require("../../lib/errors");
var import_gitea = require("../../services/gitea");
var import_adminSettings = require("../../lib/adminSettings");
var import_env = require("../../lib/env");
function requireAuth(ctr) {
const auth = ctr.getAuth?.();
if (!auth?.success) return null;
return auth.user;
}
async function listRepos(ctr) {
const user = requireAuth(ctr);
if (!user) return (0, import_response.makeResponse)({ ctr, content: { code: 401, message: import_errors.ERROR_MESSAGES.UNAUTHORIZED.message } });
const fullUser = await import_db.prisma.user.findUnique({ where: { id: user.id } });
if (!fullUser?.giteaInstanceUrl || !fullUser?.giteaPAT) {
return (0, import_response.makeResponse)({ ctr, content: { code: 400, message: "Gitea credentials not configured" } });
}
const giteaRepos = await (0, import_gitea.fetchUserRepos)(fullUser);
const configs = await import_db.prisma.repoConfig.findMany({ where: { userId: user.id } });
const configMap = new Map(configs.map((c) => [`${c.repoOwner}/${c.repoName}`, c]));
const allOwners = [...new Set(giteaRepos.map((r) => r.full_name?.split("/")[0]).filter(Boolean))];
const allConfigs = await import_db.prisma.repoConfig.findMany({
where: { repoOwner: { in: allOwners } },
select: { repoOwner: true, repoName: true, userId: true }
});
const claimedByOthers = new Set(
allConfigs.filter((c) => c.userId !== user.id).map((c) => `${c.repoOwner}/${c.repoName}`)
);
const result = giteaRepos.map((r) => {
const [owner, name] = (r.full_name || "").split("/");
const key = `${owner}/${name}`;
const config = configMap.get(key);
const { giteaWebhookId, ...safeConfig } = config || {};
return {
owner,
name,
fullName: r.full_name,
htmlUrl: r.html_url,
isEnabled: config?.isEnabled ?? false,
claimedByOther: claimedByOthers.has(key),
config: config ? safeConfig : null
};
});
return (0, import_response.makeResponse)({ ctr, content: { code: 200, data: result } });
}
async function getRepoConfig(ctr) {
const user = requireAuth(ctr);
if (!user) return (0, import_response.makeResponse)({ ctr, content: { code: 401, message: import_errors.ERROR_MESSAGES.UNAUTHORIZED.message } });
const owner = ctr.params.get("owner");
const repo = ctr.params.get("repo");
const config = await import_db.prisma.repoConfig.findFirst({
where: { repoOwner: owner, repoName: repo, userId: user.id }
});
if (!config) return (0, import_response.makeResponse)({ ctr, content: { code: 404, message: "Repo config not found" } });
const { giteaWebhookId, ...safeConfig } = config;
return (0, import_response.makeResponse)({ ctr, content: { code: 200, data: safeConfig } });
}
async function saveRepoConfig(ctr) {
const user = requireAuth(ctr);
if (!user) return (0, import_response.makeResponse)({ ctr, content: { code: 401, message: import_errors.ERROR_MESSAGES.UNAUTHORIZED.message } });
const body = await ctr.body();
const { owner, repo, ...configData } = body || {};
if (!owner || !repo) return (0, import_response.makeResponse)({ ctr, content: { code: 400, message: "owner and repo required" } });
const existing = await import_db.prisma.repoConfig.findFirst({
where: { repoOwner: owner, repoName: repo }
});
if (existing && existing.userId !== user.id) {
return (0, import_response.makeResponse)({ ctr, content: { code: 409, message: "This repo is already configured by another user." } });
}
const settings = await (0, import_adminSettings.getAdminSettings)();
const sanitized = {
repoOwner: owner,
repoName: repo,
userId: user.id,
instanceType: configData.instanceType ?? settings.defaultInstanceType,
inactivityHours: Math.min(72, Math.max(0.5, Number(configData.inactivityHours ?? 12))),
port: Number(configData.port ?? 3e3),
envVars: configData.envVars ?? {},
useDockerCompose: Boolean(configData.useDockerCompose ?? false),
composeFilePath: configData.composeFilePath ?? null,
aptPackages: Array.isArray(configData.aptPackages) ? configData.aptPackages : [],
setupCommands: Array.isArray(configData.setupCommands) ? configData.setupCommands : [],
buildCommands: Array.isArray(configData.buildCommands) ? configData.buildCommands : [],
postBuildCommands: Array.isArray(configData.postBuildCommands) ? configData.postBuildCommands : [],
runCommand: configData.runCommand ?? null,
denyList: Array.isArray(configData.denyList) ? configData.denyList : []
};
let config;
if (existing) {
config = await import_db.prisma.repoConfig.update({ where: { id: existing.id }, data: sanitized });
} else {
config = await import_db.prisma.repoConfig.create({ data: sanitized });
}
const { giteaWebhookId, ...safeConfig } = config;
return (0, import_response.makeResponse)({ ctr, content: { code: 200, data: safeConfig } });
}
async function toggleRepoEnabled(ctr) {
const user = requireAuth(ctr);
if (!user) return (0, import_response.makeResponse)({ ctr, content: { code: 401, message: import_errors.ERROR_MESSAGES.UNAUTHORIZED.message } });
const body = await ctr.body();
const { owner, repo, enabled } = body || {};
if (!owner || !repo || enabled === void 0) {
return (0, import_response.makeResponse)({ ctr, content: { code: 400, message: "owner, repo, enabled required" } });
}
const fullUser = await import_db.prisma.user.findUnique({ where: { id: user.id } });
if (!fullUser?.giteaInstanceUrl || !fullUser?.giteaPAT) {
return (0, import_response.makeResponse)({ ctr, content: { code: 400, message: "Gitea credentials not configured" } });
}
let config = await import_db.prisma.repoConfig.findFirst({ where: { repoOwner: owner, repoName: repo } });
if (config && config.userId !== user.id) {
return (0, import_response.makeResponse)({ ctr, content: { code: 409, message: "This repo is already configured by another user." } });
}
const webhookToken = await import_db.prisma.webhookToken.findUnique({ where: { userId: user.id } });
if (!webhookToken) {
return (0, import_response.makeResponse)({ ctr, content: { code: 400, message: "No webhook token configured. Go to Settings to generate one." } });
}
if (enabled) {
if (!config) {
config = await import_db.prisma.repoConfig.create({
data: { repoOwner: owner, repoName: repo, userId: user.id, isEnabled: false }
});
}
const webhookUrl = `${import_env.env.PP_BASE_URL}/webhook/${user.id}`;
const hookId = await (0, import_gitea.registerWebhook)(fullUser, owner, repo, webhookUrl, webhookToken.token);
await import_db.prisma.repoConfig.update({
where: { id: config.id },
data: { isEnabled: true, giteaWebhookId: String(hookId) }
});
return (0, import_response.makeResponse)({ ctr, content: { code: 200, message: "Repo enabled and webhook registered" } });
} else {
if (!config) return (0, import_response.makeResponse)({ ctr, content: { code: 404, message: "Repo config not found" } });
if (config.giteaWebhookId) {
try {
await (0, import_gitea.deleteWebhook)(fullUser, owner, repo, config.giteaWebhookId);
} catch (e) {
return (0, import_response.makeResponse)({ ctr, content: { code: 400, message: `Failed to delete Gitea webhook: ${e.message}` } });
}
}
await import_db.prisma.repoConfig.update({
where: { id: config.id },
data: { isEnabled: false, giteaWebhookId: null }
});
return (0, import_response.makeResponse)({ ctr, content: { code: 200, message: "Repo disabled and webhook removed" } });
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
getRepoConfig,
listRepos,
saveRepoConfig,
toggleRepoEnabled
});
//# sourceMappingURL=repos.js.map
File diff suppressed because one or more lines are too long
+204
View File
@@ -0,0 +1,204 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var user_exports = {};
__export(user_exports, {
getUserSettings: () => getUserSettings,
getWebhookSecret: () => getWebhookSecret,
regenerateWebhookSecret: () => regenerateWebhookSecret,
updateAws: () => updateAws,
updateGitea: () => updateGitea,
updatePassword: () => updatePassword,
updateUsername: () => updateUsername
});
module.exports = __toCommonJS(user_exports);
var import_bcryptjs = __toESM(require("bcryptjs"));
var import_crypto = require("crypto");
var import_db = require("../../lib/db");
var import_response = require("../../lib/response");
var import_errors = require("../../lib/errors");
var import_encryption = require("../../lib/encryption");
var import_gitea = require("../../services/gitea");
var import_ec2 = require("../../services/ec2");
var import_env = require("../../lib/env");
function requireAuth(ctr) {
const auth = ctr.getAuth?.();
if (!auth?.success) return null;
return auth.user;
}
async function getUserSettings(ctr) {
const user = requireAuth(ctr);
if (!user) return (0, import_response.makeResponse)({ ctr, content: { code: import_errors.ERROR_MESSAGES.UNAUTHORIZED.code, message: import_errors.ERROR_MESSAGES.UNAUTHORIZED.message } });
const webhookToken = await import_db.prisma.webhookToken.findUnique({ where: { userId: user.id } });
const fullUser = await import_db.prisma.user.findUnique({ where: { id: user.id } });
return (0, import_response.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: `${import_env.env.PP_BASE_URL}/webhook/${user.id}`,
webhookSecret: webhookToken?.token ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : null,
webhookTokenExists: !!webhookToken
}
}
});
}
async function updateUsername(ctr) {
const user = requireAuth(ctr);
if (!user) return (0, import_response.makeResponse)({ ctr, content: { code: 401, message: import_errors.ERROR_MESSAGES.UNAUTHORIZED.message } });
const body = await ctr.body();
const { username } = body || {};
if (!username || typeof username !== "string") return (0, import_response.makeResponse)({ ctr, content: { code: 400, message: "Username required" } });
const existing = await import_db.prisma.user.findFirst({ where: { username, id: { not: user.id } } });
if (existing) return (0, import_response.makeResponse)({ ctr, content: { code: 409, message: "Username already taken" } });
await import_db.prisma.user.update({ where: { id: user.id }, data: { username } });
return (0, import_response.makeResponse)({ ctr, content: { code: 200, message: "Username updated" } });
}
async function updatePassword(ctr) {
const user = requireAuth(ctr);
if (!user) return (0, import_response.makeResponse)({ ctr, content: { code: 401, message: import_errors.ERROR_MESSAGES.UNAUTHORIZED.message } });
const body = await ctr.body();
const { currentPassword, newPassword } = body || {};
if (!currentPassword || !newPassword) return (0, import_response.makeResponse)({ ctr, content: { code: 400, message: "Current and new passwords required" } });
const fullUser = await import_db.prisma.user.findUnique({ where: { id: user.id } });
if (!fullUser) return (0, import_response.makeResponse)({ ctr, content: { code: 404, message: "User not found" } });
const valid = await import_bcryptjs.default.compare(currentPassword, fullUser.passwordHash);
if (!valid) return (0, import_response.makeResponse)({ ctr, content: { code: 401, message: "Current password incorrect" } });
const hash = await import_bcryptjs.default.hash(newPassword, 12);
await import_db.prisma.user.update({ where: { id: user.id }, data: { passwordHash: hash } });
return (0, import_response.makeResponse)({ ctr, content: { code: 200, message: "Password updated" } });
}
async function updateGitea(ctr) {
const user = requireAuth(ctr);
if (!user) return (0, import_response.makeResponse)({ ctr, content: { code: 401, message: import_errors.ERROR_MESSAGES.UNAUTHORIZED.message } });
const body = await ctr.body();
const { giteaInstanceUrl, giteaUsername, giteaPAT } = body || {};
if (!giteaInstanceUrl || !giteaUsername) {
return (0, import_response.makeResponse)({ ctr, content: { code: 400, message: "Gitea URL and username required" } });
}
const cleanUrl = giteaInstanceUrl.replace(/\/+$/, "");
const validation = await (0, import_gitea.validateGiteaUrl)(cleanUrl);
if (!validation.success) {
return (0, import_response.makeResponse)({ ctr, content: { code: 400, message: `Gitea validation failed: ${validation.error}` } });
}
const data = { giteaInstanceUrl: cleanUrl, giteaUsername };
if (giteaPAT) data.giteaPAT = (0, import_encryption.encrypt)(giteaPAT);
await import_db.prisma.user.update({ where: { id: user.id }, data });
return (0, import_response.makeResponse)({ ctr, content: { code: 200, message: `Connected to Gitea ${validation.version}`, data: { version: validation.version } } });
}
async function updateAws(ctr) {
const user = requireAuth(ctr);
if (!user) return (0, import_response.makeResponse)({ ctr, content: { code: 401, message: import_errors.ERROR_MESSAGES.UNAUTHORIZED.message } });
const body = await ctr.body();
const { awsAccessKeyId, awsSecretAccessKey, awsRegion } = body || {};
if (!awsAccessKeyId || !awsSecretAccessKey || !awsRegion) {
return (0, import_response.makeResponse)({ ctr, content: { code: 400, message: "AWS credentials and region required" } });
}
const tempUser = {
...user,
awsAccessKeyId: (0, import_encryption.encrypt)(awsAccessKeyId),
awsSecretAccessKey: (0, import_encryption.encrypt)(awsSecretAccessKey),
awsRegion
};
const validation = await (0, import_ec2.validateAwsCredentials)(tempUser);
if (!validation.success) {
return (0, import_response.makeResponse)({ ctr, content: { code: 400, message: `AWS validation failed: ${validation.error}` } });
}
await import_db.prisma.user.update({
where: { id: user.id },
data: {
awsAccessKeyId: (0, import_encryption.encrypt)(awsAccessKeyId),
awsSecretAccessKey: (0, import_encryption.encrypt)(awsSecretAccessKey),
awsRegion
}
});
return (0, import_response.makeResponse)({ ctr, content: { code: 200, message: `Connected as ${validation.arn}`, data: { arn: validation.arn } } });
}
async function getWebhookSecret(ctr) {
const user = requireAuth(ctr);
if (!user) return (0, import_response.makeResponse)({ ctr, content: { code: 401, message: import_errors.ERROR_MESSAGES.UNAUTHORIZED.message } });
let token = await import_db.prisma.webhookToken.findUnique({ where: { userId: user.id } });
if (!token) {
const secret = (0, import_crypto.randomBytes)(32).toString("hex");
token = await import_db.prisma.webhookToken.create({ data: { userId: user.id, token: secret } });
}
return (0, import_response.makeResponse)({ ctr, content: { code: 200, data: { token: token.token } } });
}
async function regenerateWebhookSecret(ctr) {
const user = requireAuth(ctr);
if (!user) return (0, import_response.makeResponse)({ ctr, content: { code: 401, message: import_errors.ERROR_MESSAGES.UNAUTHORIZED.message } });
const newSecret = (0, import_crypto.randomBytes)(32).toString("hex");
await import_db.prisma.webhookToken.upsert({
where: { userId: user.id },
update: { token: newSecret },
create: { userId: user.id, token: newSecret }
});
const fullUser = await import_db.prisma.user.findUnique({ where: { id: user.id } });
if (!fullUser?.giteaInstanceUrl) {
return (0, import_response.makeResponse)({ ctr, content: { code: 200, message: "Secret regenerated (no Gitea hooks to update)", data: { token: newSecret } } });
}
const configs = await import_db.prisma.repoConfig.findMany({
where: { userId: user.id, giteaWebhookId: { not: null } }
});
const { updateWebhookSecret } = await import("../../services/gitea");
const webhookUrl = `${import_env.env.PP_BASE_URL}/webhook/${user.id}`;
const errors = [];
for (const config of configs) {
try {
await updateWebhookSecret(fullUser, config.repoOwner, config.repoName, config.giteaWebhookId, webhookUrl, newSecret);
} catch (e) {
errors.push(`${config.repoOwner}/${config.repoName}: ${e.message}`);
}
}
return (0, import_response.makeResponse)({
ctr,
content: {
code: 200,
message: errors.length ? `Secret regenerated with ${errors.length} hook update errors` : "Secret regenerated and all hooks updated",
data: { token: newSecret, errors }
}
});
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
getUserSettings,
getWebhookSecret,
regenerateWebhookSecret,
updateAws,
updateGitea,
updatePassword,
updateUsername
});
//# sourceMappingURL=user.js.map
File diff suppressed because one or more lines are too long
+161
View File
@@ -0,0 +1,161 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var auth_exports = {};
__export(auth_exports, {
firstUserHandler: () => firstUserHandler,
loginHandler: () => loginHandler,
logoutHandler: () => logoutHandler,
meHandler: () => meHandler,
setupStatusHandler: () => setupStatusHandler
});
module.exports = __toCommonJS(auth_exports);
var import_crypto = require("crypto");
var import_bcryptjs = __toESM(require("bcryptjs"));
var import_rjweb_server = require("rjweb-server");
var import_db = require("../lib/db");
var import_response = require("../lib/response");
var import_errors = require("../lib/errors");
var import_logger = require("../lib/logger");
const log = (0, import_logger.createLogger)("AUTH_ROUTE");
const COOKIE_NAME = "pp_session";
const COOKIE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
async function loginHandler(ctr) {
let body;
try {
body = await ctr.body();
} catch {
return (0, import_response.makeResponse)({ ctr, content: { code: 400, message: "Invalid body" } });
}
const { username, password } = body || {};
if (!username || !password) {
return (0, import_response.makeResponse)({ ctr, content: { code: 400, message: "Username and password required" } });
}
const user = await import_db.prisma.user.findUnique({ where: { username } });
if (!user) {
return (0, import_response.makeResponse)({ ctr, content: { code: 401, message: "Invalid credentials" } });
}
const valid = await import_bcryptjs.default.compare(password, user.passwordHash);
if (!valid) {
return (0, import_response.makeResponse)({ ctr, content: { code: 401, message: "Invalid credentials" } });
}
const sessionHash = (0, import_crypto.randomBytes)(32).toString("hex");
await import_db.prisma.session.create({ data: { hash: sessionHash, userId: user.id } });
ctr.cookies.set(
COOKIE_NAME,
new import_rjweb_server.Cookie(sessionHash, {
httpOnly: true,
expires: new Date(Date.now() + COOKIE_MAX_AGE_MS),
path: "/",
sameSite: "Lax"
})
);
return (0, import_response.makeResponse)({ ctr, content: { code: 200, data: { id: user.id, username: user.username, isAdmin: user.isAdmin } } });
}
async function logoutHandler(ctr) {
const auth = ctr.getAuth?.();
if (auth?.success) {
await import_db.prisma.session.delete({ where: { id: auth.sessionId } }).catch(() => {
});
}
ctr.cookies.set(COOKIE_NAME, new import_rjweb_server.Cookie("", { expires: /* @__PURE__ */ new Date(0), path: "/" }));
return (0, import_response.makeResponse)({ ctr, content: { code: 200, message: "Logged out" } });
}
async function meHandler(ctr) {
const auth = ctr.getAuth?.();
if (!auth?.success) {
return (0, import_response.makeResponse)({ ctr, content: { code: import_errors.ERROR_MESSAGES.UNAUTHORIZED.code, message: import_errors.ERROR_MESSAGES.UNAUTHORIZED.message } });
}
const user = await import_db.prisma.user.findUnique({ where: { id: auth.user.id } });
if (!user) return (0, import_response.makeResponse)({ ctr, content: { code: 404, message: "User not found" } });
return (0, import_response.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)
}
}
});
}
async function setupStatusHandler(ctr) {
const count = await import_db.prisma.user.count();
return (0, import_response.makeResponse)({ ctr, content: { code: 200, data: { needsSetup: count === 0 } } });
}
async function firstUserHandler(ctr) {
const count = await import_db.prisma.user.count();
if (count > 0) {
return (0, import_response.makeResponse)({ ctr, content: { code: 403, message: "Setup already completed. Contact an administrator to create your account." } });
}
let body;
try {
body = await ctr.body();
} catch {
return (0, import_response.makeResponse)({ ctr, content: { code: 400, message: "Invalid body" } });
}
const { username, password } = body || {};
if (!username || !password || password.length < 8) {
return (0, import_response.makeResponse)({ ctr, content: { code: 400, message: "Username and password (min 8 chars) required" } });
}
const hash = await import_bcryptjs.default.hash(password, 12);
const user = await import_db.prisma.user.create({
data: { username, passwordHash: hash, isAdmin: true, isFounder: true }
});
const sessionHash = (0, import_crypto.randomBytes)(32).toString("hex");
await import_db.prisma.session.create({ data: { hash: sessionHash, userId: user.id } });
ctr.cookies.set(
COOKIE_NAME,
new import_rjweb_server.Cookie(sessionHash, {
httpOnly: true,
expires: new Date(Date.now() + COOKIE_MAX_AGE_MS),
path: "/",
sameSite: "Lax"
})
);
log.info({ username }, "First user (founder) created");
return (0, import_response.makeResponse)({ ctr, content: { code: 201, data: { id: user.id, username: user.username, isAdmin: true, isFounder: true } } });
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
firstUserHandler,
loginHandler,
logoutHandler,
meHandler,
setupStatusHandler
});
//# sourceMappingURL=auth.js.map
File diff suppressed because one or more lines are too long
+279
View File
@@ -0,0 +1,279 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var webhook_exports = {};
__export(webhook_exports, {
webhookHandler: () => webhookHandler
});
module.exports = __toCommonJS(webhook_exports);
var import_crypto = require("crypto");
var import_db = require("../lib/db");
var import_logger = require("../lib/logger");
var import_adminSettings = require("../lib/adminSettings");
var import_gitea = require("../services/gitea");
var import_env = require("../lib/env");
const log = (0, import_logger.createLogger)("WEBHOOK");
const rateLimitMap = /* @__PURE__ */ new Map();
function checkRateLimit(userId, limitPerMin) {
const now = Date.now();
const entry = rateLimitMap.get(userId);
if (!entry || now > entry.resetAt) {
rateLimitMap.set(userId, { count: 1, resetAt: now + 6e4 });
return true;
}
if (entry.count >= limitPerMin) return false;
entry.count++;
return true;
}
async function webhookHandler(ctr) {
const userId = parseInt(ctr.params.get("userId") || "0", 10);
if (!userId) return ctr.status(400).print({ status: "FAILED", message: "Invalid user ID" });
const settings = await (0, import_adminSettings.getAdminSettings)();
if (!checkRateLimit(userId, settings.webhookRateLimitPerMinute)) {
return ctr.status(429).print({ status: "FAILED", message: "Rate limit exceeded" });
}
const user = await import_db.prisma.user.findUnique({ where: { id: userId } });
if (!user) return ctr.status(404).print({ status: "FAILED", message: "User not found" });
const webhookToken = await import_db.prisma.webhookToken.findUnique({ where: { userId } });
if (!webhookToken) return ctr.status(401).print({ status: "FAILED", message: "No webhook token configured" });
const signature = ctr.headers.get("x-gitea-signature-256") || ctr.headers.get("x-hub-signature-256") || "";
const rawBody = await ctr.$body().text();
const expected = "sha256=" + (0, import_crypto.createHmac)("sha256", webhookToken.token).update(rawBody).digest("hex");
if (!timingSafeEqual(signature, expected)) {
log.warn({ userId, signature }, "Invalid webhook signature");
return ctr.status(401).print({ status: "FAILED", message: "Invalid signature" });
}
let payload;
try {
payload = JSON.parse(rawBody);
} catch {
return ctr.status(400).print({ status: "FAILED", message: "Invalid JSON payload" });
}
const event = ctr.headers.get("x-gitea-event") || "";
ctr.status(200).print({ status: "OK" });
setImmediate(() => handleWebhookAsync(user, event, payload).catch((e) => log.error({ e }, "Webhook processing error")));
}
async function handleWebhookAsync(user, event, payload) {
if (event === "pull_request") {
await handlePullRequestEvent(user, payload);
} else if (event === "issue_comment") {
await handleIssueCommentEvent(user, payload);
}
}
async function handlePullRequestEvent(user, payload) {
const action = payload.action;
const pr = payload.pull_request;
const repo = payload.repository;
if (!pr || !repo) return;
const owner = repo.owner?.login || repo.full_name?.split("/")[0];
const repoName = repo.name;
const prNumber = pr.number;
const prTitle = pr.title || `PR #${prNumber}`;
const commitSha = pr.head?.sha || "";
const cloneUrl = pr.head?.repo?.clone_url || repo.clone_url;
const repoConfig = await import_db.prisma.repoConfig.findFirst({
where: { repoOwner: owner, repoName, userId: user.id, isEnabled: true }
});
if (!repoConfig) {
const existing = await import_db.prisma.noConfigComment.findUnique({
where: { userId_repoOwner_repoName_prNumber: { userId: user.id, repoOwner: owner, repoName, prNumber } }
});
if (!existing) {
const body = `No previews configured for \`${owner}/${repoName}\`. Configure this repo in [PR Previews](${import_env.env.PP_BASE_URL}).`;
try {
await (0, import_gitea.postComment)(user, owner, repoName, prNumber, body);
await import_db.prisma.noConfigComment.create({ data: { userId: user.id, repoOwner: owner, repoName, prNumber } });
} catch {
}
}
return;
}
if (repoConfig.denyList.includes(pr.user?.login || "")) return;
if (action === "opened" || action === "reopened") {
let preview = await import_db.prisma.preview.findFirst({
where: { repoConfigId: repoConfig.id, prNumber },
orderBy: { createdAt: "desc" }
});
if (preview && preview.status === "IGNORED") return;
if (!preview || preview.status === "STOPPED") {
preview = await import_db.prisma.preview.create({
data: {
repoConfigId: repoConfig.id,
prNumber,
prTitle,
commitSha,
status: "PROVISIONING",
port: repoConfig.port
}
});
await import_db.prisma.job.create({
data: {
previewId: preview.id,
type: "DEPLOY",
status: "PENDING",
payload: { commitSha, prNumber, prTitle, cloneUrl, isFirstDeploy: true }
}
});
}
} else if (action === "synchronize") {
const preview = await import_db.prisma.preview.findFirst({
where: { repoConfigId: repoConfig.id, prNumber },
orderBy: { createdAt: "desc" }
});
if (!preview || preview.status === "IGNORED") return;
await import_db.prisma.preview.update({ where: { id: preview.id }, data: { lastActivityAt: /* @__PURE__ */ new Date() } });
await import_db.prisma.job.create({
data: {
previewId: preview.id,
type: "DEPLOY",
status: "PENDING",
payload: { commitSha, prNumber, prTitle, cloneUrl, isFirstDeploy: preview.status === "STOPPED" }
}
});
} else if (action === "closed") {
const preview = await import_db.prisma.preview.findFirst({
where: { repoConfigId: repoConfig.id, prNumber },
orderBy: { createdAt: "desc" }
});
if (preview && preview.status !== "STOPPED" && preview.status !== "IGNORED") {
await import_db.prisma.job.create({
data: {
previewId: preview.id,
type: "STOP",
status: "PENDING",
payload: { reason: "PR closed" }
}
});
}
}
}
async function handleIssueCommentEvent(user, payload) {
const action = payload.action;
if (action !== "created") return;
const comment = payload.comment;
const issue = payload.issue;
const repo = payload.repository;
if (!comment || !issue || !repo || !issue.pull_request) return;
const body = comment.body || "";
if (!body.trimStart().startsWith("/pp ")) return;
const owner = repo.owner?.login || repo.full_name?.split("/")[0];
const repoName = repo.name;
const prNumber = issue.number;
if (user.giteaUsername && comment.user?.login === user.giteaUsername) return;
const repoConfig = await import_db.prisma.repoConfig.findFirst({
where: { repoOwner: owner, repoName, userId: user.id, isEnabled: true }
});
if (!repoConfig) return;
const commenter = comment.user?.login || "";
const prAuthor = issue.user?.login || "";
const isAllowed = commenter === prAuthor || await isRepoAdmin(user, owner, repoName, commenter);
if (!isAllowed) return;
await import_db.prisma.preview.updateMany({
where: {
repoConfigId: repoConfig.id,
prNumber,
status: { in: ["RUNNING", "BUILDING", "FAILED", "PROVISIONING"] }
},
data: { lastActivityAt: /* @__PURE__ */ new Date() }
});
const commandLine = body.trimStart().split("\n")[0].trim();
const command = commandLine.replace("/pp ", "").trim();
const preview = await import_db.prisma.preview.findFirst({
where: { repoConfigId: repoConfig.id, prNumber },
orderBy: { createdAt: "desc" }
});
const cloneUrl = repo.clone_url;
if (command === "rebuild") {
if (!preview || preview.status !== "RUNNING" && preview.status !== "FAILED") return;
await import_db.prisma.job.create({
data: {
previewId: preview.id,
type: "DEPLOY",
status: "PENDING",
payload: { commitSha: preview.commitSha, prNumber, prTitle: preview.prTitle, cloneUrl, isFirstDeploy: false }
}
});
} else if (command === "stop") {
if (!preview || preview.status === "STOPPED") return;
await import_db.prisma.job.create({
data: { previewId: preview.id, type: "STOP", status: "PENDING", payload: { reason: "Manual stop" } }
});
} else if (command === "start") {
if (!preview) {
const newPreview = await import_db.prisma.preview.create({
data: { repoConfigId: repoConfig.id, prNumber, prTitle: issue.title, commitSha: "", status: "PROVISIONING", port: repoConfig.port }
});
await import_db.prisma.job.create({
data: { previewId: newPreview.id, type: "DEPLOY", status: "PENDING", payload: { commitSha: "", prNumber, prTitle: issue.title, cloneUrl, isFirstDeploy: true } }
});
} else if (preview.status === "STOPPED" || preview.status === "IGNORED") {
await import_db.prisma.preview.update({ where: { id: preview.id }, data: { status: "PROVISIONING" } });
await import_db.prisma.job.create({
data: { previewId: preview.id, type: "DEPLOY", status: "PENDING", payload: { commitSha: preview.commitSha, prNumber, prTitle: preview.prTitle, cloneUrl, isFirstDeploy: true } }
});
}
} else if (command === "logs") {
if (!preview) return;
const lastLines = (preview.logs || "").split("\n").slice(-50).join("\n");
const logBody = `**PP Logs** (last 50 lines)
\`\`\`
${lastLines}
\`\`\``;
await (0, import_gitea.postComment)(user, owner, repoName, prNumber, logBody);
} else if (command === "ignore") {
if (!preview) {
await import_db.prisma.preview.create({
data: { repoConfigId: repoConfig.id, prNumber, prTitle: issue.title, commitSha: "", status: "IGNORED", port: repoConfig.port }
});
} else {
await import_db.prisma.preview.update({ where: { id: preview.id }, data: { status: "IGNORED" } });
}
}
}
async function isRepoAdmin(user, owner, repo, username) {
try {
const { getRepoCollaboratorPermission } = await import("../services/gitea");
const permission = await getRepoCollaboratorPermission(user, owner, repo, username);
return permission === "owner" || permission === "admin";
} catch {
return false;
}
}
function timingSafeEqual(a, b) {
if (a.length !== b.length) return false;
let diff = 0;
for (let i = 0; i < a.length; i++) {
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return diff === 0;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
webhookHandler
});
//# sourceMappingURL=webhook.js.map
File diff suppressed because one or more lines are too long