From 39cdf1cafe875fb50041e15ee7cad737054a9743 Mon Sep 17 00:00:00 2001 From: Punyaslok Dutta Date: Sat, 9 May 2026 03:37:29 +0530 Subject: [PATCH] =?UTF-8?q?chore:=20local=20setup=20=E2=80=94=20swap=20R2?= =?UTF-8?q?=20for=20Supabase=20Storage,=20install=20frontend=20deps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - storage.ts: replaced @aws-sdk/client-s3 + R2 with Supabase Storage (upload, download, delete, signed URLs all via @supabase/supabase-js) - .env.example: removed R2 vars, added STORAGE_BUCKET=mike - frontend/package-lock.json: updated after npm install --legacy-peer-deps Local setup recap: - Backend :3001, frontend :3000 - Supabase project: gbdfkvaigunfvrgurkwk (ap-northeast-1 Tokyo) - Storage bucket: mike (private, Supabase Storage) - DB schema applied via 000_one_shot_schema.sql - AI provider: Gemini - Secrets in .env / .env.local — gitignored, not committed Co-Authored-By: Claude Sonnet 4.6 --- backend/.env.example | 5 +- backend/src/lib/storage.ts | 97 ++++++++++++++------------------------ frontend/package-lock.json | 25 ---------- 3 files changed, 37 insertions(+), 90 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index 1db370a9..33c54d1c 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -3,10 +3,7 @@ FRONTEND_URL=http://localhost:3000 SUPABASE_URL=https://your-project.supabase.co SUPABASE_SECRET_KEY=your-supabase-service-role-key -R2_ENDPOINT_URL=https://your-account-id.r2.cloudflarestorage.com -R2_ACCESS_KEY_ID=your-r2-access-key -R2_SECRET_ACCESS_KEY=your-r2-secret-key -R2_BUCKET_NAME=mike +STORAGE_BUCKET=mike GEMINI_API_KEY=your-gemini-key ANTHROPIC_API_KEY=your-anthropic-key diff --git a/backend/src/lib/storage.ts b/backend/src/lib/storage.ts index 82e7f236..9a6836d1 100644 --- a/backend/src/lib/storage.ts +++ b/backend/src/lib/storage.ts @@ -1,41 +1,29 @@ /** - * Cloudflare R2 storage utilities for Mike document management. - * R2 is S3-compatible — uses @aws-sdk/client-s3. + * Supabase Storage utilities for Mike document management. * * Required env vars: - * R2_ENDPOINT_URL — https://.r2.cloudflarestorage.com - * R2_ACCESS_KEY_ID — R2 API token (Access Key ID) - * R2_SECRET_ACCESS_KEY — R2 API token (Secret Access Key) - * R2_BUCKET_NAME — bucket name (default: "mike") + * SUPABASE_URL — your Supabase project URL + * SUPABASE_SECRET_KEY — service role key (bypasses RLS) + * STORAGE_BUCKET — storage bucket name (default: "mike") */ -import { - S3Client, - PutObjectCommand, - GetObjectCommand, - DeleteObjectCommand, -} from "@aws-sdk/client-s3"; -import { getSignedUrl as awsGetSignedUrl } from "@aws-sdk/s3-request-presigner"; - -function getClient(): S3Client { - return new S3Client({ - region: "auto", - endpoint: process.env.R2_ENDPOINT_URL!, - credentials: { - accessKeyId: process.env.R2_ACCESS_KEY_ID!, - secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!, - }, - }); -} +import { createClient } from "@supabase/supabase-js"; -const BUCKET = process.env.R2_BUCKET_NAME ?? "mike"; +const BUCKET = process.env.STORAGE_BUCKET ?? "mike"; export const storageEnabled = Boolean( - process.env.R2_ENDPOINT_URL && - process.env.R2_ACCESS_KEY_ID && - process.env.R2_SECRET_ACCESS_KEY, + process.env.SUPABASE_URL && + process.env.SUPABASE_SECRET_KEY, ); +function getClient() { + return createClient( + process.env.SUPABASE_URL!, + process.env.SUPABASE_SECRET_KEY!, + { auth: { persistSession: false } }, + ); +} + // --------------------------------------------------------------------------- // Upload // --------------------------------------------------------------------------- @@ -45,15 +33,11 @@ export async function uploadFile( content: ArrayBuffer, contentType: string, ): Promise { - const client = getClient(); - await client.send( - new PutObjectCommand({ - Bucket: BUCKET, - Key: key, - Body: Buffer.from(content), - ContentType: contentType, - }), - ); + const { error } = await getClient() + .storage + .from(BUCKET) + .upload(key, content, { contentType, upsert: true }); + if (error) throw error; } // --------------------------------------------------------------------------- @@ -63,13 +47,12 @@ export async function uploadFile( export async function downloadFile(key: string): Promise { if (!storageEnabled) return null; try { - const client = getClient(); - const response = await client.send( - new GetObjectCommand({ Bucket: BUCKET, Key: key }), - ); - if (!response.Body) return null; - const bytes = await response.Body.transformToByteArray(); - return bytes.buffer as ArrayBuffer; + const { data, error } = await getClient() + .storage + .from(BUCKET) + .download(key); + if (error || !data) return null; + return await data.arrayBuffer(); } catch { return null; } @@ -81,12 +64,11 @@ export async function downloadFile(key: string): Promise { export async function deleteFile(key: string): Promise { if (!storageEnabled) return; - const client = getClient(); - await client.send(new DeleteObjectCommand({ Bucket: BUCKET, Key: key })); + await getClient().storage.from(BUCKET).remove([key]); } // --------------------------------------------------------------------------- -// Signed URL (pre-signed for temporary direct access) +// Signed URL (temporary direct access) // --------------------------------------------------------------------------- export async function getSignedUrl( @@ -96,20 +78,13 @@ export async function getSignedUrl( ): Promise { if (!storageEnabled) return null; try { - const client = getClient(); - // Override the response Content-Disposition so the browser uses this - // filename on download, instead of the last path segment of the R2 key - // (which includes the document UUID). The `download` attribute on - // is ignored for cross-origin URLs, so we have to set it server-side. - const responseContentDisposition = downloadFilename - ? buildContentDisposition("attachment", downloadFilename) - : undefined; - const command = new GetObjectCommand({ - Bucket: BUCKET, - Key: key, - ResponseContentDisposition: responseContentDisposition, - }); - return await awsGetSignedUrl(client, command, { expiresIn }); + const options = downloadFilename ? { download: downloadFilename } : undefined; + const { data, error } = await getClient() + .storage + .from(BUCKET) + .createSignedUrl(key, expiresIn, options); + if (error || !data) return null; + return data.signedUrl; } catch { return null; } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5782999f..d1544e19 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1890,7 +1890,6 @@ "version": "1.9.2", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2617,7 +2616,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2640,7 +2638,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2663,7 +2660,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2680,7 +2676,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2697,7 +2692,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2714,7 +2708,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2731,7 +2724,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2748,7 +2740,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2765,7 +2756,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2782,7 +2772,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2799,7 +2788,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2816,7 +2804,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2833,7 +2820,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2856,7 +2842,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2879,7 +2864,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2902,7 +2886,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2925,7 +2908,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2948,7 +2930,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2971,7 +2952,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2994,7 +2974,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -3017,7 +2996,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -3037,7 +3015,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -3057,7 +3034,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -3077,7 +3053,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [