diff --git a/src/app/(dashboard)/dashboard/profile/page.js b/src/app/(dashboard)/dashboard/profile/page.js index 14ec932e..b1d1ea69 100644 --- a/src/app/(dashboard)/dashboard/profile/page.js +++ b/src/app/(dashboard)/dashboard/profile/page.js @@ -1,7 +1,7 @@ "use client"; -import { useState, useEffect } from "react"; -import { Card, Button, Badge, Toggle, Input } from "@/shared/components"; +import { useState, useEffect, useRef } from "react"; +import { Card, Button, Toggle, Input } from "@/shared/components"; import { useTheme } from "@/shared/hooks/useTheme"; import { cn } from "@/shared/utils/cn"; import { APP_CONFIG } from "@/shared/constants/config"; @@ -13,6 +13,9 @@ export default function ProfilePage() { const [passwords, setPasswords] = useState({ current: "", new: "", confirm: "" }); const [passStatus, setPassStatus] = useState({ type: "", message: "" }); const [passLoading, setPassLoading] = useState(false); + const [dbLoading, setDbLoading] = useState(false); + const [dbStatus, setDbStatus] = useState({ type: "", message: "" }); + const importFileRef = useRef(null); useEffect(() => { fetch("/api/settings") @@ -143,6 +146,82 @@ export default function ProfilePage() { } }; + const reloadSettings = async () => { + try { + const res = await fetch("/api/settings"); + if (!res.ok) return; + const data = await res.json(); + setSettings(data); + } catch (err) { + console.error("Failed to reload settings:", err); + } + }; + + const handleExportDatabase = async () => { + setDbLoading(true); + setDbStatus({ type: "", message: "" }); + try { + const res = await fetch("/api/settings/database"); + if (!res.ok) { + const data = await res.json().catch(() => ({})); + throw new Error(data.error || "Failed to export database"); + } + + const payload = await res.json(); + const content = JSON.stringify(payload, null, 2); + const blob = new Blob([content], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + const stamp = new Date().toISOString().replace(/[.:]/g, "-"); + anchor.href = url; + anchor.download = `9router-backup-${stamp}.json`; + document.body.appendChild(anchor); + anchor.click(); + document.body.removeChild(anchor); + URL.revokeObjectURL(url); + + setDbStatus({ type: "success", message: "Database backup downloaded" }); + } catch (err) { + setDbStatus({ type: "error", message: err.message || "Failed to export database" }); + } finally { + setDbLoading(false); + } + }; + + const handleImportDatabase = async (event) => { + const file = event.target.files?.[0]; + if (!file) return; + + setDbLoading(true); + setDbStatus({ type: "", message: "" }); + + try { + const raw = await file.text(); + const payload = JSON.parse(raw); + + const res = await fetch("/api/settings/database", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + const data = await res.json().catch(() => ({})); + if (!res.ok) { + throw new Error(data.error || "Failed to import database"); + } + + await reloadSettings(); + setDbStatus({ type: "success", message: "Database imported successfully" }); + } catch (err) { + setDbStatus({ type: "error", message: err.message || "Invalid backup file" }); + } finally { + if (importFileRef.current) { + importFileRef.current.value = ""; + } + setDbLoading(false); + } + }; + const observabilityEnabled = settings.observabilityEnabled !== false; return ( @@ -363,6 +442,36 @@ export default function ProfilePage() {

~/.9router/db.json

+
+ + + +
+ {dbStatus.message && ( +

+ {dbStatus.message} +

+ )} diff --git a/src/app/api/settings/database/route.js b/src/app/api/settings/database/route.js new file mode 100644 index 00000000..403f3110 --- /dev/null +++ b/src/app/api/settings/database/route.js @@ -0,0 +1,26 @@ +import { NextResponse } from "next/server"; +import { exportDb, importDb } from "@/lib/localDb"; + +export async function GET() { + try { + const payload = await exportDb(); + return NextResponse.json(payload); + } catch (error) { + console.log("Error exporting database:", error); + return NextResponse.json({ error: "Failed to export database" }, { status: 500 }); + } +} + +export async function POST(request) { + try { + const payload = await request.json(); + await importDb(payload); + return NextResponse.json({ success: true }); + } catch (error) { + console.log("Error importing database:", error); + return NextResponse.json( + { error: error?.message || "Failed to import database" }, + { status: 400 } + ); + } +} diff --git a/src/lib/localDb.js b/src/lib/localDb.js index 5a9ab193..1db52ccf 100644 --- a/src/lib/localDb.js +++ b/src/lib/localDb.js @@ -779,6 +779,41 @@ export async function updateSettings(updates) { return db.data.settings; } +/** + * Export full database payload + */ +export async function exportDb() { + const db = await getDb(); + return db.data || cloneDefaultData(); +} + +/** + * Import full database payload + */ +export async function importDb(payload) { + if (!payload || typeof payload !== "object" || Array.isArray(payload)) { + throw new Error("Invalid database payload"); + } + + const nextData = { + ...cloneDefaultData(), + ...payload, + settings: { + ...cloneDefaultData().settings, + ...(payload.settings && typeof payload.settings === "object" && !Array.isArray(payload.settings) + ? payload.settings + : {}), + }, + }; + + const { data: normalized } = ensureDbShape(nextData); + const db = await getDb(); + db.data = normalized; + await db.write(); + + return db.data; +} + /** * Check if cloud is enabled */