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
+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