mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-20 14:12:59 +03:00
feat: restore legacy JSON config import/export (#1012)
Adds legacy 9router JSON config import/export with Zero-Trust security (password/requireLogin redacted). Integrated into release/v3.5.4.
This commit is contained in:
59
src/app/api/settings/export-json/route.ts
Normal file
59
src/app/api/settings/export-json/route.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
getSettings,
|
||||
getProviderConnections,
|
||||
getProviderNodes,
|
||||
getCombos,
|
||||
getApiKeys,
|
||||
} from "@/lib/localDb";
|
||||
import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
|
||||
/**
|
||||
* GET /api/settings/export-json
|
||||
* Exports a legacy 9router compatible JSON backup.
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
if (await isAuthRequired()) {
|
||||
if (!(await isAuthenticated(request))) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const rawSettings = await getSettings();
|
||||
|
||||
// REDACT sensitive security keys to maintain Zero-Trust posture
|
||||
// even if the admin shares their backup file.
|
||||
// Use destructuring (not delete) to avoid mutating a potentially cached object.
|
||||
const { password: _pw, requireLogin: _rl, ...safeSettings } = rawSettings;
|
||||
|
||||
const providerConnections = await getProviderConnections();
|
||||
const providerNodes = await getProviderNodes();
|
||||
const combos = await getCombos();
|
||||
const apiKeys = await getApiKeys();
|
||||
|
||||
const exportData = {
|
||||
settings: safeSettings,
|
||||
providerConnections,
|
||||
providerNodes,
|
||||
combos,
|
||||
apiKeys,
|
||||
// Metadata to identify export version
|
||||
_meta: {
|
||||
exportedAt: new Date().toISOString(),
|
||||
version: "omniroute-v3-legacy-export"
|
||||
}
|
||||
};
|
||||
|
||||
return new NextResponse(JSON.stringify(exportData, null, 2), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Disposition": `attachment; filename="omniroute-legacy-backup-${new Date().toISOString().replace(/[:.]/g, "-")}.json"`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[API] Error exporting JSON backup:", error);
|
||||
return NextResponse.json({ error: "Failed to export JSON" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
80
src/app/api/settings/import-json/route.ts
Normal file
80
src/app/api/settings/import-json/route.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getDbInstance } from "@/lib/db/core";
|
||||
import { backupDbFile } from "@/lib/db/backup";
|
||||
import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { runJsonMigration, type LegacyJsonData } from "@/lib/db/jsonMigration";
|
||||
|
||||
/**
|
||||
* POST /api/settings/import-json
|
||||
*
|
||||
* Imports a legacy 9router / OmniRoute JSON backup into the current SQLite
|
||||
* database. Accepts either multipart/form-data (file field) or a raw JSON body.
|
||||
*
|
||||
* 🔒 Auth-guarded.
|
||||
* 🔒 Zero-Trust: password and requireLogin keys are stripped before insertion.
|
||||
* 🔒 A pre-import backup is created automatically before any data is written.
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
if (await isAuthRequired()) {
|
||||
if (!(await isAuthenticated(request))) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let rawText: string | null = null;
|
||||
const contentType = request.headers.get("content-type") ?? "";
|
||||
|
||||
if (contentType.includes("multipart/form-data")) {
|
||||
const formData = await request.formData();
|
||||
const file = formData.get("file") as File | null;
|
||||
if (!file) return NextResponse.json({ error: "No json file provided" }, { status: 400 });
|
||||
rawText = await file.text();
|
||||
} else {
|
||||
rawText = await request.text();
|
||||
}
|
||||
|
||||
if (!rawText?.trim()) {
|
||||
return NextResponse.json({ error: "Empty request payload" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Parse with explicit 400 on malformed JSON (Gemini suggestion)
|
||||
let data: LegacyJsonData;
|
||||
try {
|
||||
data = JSON.parse(rawText) as LegacyJsonData;
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid JSON: the file could not be parsed. Please upload a valid .json backup." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 🔒 Zero-Trust: strip authentication config before migration
|
||||
if (data.settings) {
|
||||
const { password: _pw, requireLogin: _rl, ...safeSettings } = data.settings;
|
||||
data = { ...data, settings: safeSettings };
|
||||
}
|
||||
|
||||
const db = getDbInstance();
|
||||
|
||||
// Create a safety backup before writing anything
|
||||
backupDbFile("pre-json-import");
|
||||
|
||||
// Delegate the actual migration to the shared helper (avoids duplication with core.ts)
|
||||
const counts = runJsonMigration(db, data);
|
||||
|
||||
console.log(
|
||||
`[JSON Import] Imported ${counts.connections} connections, ${counts.nodes} nodes, ` +
|
||||
`${counts.combos} combos, ${counts.apiKeys} API keys`
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "Legacy JSON database imported successfully",
|
||||
...counts,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("[API] Error importing JSON backup:", err);
|
||||
return NextResponse.json({ error: (err as Error).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user