This commit is contained in:
commit
59bb5a38a3
132 files changed
+19435
No files matched your search
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Legt ein Konto fuer den Adminbereich an. Es gibt keine Selbstregistrierung
|
||||
* und kein Standardpasswort.
|
||||
*
|
||||
* pnpm admin:create --email [email protected] --name "Vorname Nachname"
|
||||
*
|
||||
* Passwort: aus ADMIN_PASSWORD (mind. 12 Zeichen) oder zufaellig erzeugt und
|
||||
* EINMAL ausgegeben. Nach der ersten Anmeldung erzwingt der Adminbereich die
|
||||
* Einrichtung der Zwei-Faktor-Anmeldung.
|
||||
*/
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { parseArgs } from "node:util";
|
||||
import { createProvisioningAuth } from "@/lib/auth";
|
||||
import { getDb } from "@/lib/db";
|
||||
|
||||
try {
|
||||
process.loadEnvFile();
|
||||
} catch {
|
||||
// Umgebung kommt aus dem Container.
|
||||
}
|
||||
|
||||
const { values } = parseArgs({ options: { email: { type: "string" }, name: { type: "string" } } });
|
||||
const email = values.email?.trim().toLowerCase();
|
||||
const name = values.name?.trim();
|
||||
if (!email || !/^\S+@\S+\.\S+$/.test(email) || !name) {
|
||||
console.error('Aufruf: pnpm admin:create --email <adresse> --name "<Name>"');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const provided = process.env.ADMIN_PASSWORD;
|
||||
if (provided !== undefined && provided.length < 12) {
|
||||
console.error("ADMIN_PASSWORD muss mindestens 12 Zeichen haben.");
|
||||
process.exit(1);
|
||||
}
|
||||
const password = provided ?? randomBytes(18).toString("base64url");
|
||||
|
||||
const db = getDb();
|
||||
try {
|
||||
if (await db.user.findUnique({ where: { email } })) {
|
||||
console.error("Für diese E-Mail-Adresse existiert bereits ein Konto.");
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
// Offizieller Weg ueber Better Auth: Passwortregeln und Leak-Pruefung
|
||||
// greifen dabei automatisch.
|
||||
const result = await createProvisioningAuth().api.signUpEmail({ body: { email, name, password } });
|
||||
await db.user.update({ where: { id: result.user.id }, data: { emailVerified: true } });
|
||||
await db.auditLog.create({ data: { actorType: "SYSTEM", action: "admin.created", entityType: "User", entityId: result.user.id } });
|
||||
console.log(`Konto angelegt: ${email}`);
|
||||
if (!provided) console.log(`Einmaliges Passwort (jetzt sicher ablegen, wird nicht erneut angezeigt): ${password}`);
|
||||
console.log("Bei der ersten Anmeldung wird die Zwei-Faktor-Anmeldung eingerichtet.");
|
||||
}
|
||||
} finally {
|
||||
await db.$disconnect();
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Lokale PostgreSQL-Instanz fuer die Entwicklung ohne Docker.
|
||||
//
|
||||
// pnpm db:dev startet PostgreSQL auf Port 54329 (Strg+C beendet)
|
||||
//
|
||||
// Mit Docker stattdessen: docker compose up -d db (siehe docker-compose.yml).
|
||||
//
|
||||
// Das Datenverzeichnis liegt bewusst NICHT im Projektordner: dieser liegt
|
||||
// hier in einem OneDrive-synchronisierten Verzeichnis, und ein
|
||||
// PostgreSQL-Datenverzeichnis in einer Cloud-Synchronisierung fuehrt zu
|
||||
// gesperrten oder beschaedigten Dateien.
|
||||
import EmbeddedPostgres from "embedded-postgres";
|
||||
import { existsSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const baseDir = process.env.LOCALAPPDATA ?? path.join(os.homedir(), ".local", "share");
|
||||
const databaseDir = path.join(baseDir, "artista-modelcamp", "dev-db");
|
||||
const port = Number(process.env.DEV_DB_PORT ?? 54329);
|
||||
|
||||
const pg = new EmbeddedPostgres({
|
||||
databaseDir,
|
||||
user: "artista",
|
||||
password: "artista-dev",
|
||||
port,
|
||||
persistent: true,
|
||||
onLog: () => {},
|
||||
});
|
||||
|
||||
if (!existsSync(path.join(databaseDir, "PG_VERSION"))) {
|
||||
await pg.initialise();
|
||||
}
|
||||
await pg.start();
|
||||
try {
|
||||
await pg.createDatabase("artista");
|
||||
} catch {
|
||||
// existiert bereits
|
||||
}
|
||||
|
||||
console.log(`PostgreSQL laeuft: postgresql://artista:artista-dev@localhost:${port}/artista`);
|
||||
console.log(`Daten: ${databaseDir}`);
|
||||
|
||||
const shutdown = async () => {
|
||||
await pg.stop();
|
||||
process.exit(0);
|
||||
};
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
// Prozess am Leben halten.
|
||||
setInterval(() => {}, 1 << 30);
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Testdaten NUR fuer Entwicklung und Staging, damit eine Stripe-Testbuchung
|
||||
* sofort moeglich ist:
|
||||
* - Preisphase "Early Bird" simuliert (wirkt nur mit Stripe-Testschluessel)
|
||||
* - ein TEST-Anreisetag, ausdruecklich NICHT bestaetigt (erscheint deshalb
|
||||
* nicht oeffentlich, erlaubt aber Ratenplaene im Testmodus)
|
||||
* - ein Test-Gutschein TEST-STAMMMODEL (nicht der Beispielcode aus dem Briefing)
|
||||
*
|
||||
* Verweigert den Lauf mit APP_ENV=production.
|
||||
*/
|
||||
import { getDb, CAMP_ID } from "@/lib/db";
|
||||
import { isoToDbDate } from "@/lib/time";
|
||||
|
||||
try {
|
||||
process.loadEnvFile();
|
||||
} catch {
|
||||
// Umgebung kommt aus dem Container.
|
||||
}
|
||||
|
||||
if ((process.env.APP_ENV ?? "development") === "production") {
|
||||
console.error("seed-dev laeuft nicht mit APP_ENV=production.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
try {
|
||||
await db.camp.upsert({
|
||||
where: { id: CAMP_ID },
|
||||
create: { id: CAMP_ID, testPhaseOverride: "EARLY_BIRD", campStartDate: isoToDbDate("2027-06-05"), dateConfirmed: false },
|
||||
update: { testPhaseOverride: "EARLY_BIRD", campStartDate: isoToDbDate("2027-06-05"), dateConfirmed: false },
|
||||
});
|
||||
await db.voucher.upsert({
|
||||
where: { code: "TEST-STAMMMODEL" },
|
||||
create: { code: "TEST-STAMMMODEL", maxRedemptions: 3, internalNote: "Entwicklung: Testgutschein" },
|
||||
update: {},
|
||||
});
|
||||
console.log("Entwicklungsdaten gesetzt: Early Bird simuliert, Test-Anreisetag 05.06.2027 (unbestätigt), Gutschein TEST-STAMMMODEL.");
|
||||
} finally {
|
||||
await db.$disconnect();
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Hintergrundlauf fuer Reservierungen, Folgeraten, Zahlungsmethoden und
|
||||
* E-Mails. Im Cluster als Kubernetes-CronJob (k8s/base/cronjob-worker.yaml),
|
||||
* lokal per `pnpm worker`.
|
||||
*
|
||||
* Endet mit Exit-Code 1, wenn etwas fehlschlaegt — der CronJob-Lauf wird
|
||||
* dann als fehlgeschlagen sichtbar statt still im Log zu verschwinden.
|
||||
*/
|
||||
import { getDb } from "@/lib/db";
|
||||
import { env, requireAppSecret } from "@/lib/env";
|
||||
import { getGateway } from "@/lib/payment-gateway";
|
||||
import { runWorker } from "@/lib/worker-jobs";
|
||||
|
||||
try {
|
||||
process.loadEnvFile();
|
||||
} catch {
|
||||
// Im Container kommen die Variablen aus der Umgebung.
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
try {
|
||||
const result = await runWorker(db, getGateway(), {
|
||||
now: new Date(),
|
||||
secret: requireAppSecret(),
|
||||
adminEmail: env().ADMIN_NOTIFY_EMAIL ?? null,
|
||||
});
|
||||
console.log(`[worker] ${new Date().toISOString()} ${JSON.stringify(result)}`);
|
||||
process.exitCode = result.emails.failed > 0 ? 1 : 0;
|
||||
} catch (error) {
|
||||
console.error(`[worker] Abbruch: ${error instanceof Error ? error.message : "unbekannter Fehler"}`);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
await db.$disconnect();
|
||||
}
|
||||
Reference in new issue
Block a user