File size: 3,574 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
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";
import { getSettings } from "@/lib/db/settings";
import { setSystemPromptConfig } from "@omniroute/open-sse/services/systemPrompt.ts";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";

/**
 * POST /api/settings/import-json
 *
 * Imports a legacy 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(request)) {
    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);

    // Re-hydrate the in-memory Global System Prompt config β€” the migration writes it to
    // the DB but the in-memory state would stay stale until a restart otherwise (#2470).
    const importedSettings = await getSettings();
    if (importedSettings.systemPrompt) {
      setSystemPromptConfig(importedSettings.systemPrompt);
    }

    console.log(
      `[JSON Import] Imported ${counts.connections} connections, ${counts.nodes} nodes, ` +
        `${counts.combos} combos, ${counts.apiKeys} API keys, ` +
        `${counts.usageHistory} usage rows, ${counts.domainCostHistory} cost rows, ` +
        `${counts.domainBudgets} budgets`
    );

    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: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
      { status: 500 }
    );
  }
}