feat(export): Projekte exportieren und importieren
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>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
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",
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user