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 6b4f7492..f0bfae65 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 decbac1f..366db1b6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1889,7 +1889,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": { @@ -2616,7 +2615,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2639,7 +2637,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2662,7 +2659,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2679,7 +2675,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2696,7 +2691,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2713,7 +2707,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2730,7 +2723,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2747,7 +2739,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2764,7 +2755,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2781,7 +2771,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2798,7 +2787,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2815,7 +2803,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2832,7 +2819,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2855,7 +2841,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2878,7 +2863,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2901,7 +2885,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2924,7 +2907,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2947,7 +2929,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2970,7 +2951,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2993,7 +2973,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -3016,7 +2995,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -3036,7 +3014,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -3056,7 +3033,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -3076,7 +3052,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [