Bislang lagen alle Daten ausschliesslich in der Datenbank - ein "docker compose down -v" haette alles geloescht, ohne Rueckweg. Export: - alle eigenen Projekte oder ein einzelnes als JSON - ein Projekt zusaetzlich als lesbares Markdown, inklusive letztem bekannten Git-Stand Was bewusst nicht in der Datei steht: - Access Tokens. Die Verbindung wird nur beschrieben (Name, System, Basis-URL), das Token muss beim Import neu hinterlegt werden. Eine Exportdatei landet sonst schnell in einem Download-Ordner oder einem Cloud-Abgleich. - Repo-Snapshots. Reproduzierbar und gross; ein Abgleich holt sie zurueck. - Datenbank-IDs und der Besitzer. Beides gilt nur in der Quellinstanz. Import: - die hochgeladene Datei ist Fremdinhalt und wird vor jeder Verwendung streng gegen ein Zod-Schema geprueft - Abgleich ueber den Slug, drei Strategien: ueberspringen, zusaetzlich anlegen, ersetzen - dryRun liefert dieselbe Auswertung, ohne zu schreiben - Grundlage der Vorschau in der Oberflaeche - Verbindungen werden ueber Name oder Provider samt Basis-URL zugeordnet; fehlt eine, wird das Projekt ohne Verknuepfung angelegt und das gemeldet - beim Ersetzen werden Notizen ausgetauscht statt zusammengefuehrt: das ist vorhersagbar, und ein erneuter Import erzeugt keine Dubletten Die Instanz-Sicherung fuer Administratoren enthaelt dagegen alles, was zum Wiederherstellen noetig ist - Passwort-Hashes und Token-Chiffrate eingeschlossen. Ohne dasselbe APP_SECRET sind die Tokens wertlos. Sitzungen und Passkeys bleiben draussen: erstere sind kurzlebig, letztere haengen an Geraet und rpID. readJsonBody bekommt eine einstellbare Obergrenze - 256 KB reichen fuer eine Exportdatei mit vielen Notizen nicht. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
89 lines
2.5 KiB
TypeScript
89 lines
2.5 KiB
TypeScript
import { requireAdminApi } from "@/lib/auth/guard";
|
|
import { APP_NAME } from "@/lib/config";
|
|
import { prisma } from "@/lib/db";
|
|
import { exportFileName } from "@/lib/export/format";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
/**
|
|
* Vollstaendige Sicherung der Instanz als JSON.
|
|
*
|
|
* Anders als der Projekt-Export ist das eine echte Sicherung und enthaelt
|
|
* deshalb auch Konten und Verbindungen. Was drinsteht:
|
|
*
|
|
* - Passwoerter nur als scrypt-Hash, nicht im Klartext
|
|
* - Git-Tokens nur als Chiffrat; ohne dasselbe APP_SECRET sind sie wertlos
|
|
* - Sitzungen und Passkeys bewusst nicht: Sitzungen sind kurzlebig, und
|
|
* Passkeys haengen ohnehin am Geraet und an der rpID
|
|
*
|
|
* Die Datei ist damit vertraulich zu behandeln. Fuer eine byteweise Sicherung
|
|
* der Datenbank siehe scripts/backup.* mit pg_dump.
|
|
*/
|
|
export async function GET() {
|
|
const auth = await requireAdminApi();
|
|
if (!auth.ok) return auth.response;
|
|
|
|
const [settings, users, connections, projects] = await Promise.all([
|
|
prisma.appSettings.findUnique({ where: { id: "singleton" } }),
|
|
prisma.user.findMany({
|
|
select: {
|
|
id: true,
|
|
username: true,
|
|
displayName: true,
|
|
email: true,
|
|
passwordHash: true,
|
|
role: true,
|
|
isActive: true,
|
|
createdAt: true,
|
|
lastLoginAt: true,
|
|
},
|
|
orderBy: { username: "asc" },
|
|
}),
|
|
prisma.gitConnection.findMany({
|
|
select: {
|
|
id: true,
|
|
ownerId: true,
|
|
name: true,
|
|
provider: true,
|
|
baseUrl: true,
|
|
username: true,
|
|
tokenCipher: true,
|
|
tokenHint: true,
|
|
createdAt: true,
|
|
},
|
|
orderBy: { name: "asc" },
|
|
}),
|
|
prisma.project.findMany({
|
|
include: {
|
|
notes: { orderBy: { createdAt: "asc" } },
|
|
// Snapshots bleiben draussen: reproduzierbar und gross.
|
|
},
|
|
orderBy: { name: "asc" },
|
|
}),
|
|
]);
|
|
|
|
const payload = {
|
|
kind: "nebula.backup",
|
|
version: 1,
|
|
exportedAt: new Date().toISOString(),
|
|
app: APP_NAME,
|
|
note:
|
|
"Enthaelt Passwort-Hashes und verschluesselte Git-Tokens. " +
|
|
"Die Tokens sind nur mit demselben APP_SECRET wieder lesbar. Vertraulich behandeln.",
|
|
settings,
|
|
users,
|
|
connections,
|
|
projects,
|
|
};
|
|
|
|
const filename = exportFileName(`${APP_NAME}-sicherung`, new Date(), "json");
|
|
|
|
return new Response(JSON.stringify(payload, null, 2), {
|
|
headers: {
|
|
"Content-Type": "application/json; charset=utf-8",
|
|
"Content-Disposition": `attachment; filename="${filename}"`,
|
|
"Cache-Control": "no-store",
|
|
},
|
|
});
|
|
}
|