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
+74
View File
@@ -0,0 +1,74 @@
const BASE = "/api";
async function request<T = any>(
method: string,
path: string,
body?: any,
): Promise<{ ok: boolean; data?: T; message?: string; status: number }> {
const res = await fetch(`${BASE}${path}`, {
method,
headers: body ? { "Content-Type": "application/json" } : undefined,
body: body ? JSON.stringify(body) : undefined,
credentials: "include",
});
let json: any = {};
try {
json = await res.json();
} catch {}
return { ok: res.ok, data: json.data, message: json.message, status: res.status };
}
export const api = {
auth: {
login: (username: string, password: string) => request("POST", "/auth/login", { username, password }),
logout: () => request("POST", "/auth/logout"),
me: () => request("GET", "/auth/me"),
setupStatus: () => request("GET", "/auth/setup-status"),
firstUser: (username: string, password: string) => request("POST", "/auth/first-user", { username, password }),
},
user: {
settings: () => request("GET", "/user/settings"),
updateUsername: (username: string) => request("PATCH", "/user/username", { username }),
updatePassword: (currentPassword: string, newPassword: string) =>
request("PATCH", "/user/password", { currentPassword, newPassword }),
updateGitea: (data: { giteaInstanceUrl: string; giteaUsername: string; giteaPAT?: string }) =>
request("PUT", "/user/gitea", data),
updateAws: (data: { awsAccessKeyId: string; awsSecretAccessKey: string; awsRegion: string }) =>
request("PUT", "/user/aws", data),
getWebhookSecret: () => request("GET", "/user/webhook-secret"),
regenerateWebhookSecret: () => request("POST", "/user/webhook-secret/regenerate"),
},
repos: {
list: () => request("GET", "/repos"),
getConfig: (owner: string, repo: string) => request("GET", `/repos/${owner}/${repo}/config`),
saveConfig: (data: any) => request("POST", "/repos/config", data),
toggle: (owner: string, repo: string, enabled: boolean) => request("POST", "/repos/toggle", { owner, repo, enabled }),
},
previews: {
list: () => request("GET", "/previews"),
get: (id: number) => request("GET", `/previews/${id}`),
stop: (id: number) => request("POST", `/previews/${id}/stop`),
},
admin: {
listUsers: () => request("GET", "/admin/users"),
createUser: (username: string, password: string) => request("POST", "/admin/users", { username, password }),
updateUser: (id: number, data: any) => request("PATCH", `/admin/users/${id}`, data),
deleteUser: (id: number) => request("DELETE", `/admin/users/${id}`),
getSettings: () => request("GET", "/admin/settings"),
updateSettings: (data: any) => request("PUT", "/admin/settings", data),
listPreviews: () => request("GET", "/admin/previews"),
stopPreview: (id: number) => request("POST", `/admin/previews/${id}/stop`),
},
};
export function openLogsWs(previewId: number, onMessage: (msg: any) => void, onClose?: () => void): WebSocket {
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
const ws = new WebSocket(`${protocol}//${location.host}/api/previews/${previewId}/logs`);
ws.onmessage = (e) => {
try { onMessage(JSON.parse(e.data)); } catch {}
};
ws.onclose = onClose || (() => {});
return ws;
}