Replaces the emailed Supabase signed URL with a bespoke DataExportToken DB record, delivered via a new /api/gdpr-export/[token] route that streams the file server-side. The storage backend is now an internal implementation detail; every link the user sees reads pawfeed.org.
68 lines
2.5 KiB
TypeScript
68 lines
2.5 KiB
TypeScript
import "server-only";
|
|
import { createClient, type SupabaseClient } from "@supabase/supabase-js";
|
|
|
|
/**
|
|
* Storage for GDPR Art. 15 data-export bundles — deliberately a SEPARATE,
|
|
* private bucket from supabase-storage.ts's "pet-avatars" (which is public,
|
|
* fine for avatars/post photos, wrong for a personal-data dump).
|
|
*
|
|
* No Supabase signed URL is ever handed to the end user — the download link
|
|
* emailed out is a pawfeed.org-hosted token (see DataExportToken / the
|
|
* /api/gdpr-export/[token] route), which fetches the bytes from here
|
|
* server-side using the service-role client and streams them through.
|
|
*/
|
|
|
|
let _client: SupabaseClient | null = null;
|
|
|
|
function getClient(): SupabaseClient {
|
|
if (!_client) {
|
|
_client = createClient(
|
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
process.env.SUPABASE_SERVICE_ROLE_KEY!,
|
|
{ auth: { persistSession: false } }
|
|
);
|
|
}
|
|
return _client;
|
|
}
|
|
|
|
export const EXPORTS_BUCKET = "data-exports";
|
|
|
|
/** Idempotent — creates the private bucket if it doesn't exist yet. */
|
|
async function ensureBucketExists(): Promise<void> {
|
|
const { error } = await getClient().storage.createBucket(EXPORTS_BUCKET, {
|
|
public: false,
|
|
});
|
|
// "already exists" is expected on every call after the first — not an error.
|
|
if (error && !error.message.toLowerCase().includes("already exists")) {
|
|
throw new Error(`Storage error creating exports bucket: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Uploads a JSON export bundle. The object itself is left in Storage
|
|
* afterward — no automated cleanup yet (acceptable for now: content is inert
|
|
* JSON, unreachable without a valid DataExportToken).
|
|
*/
|
|
export async function uploadExport(key: string, jsonContent: string): Promise<void> {
|
|
await ensureBucketExists();
|
|
|
|
const { error } = await getClient()
|
|
.storage.from(EXPORTS_BUCKET)
|
|
.upload(key, jsonContent, { contentType: "application/json", upsert: true });
|
|
if (error) throw new Error(`Storage error uploading export: ${error.message}`);
|
|
}
|
|
|
|
/**
|
|
* Downloads an export bundle's raw bytes using the service-role client —
|
|
* called only from the /api/gdpr-export/[token] route after that route has
|
|
* already validated the caller's DataExportToken. Never exposes a Supabase
|
|
* URL to the browser.
|
|
*/
|
|
export async function downloadExport(key: string): Promise<Buffer> {
|
|
const { data, error } = await getClient().storage.from(EXPORTS_BUCKET).download(key);
|
|
if (error || !data) {
|
|
throw new Error(`Storage error downloading export: ${error?.message ?? "unknown"}`);
|
|
}
|
|
return Buffer.from(await data.arrayBuffer());
|
|
}
|