feat(deploy): Auslieferung als Docker-Stack und als Helper-Script (#026)

Docker: deploy/update.sh sichert, baut, migriert und prueft die
Erreichbarkeit; der Worker hat jetzt eine Zustandspruefung. Der ganze Stapel
wurde durchgespielt - bauen, starten, migrieren, anmelden, Agent-Skript
abrufen.

Dabei ein echter Fehler aufgefallen: Nach der Anmeldung zeigte die
Weiterleitung auf den internen Containernamen. Hinter einem Reverse Proxy
waere das eine tote Adresse gewesen. Weiterleitungen nehmen jetzt die
eingetragene Adresse, sonst APP_URL, sonst die Proxy-Kopfzeilen.

Dazu Skripte nach Art der Proxmox VE Helper-Scripts (Node und PostgreSQL
direkt im LXC, ohne Docker). Vorerst intern: Eine Einreichung setzt
oeffentlichen Quelltext voraus und laeuft ueber ProxmoxVED.

Ausserdem docs/roadmap.md: Bis 1.0.0 fehlt kein Code mehr, sondern
Erprobung.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-19 00:11:33 +02:00
co-authored by Claude Opus 5
parent ffc80a8fcf
commit 6c3729bae8
17 changed files with 601 additions and 19 deletions
+4 -2
View File
@@ -1,4 +1,5 @@
import { cookies, headers } from "next/headers";
import { eigeneAdresse } from "@/lib/adresse";
import { clientIp } from "@/lib/ratelimit";
import { cookieOptionen, SITZUNGS_COOKIE, sitzungErzeugen } from "@/lib/sitzung";
import { AnmeldeFehler, mitPasswortAnmelden } from "@/server/auth/benutzer";
@@ -38,6 +39,7 @@ export function herkunftStimmt(request: Request): boolean {
export async function POST(request: Request) {
const formular = await request.formData();
const { dashboardUrl } = await einstellungen();
const email = formular.get("email")?.toString().trim() ?? "";
const passwort = formular.get("passwort")?.toString() ?? "";
const zielEingabe = formular.get("ziel")?.toString() || "/";
@@ -45,7 +47,7 @@ export async function POST(request: Request) {
const ziel = zielEingabe.startsWith("/") && !zielEingabe.startsWith("//") ? zielEingabe : "/";
function zurueckZurAnmeldung(meldung: string) {
const adresse = new URL("/login", request.url);
const adresse = eigeneAdresse(request, "/login", dashboardUrl);
adresse.searchParams.set("fehler", meldung);
if (ziel !== "/") adresse.searchParams.set("ziel", ziel);
return Response.redirect(adresse, 303);
@@ -75,7 +77,7 @@ export async function POST(request: Request) {
);
(await cookies()).set(SITZUNGS_COOKIE, token, cookieOptionen(sitzungsdauerStunden));
return Response.redirect(new URL(ziel, request.url), 303);
return Response.redirect(eigeneAdresse(request, ziel, dashboardUrl), 303);
} catch (fehler) {
if (fehler instanceof AnmeldeFehler) return zurueckZurAnmeldung(fehler.message);
console.error("Anmeldung fehlgeschlagen:", fehler);
+6 -3
View File
@@ -1,4 +1,5 @@
import { cookies } from "next/headers";
import { eigeneAdresse } from "@/lib/adresse";
import { cookieOptionen, SITZUNGS_COOKIE, sitzungErzeugen } from "@/lib/sitzung";
import { mitOidcAnmelden } from "@/server/auth/benutzer";
import { anmeldungAbschliessen, oidcEinstellungen } from "@/server/auth/oidc";
@@ -18,8 +19,10 @@ export async function GET(request: Request) {
speicher.delete(name);
}
const { dashboardUrl } = await systemEinstellungen();
if (!einstellungen || !pruefwert || !zustand) {
return Response.redirect(new URL("/login?fehler=oidc", request.url), 302);
return Response.redirect(eigeneAdresse(request, "/login?fehler=oidc", dashboardUrl), 302);
}
try {
@@ -38,9 +41,9 @@ export async function GET(request: Request) {
);
speicher.set(SITZUNGS_COOKIE, token, cookieOptionen(sitzungsdauerStunden));
return Response.redirect(new URL(ziel.startsWith("/") ? ziel : "/", request.url), 302);
return Response.redirect(eigeneAdresse(request, ziel.startsWith("/") ? ziel : "/", dashboardUrl), 302);
} catch (fehler) {
console.error("OIDC-Rückkehr fehlgeschlagen:", fehler);
return Response.redirect(new URL("/login?fehler=oidc", request.url), 302);
return Response.redirect(eigeneAdresse(request, "/login?fehler=oidc", dashboardUrl), 302);
}
}