mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 02:45:39 -04:00
feat: add user data export and sofa export re-import support
- Add `GET /api/export/user-data` route that streams a JSON attachment of the authenticated user's full library data, named `sofa-export-<name>-<date>.json` - Add `generateUserExport` in `@sofa/core/export` and a matching `parseSofaExport` parser so exported files can be re-imported via the existing import pipeline - Register `sofa` as a new `ImportSource` in the API contract/schemas and wire up `parseSofaExport` in the `parseFile` procedure - Add `EXPORT_FAILED` error code to the API error registry and surface it in web and native error-message maps - Update the account settings section with export/import UI (download button, import progress) - Add tests for `generateUserExport`, `parseSofaExport`, and round-trip fidelity
This commit is contained in:
@@ -16,6 +16,7 @@ import { openApiHandler } from "./orpc/openapi-handler";
|
||||
import authRoutes from "./routes/auth";
|
||||
import avatarsRoutes from "./routes/avatars";
|
||||
import backupsRoutes from "./routes/backups";
|
||||
import exportRoutes from "./routes/export";
|
||||
import healthRoutes from "./routes/health";
|
||||
import imagesRoutes from "./routes/images";
|
||||
import listsRoutes from "./routes/lists";
|
||||
@@ -66,6 +67,7 @@ app.route("/api/health", healthRoutes);
|
||||
app.route("/api/auth", authRoutes);
|
||||
app.route("/api/avatars", avatarsRoutes);
|
||||
app.route("/api/backup", backupsRoutes);
|
||||
app.route("/api/export", exportRoutes);
|
||||
app.route("/api/webhooks", webhooksRoutes);
|
||||
app.route("/api/lists", listsRoutes);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
insertImportJob,
|
||||
parseLetterboxdExport,
|
||||
parseSimklPayload,
|
||||
parseSofaExport,
|
||||
parseTraktPayload,
|
||||
processImportJob,
|
||||
readImportJob,
|
||||
@@ -57,6 +58,19 @@ export const parseFile = os.imports.parseFile.use(authed).handler(async ({ input
|
||||
result = parseSimklPayload(json as Parameters<typeof parseSimklPayload>[0]);
|
||||
break;
|
||||
}
|
||||
case "sofa": {
|
||||
let json: unknown;
|
||||
try {
|
||||
json = await file.json();
|
||||
} catch {
|
||||
throw new ORPCError("BAD_REQUEST", {
|
||||
message: "Invalid JSON file",
|
||||
data: { code: AppErrorCode.IMPORT_INVALID_FILE },
|
||||
});
|
||||
}
|
||||
result = parseSofaExport(json);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Hono } from "hono";
|
||||
|
||||
import { auth } from "@sofa/auth/server";
|
||||
import { generateUserExport } from "@sofa/core/export";
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
app.get("/user-data", async (c) => {
|
||||
const session = await auth.api.getSession({ headers: c.req.raw.headers });
|
||||
if (!session) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
const data = generateUserExport(session.user.id, {
|
||||
name: session.user.name,
|
||||
email: session.user.email,
|
||||
});
|
||||
|
||||
const safeName = session.user.name.replaceAll(/[^a-zA-Z0-9-_]/g, "-").toLowerCase();
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
const filename = `sofa-export-${safeName}-${date}.json`;
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
|
||||
return new Response(json, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Content-Disposition": `attachment; filename="${filename}"`,
|
||||
"Content-Length": String(new TextEncoder().encode(json).byteLength),
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export default app;
|
||||
Reference in New Issue
Block a user