139 lines
4.3 KiB
TypeScript
139 lines
4.3 KiB
TypeScript
import { Router } from "@oak/oak";
|
|
import { getDB } from "../db/mongo.ts";
|
|
import { authMiddleware, type AuthState } from "../middleware/auth.ts";
|
|
import type { Context, Next } from "@oak/oak";
|
|
|
|
export const exportRouter = new Router({ prefix: "/api/data" });
|
|
|
|
async function adminOnly(ctx: Context, next: Next) {
|
|
const user = (ctx.state as AuthState).user;
|
|
if (user.role !== "admin") {
|
|
ctx.response.status = 403;
|
|
ctx.response.body = { error: "Admin access required" };
|
|
return;
|
|
}
|
|
await next();
|
|
}
|
|
|
|
// Export all data as JSON
|
|
exportRouter.get("/export", authMiddleware, adminOnly, async (ctx) => {
|
|
try {
|
|
const db = await getDB();
|
|
const url = ctx.request.url;
|
|
|
|
// Which collections to export (default: all)
|
|
const collectionsParam = url.searchParams.get("collections");
|
|
const available = ["tasks", "projects", "agents", "labels", "logs", "users"];
|
|
const requested = collectionsParam
|
|
? collectionsParam.split(",").filter((c) => available.includes(c))
|
|
: available;
|
|
|
|
const data: Record<string, unknown[]> = {};
|
|
for (const col of requested) {
|
|
data[col] = await db.collection(col).find({}).toArray();
|
|
}
|
|
|
|
const exportPayload = {
|
|
version: "1.0",
|
|
exportedAt: new Date().toISOString(),
|
|
collections: requested,
|
|
data,
|
|
};
|
|
|
|
ctx.response.headers.set(
|
|
"Content-Disposition",
|
|
`attachment; filename="ams-export-${new Date().toISOString().slice(0, 10)}.json"`
|
|
);
|
|
ctx.response.type = "application/json";
|
|
ctx.response.body = exportPayload;
|
|
} catch (error: unknown) {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
ctx.response.status = 500;
|
|
ctx.response.body = { error: "Export failed", message };
|
|
}
|
|
});
|
|
|
|
// Import data from JSON
|
|
exportRouter.post("/import", authMiddleware, adminOnly, async (ctx) => {
|
|
try {
|
|
const body = await ctx.request.body.json();
|
|
const { data, mode } = body as {
|
|
data: Record<string, unknown[]>;
|
|
mode: "merge" | "replace";
|
|
};
|
|
|
|
if (!data || typeof data !== "object") {
|
|
ctx.response.status = 400;
|
|
ctx.response.body = { error: "Invalid import data" };
|
|
return;
|
|
}
|
|
|
|
const db = await getDB();
|
|
const available = ["tasks", "projects", "agents", "labels", "logs"];
|
|
const results: Record<string, { inserted: number; skipped: number }> = {};
|
|
|
|
for (const [collection, documents] of Object.entries(data)) {
|
|
// Skip users for security
|
|
if (!available.includes(collection) || !Array.isArray(documents)) continue;
|
|
|
|
if (mode === "replace") {
|
|
await db.collection(collection).deleteMany({});
|
|
}
|
|
|
|
let inserted = 0;
|
|
let skipped = 0;
|
|
|
|
for (const doc of documents) {
|
|
try {
|
|
const { _id, ...rest } = doc as Record<string, unknown>;
|
|
if (mode === "merge" && _id) {
|
|
// Try to find existing by _id, skip if exists
|
|
const existing = await db
|
|
.collection(collection)
|
|
.findOne({ _id });
|
|
if (existing) {
|
|
skipped++;
|
|
continue;
|
|
}
|
|
}
|
|
await db.collection(collection).insertOne(rest);
|
|
inserted++;
|
|
} catch {
|
|
skipped++;
|
|
}
|
|
}
|
|
|
|
results[collection] = { inserted, skipped };
|
|
}
|
|
|
|
ctx.response.body = {
|
|
success: true,
|
|
message: "Import completed",
|
|
results,
|
|
};
|
|
} catch (error: unknown) {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
ctx.response.status = 500;
|
|
ctx.response.body = { error: "Import failed", message };
|
|
}
|
|
});
|
|
|
|
// Get export stats (what's available to export)
|
|
exportRouter.get("/stats", authMiddleware, adminOnly, async (ctx) => {
|
|
try {
|
|
const db = await getDB();
|
|
const collections = ["tasks", "projects", "agents", "labels", "logs", "users"];
|
|
const stats: Record<string, number> = {};
|
|
|
|
for (const col of collections) {
|
|
stats[col] = await db.collection(col).countDocuments({});
|
|
}
|
|
|
|
ctx.response.body = { stats };
|
|
} catch (error: unknown) {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
ctx.response.status = 500;
|
|
ctx.response.body = { error: "Failed to get stats", message };
|
|
}
|
|
});
|