package handlers import ( "net/http" "time" "github.com/andrew/go-computer/internal/db" ) // Structured export (trust phase): a single owner-scoped JSON archive that is // richer than the per-item Markdown downloads. It deliberately excludes // credentials (IMAP config, CardDAV passwords) and includes only data the // caller owns or may read through the owner projection. func HandleExport(w http.ResponseWriter, r *http.Request) { ctx := r.Context() uid := user(r) me, e := db.One(ctx, database(r), `SELECT jsonb_build_object('id',u.id,'name',u.name,'email',u.email,'role',u.role,'createdAt',u."createdAt") FROM "User" u WHERE u.id=$1`, uid) if e != nil { respond(w, 0, nil, e) return } bookmarks, e := db.Many(ctx, database(r), `SELECT `+db.BookmarkOwnerView+` FROM "Bookmark" b WHERE b."ownerId"=$1 ORDER BY b."createdAt",b.id`, uid) if e != nil { respond(w, 0, nil, e) return } tags, e := db.Many(ctx, database(r), `SELECT to_jsonb(t)||jsonb_build_object('sharedWith',COALESCE((SELECT jsonb_agg(jsonb_build_object('userId',s."userId",'email',u.email,'name',u.name)) FROM "TagShare" s JOIN "User" u ON u.id=s."userId" WHERE s."tagId"=t.id),'[]'::jsonb)) FROM "Tag" t WHERE t."ownerId"=$1 ORDER BY t."createdAt",t.id`, uid) if e != nil { respond(w, 0, nil, e) return } pages, e := db.Many(ctx, database(r), `SELECT to_jsonb(p)||jsonb_build_object( 'bookmarks',COALESCE((SELECT jsonb_agg(to_jsonb(pb)||jsonb_build_object('bookmark',`+db.BookmarkOwnerView+`) ORDER BY pb.position) FROM "PageBookmark" pb JOIN "Bookmark" b ON b.id=pb."bookmarkId" WHERE pb."pageId"=p.id AND b."ownerId"=$1),'[]'::jsonb), 'revisions',COALESCE((SELECT jsonb_agg(to_jsonb(r) ORDER BY r.version) FROM "PageRevision" r WHERE r."pageId"=p.id),'[]'::jsonb)) FROM "Page" p WHERE p."ownerId"=$1 ORDER BY p."createdAt",p.id`, uid) if e != nil { respond(w, 0, nil, e) return } contacts, e := db.Many(ctx, database(r), `SELECT to_jsonb(c) FROM "Contact" c WHERE c."userId"=$1 ORDER BY c."displayName",c.id`, uid) if e != nil { respond(w, 0, nil, e) return } collections, e := db.Many(ctx, database(r), `SELECT to_jsonb(c) FROM "Collection" c WHERE c."ownerId"=$1 ORDER BY c."createdAt",c.id`, uid) if e != nil { respond(w, 0, nil, e) return } exported := make([]object, 0, len(collections)) for _, col := range collections { detail, e := collectionDetail(r, database(r), col) if e != nil { respond(w, 0, nil, e) return } cid := str(col, "id") items, e := db.Many(ctx, database(r), `SELECT to_jsonb(ci)||jsonb_build_object('bookmark',`+db.BookmarkOwnerView+`) FROM "CollectionItem" ci JOIN "Bookmark" b ON b.id=ci."bookmarkId" WHERE ci."collectionId"=$1 ORDER BY ci.position,ci.id`, cid) if e != nil { respond(w, 0, nil, e) return } shares, e := db.Many(ctx, database(r), `SELECT jsonb_build_object('userId',s."userId",'email',u.email,'name',u.name,'createdAt',s."createdAt") FROM "CollectionShare" s JOIN "User" u ON u.id=s."userId" WHERE s."collectionId"=$1 ORDER BY s."createdAt"`, cid) if e != nil { respond(w, 0, nil, e) return } detail["items"], detail["sharedWith"] = items, shares exported = append(exported, detail) } w.Header().Set("Content-Disposition", `attachment; filename="computer-export.json"`) respond(w, 200, object{ "format": "computer-export", "version": 1, "exportedAt": time.Now().UTC(), "user": me, "bookmarks": bookmarks, "tags": tags, "pages": pages, "collections": exported, "contacts": contacts, }, nil) }