squarefi-bff-api-module 1.30.9 → 1.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,490 @@
1
+ # Supabase Storage Module
2
+
3
+ The module provides functions for working with Supabase file storage with automatic user-level access protection.
4
+
5
+ ## Features
6
+
7
+ - ✅ Upload files to protected storage
8
+ - ✅ Get signed URLs for secure access
9
+ - ✅ Automatic file organization by user
10
+ - ✅ Security policies: access only for owner or superadmin
11
+ - ✅ Support for multiple buckets (user-files, documents, images)
12
+ - ✅ File and folder deletion
13
+ - ✅ File download
14
+
15
+ ## Installation and Setup
16
+
17
+ ### 1. Environment Variables Setup
18
+
19
+ ```bash
20
+ # Copy env.example to .env and fill in your values
21
+ # See env.example in repository root for all available variables
22
+
23
+ SUPABASE_URL=your-supabase-url
24
+ SUPABASE_PUBLIC_KEY=your-supabase-anon-key
25
+ ```
26
+
27
+ ### 2. Supabase Storage Setup
28
+
29
+ Execute the SQL script `scripts/supabase-storage-setup.sql` in your Supabase database via SQL Editor:
30
+
31
+ 1. Open Supabase Dashboard
32
+ 2. Go to SQL Editor
33
+ 3. Copy the content of `supabase-storage-setup.sql`
34
+ 4. Execute the script
35
+
36
+ **Important:** Modify the `is_super_admin()` function in the script according to your user and role schema.
37
+
38
+ ## Usage
39
+
40
+ ### Import
41
+
42
+ ```typescript
43
+ import {
44
+ uploadFile,
45
+ getSignedUrl,
46
+ getPublicUrl,
47
+ deleteFile,
48
+ deleteFiles,
49
+ listUserFiles,
50
+ downloadFile,
51
+ DEFAULT_BUCKET,
52
+ DOCUMENTS_BUCKET,
53
+ IMAGES_BUCKET,
54
+ } from '@your-package/bff-api-module';
55
+ ```
56
+
57
+ ### Upload File
58
+
59
+ ```typescript
60
+ const handleFileUpload = async (file: File, userId: string) => {
61
+ const result = await uploadFile({
62
+ file: file,
63
+ fileName: file.name,
64
+ userId: userId,
65
+ bucket: DEFAULT_BUCKET, // or DOCUMENTS_BUCKET, IMAGES_BUCKET
66
+ contentType: file.type,
67
+ upsert: false, // if true, will overwrite existing file
68
+ });
69
+
70
+ if (result.success) {
71
+ console.log('File uploaded:', result.path);
72
+ console.log('Public URL:', result.publicUrl);
73
+ console.log('Signed URL:', result.signedUrl);
74
+
75
+ // Save result.path in your database for future access
76
+ return result.path;
77
+ } else {
78
+ console.error('Upload error:', result.error);
79
+ return null;
80
+ }
81
+ };
82
+ ```
83
+
84
+ ### Get Signed URL
85
+
86
+ ```typescript
87
+ // Signed URL for secure file access (temporary, expires)
88
+ const getFileUrl = async (filePath: string) => {
89
+ const signedUrl = await getSignedUrl({
90
+ path: filePath,
91
+ bucket: DEFAULT_BUCKET,
92
+ expiresIn: 3600, // URL valid for 1 hour (in seconds)
93
+ });
94
+
95
+ if (signedUrl) {
96
+ // Use URL to access the file
97
+ window.open(signedUrl, '_blank');
98
+ }
99
+ };
100
+ ```
101
+
102
+ ### Get Public URL (for Superadmin Backend Access)
103
+
104
+ For **private buckets**, `getPublicUrl()` returns a permanent URL that requires authentication:
105
+
106
+ ```typescript
107
+ import { getPublicUrl, DEFAULT_BUCKET } from '@your-package/bff-api-module';
108
+
109
+ // Get permanent URL (works on frontend or backend)
110
+ const publicUrl = getPublicUrl(filePath, DEFAULT_BUCKET);
111
+
112
+ console.log('Public URL:', publicUrl);
113
+ // Example: https://xxx.supabase.co/storage/v1/object/public/user-files/userId/file.pdf
114
+
115
+ // On BACKEND, access with service role key:
116
+ fetch(publicUrl, {
117
+ headers: {
118
+ 'Authorization': `Bearer ${process.env.SUPABASE_SERVICE_ROLE_KEY}`
119
+ }
120
+ })
121
+ ```
122
+
123
+ **Important**:
124
+ - For private buckets, this URL requires service role key in Authorization header
125
+ - Never expose service role key on frontend
126
+ - For regular users, use `getSignedUrl()` instead
127
+
128
+ ### Download File
129
+
130
+ ```typescript
131
+ const downloadUserFile = async (filePath: string) => {
132
+ const blob = await downloadFile(filePath, DEFAULT_BUCKET);
133
+
134
+ if (blob) {
135
+ // Create download link
136
+ const url = URL.createObjectURL(blob);
137
+ const a = document.createElement('a');
138
+ a.href = url;
139
+ a.download = filePath.split('/').pop() || 'download';
140
+ a.click();
141
+ URL.revokeObjectURL(url);
142
+ }
143
+ };
144
+ ```
145
+
146
+ ### List User Files
147
+
148
+ ```typescript
149
+ const getUserFiles = async (userId: string) => {
150
+ const files = await listUserFiles(userId, DEFAULT_BUCKET);
151
+
152
+ console.log('User files:', files);
153
+
154
+ // Each file contains:
155
+ // - name: file name
156
+ // - id: file ID
157
+ // - updated_at: update date
158
+ // - created_at: creation date
159
+ // - last_accessed_at: last access date
160
+ // - metadata: file metadata
161
+
162
+ return files;
163
+ };
164
+ ```
165
+
166
+ ### Delete File
167
+
168
+ ```typescript
169
+ const removeFile = async (filePath: string) => {
170
+ const success = await deleteFile(filePath, DEFAULT_BUCKET);
171
+
172
+ if (success) {
173
+ console.log('File deleted');
174
+ } else {
175
+ console.error('Error deleting file');
176
+ }
177
+ };
178
+ ```
179
+
180
+ ### Delete Multiple Files
181
+
182
+ ```typescript
183
+ const removeMultipleFiles = async (filePaths: string[]) => {
184
+ const success = await deleteFiles(filePaths, DEFAULT_BUCKET);
185
+
186
+ if (success) {
187
+ console.log('Files deleted');
188
+ } else {
189
+ console.error('Error deleting files');
190
+ }
191
+ };
192
+ ```
193
+
194
+ ## React Usage Examples
195
+
196
+ ### File Upload Component
197
+
198
+ ```typescript
199
+ import React, { useState } from 'react';
200
+ import { uploadFile, DEFAULT_BUCKET } from '@your-package/bff-api-module';
201
+
202
+ interface FileUploaderProps {
203
+ userId: string;
204
+ onUploadSuccess: (path: string) => void;
205
+ }
206
+
207
+ export const FileUploader: React.FC<FileUploaderProps> = ({ userId, onUploadSuccess }) => {
208
+ const [uploading, setUploading] = useState(false);
209
+ const [error, setError] = useState<string | null>(null);
210
+
211
+ const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
212
+ const file = e.target.files?.[0];
213
+ if (!file) return;
214
+
215
+ setUploading(true);
216
+ setError(null);
217
+
218
+ try {
219
+ const result = await uploadFile({
220
+ file,
221
+ fileName: `${Date.now()}-${file.name}`,
222
+ userId,
223
+ bucket: DEFAULT_BUCKET,
224
+ contentType: file.type,
225
+ });
226
+
227
+ if (result.success && result.path) {
228
+ onUploadSuccess(result.path);
229
+ } else {
230
+ setError(result.error || 'Upload error');
231
+ }
232
+ } catch (err) {
233
+ setError('Unexpected error');
234
+ } finally {
235
+ setUploading(false);
236
+ }
237
+ };
238
+
239
+ return (
240
+ <div>
241
+ <input
242
+ type="file"
243
+ onChange={handleFileChange}
244
+ disabled={uploading}
245
+ />
246
+ {uploading && <p>Uploading...</p>}
247
+ {error && <p style={{ color: 'red' }}>{error}</p>}
248
+ </div>
249
+ );
250
+ };
251
+ ```
252
+
253
+ ### File List Component
254
+
255
+ ```typescript
256
+ import React, { useEffect, useState } from 'react';
257
+ import {
258
+ listUserFiles,
259
+ getSignedUrl,
260
+ deleteFile,
261
+ DEFAULT_BUCKET,
262
+ } from '@your-package/bff-api-module';
263
+
264
+ interface FileListProps {
265
+ userId: string;
266
+ }
267
+
268
+ export const FileList: React.FC<FileListProps> = ({ userId }) => {
269
+ const [files, setFiles] = useState<any[]>([]);
270
+ const [loading, setLoading] = useState(true);
271
+
272
+ useEffect(() => {
273
+ loadFiles();
274
+ }, [userId]);
275
+
276
+ const loadFiles = async () => {
277
+ setLoading(true);
278
+ const userFiles = await listUserFiles(userId, DEFAULT_BUCKET);
279
+ setFiles(userFiles);
280
+ setLoading(false);
281
+ };
282
+
283
+ const handleDownload = async (fileName: string) => {
284
+ const filePath = `${userId}/${fileName}`;
285
+ const signedUrl = await getSignedUrl({
286
+ path: filePath,
287
+ bucket: DEFAULT_BUCKET,
288
+ expiresIn: 3600,
289
+ });
290
+
291
+ if (signedUrl) {
292
+ window.open(signedUrl, '_blank');
293
+ }
294
+ };
295
+
296
+ const handleDelete = async (fileName: string) => {
297
+ const filePath = `${userId}/${fileName}`;
298
+ const success = await deleteFile(filePath, DEFAULT_BUCKET);
299
+
300
+ if (success) {
301
+ loadFiles(); // Reload list
302
+ }
303
+ };
304
+
305
+ if (loading) return <div>Loading...</div>;
306
+
307
+ return (
308
+ <div>
309
+ <h3>My Files</h3>
310
+ <ul>
311
+ {files.map((file) => (
312
+ <li key={file.id}>
313
+ <span>{file.name}</span>
314
+ <button onClick={() => handleDownload(file.name)}>Download</button>
315
+ <button onClick={() => handleDelete(file.name)}>Delete</button>
316
+ </li>
317
+ ))}
318
+ </ul>
319
+ </div>
320
+ );
321
+ };
322
+ ```
323
+
324
+ ## Storage Structure
325
+
326
+ Files are organized by the following structure:
327
+
328
+ ```
329
+ bucket/
330
+ └── {userId}/
331
+ ├── file1.pdf
332
+ ├── file2.jpg
333
+ └── file3.docx
334
+ ```
335
+
336
+ Where `{userId}` is the UUID of the authenticated user from Supabase Auth.
337
+
338
+ ## Security
339
+
340
+ ### Row Level Security (RLS)
341
+
342
+ All files are protected by RLS policies at Supabase level:
343
+
344
+ 1. **Upload**: Users can only upload files to their own folder (`{userId}/`)
345
+ 2. **View**: Users can only view their own files (or all if superadmin)
346
+ 3. **Update**: Users can only update their own files (or all if superadmin)
347
+ 4. **Delete**: Users can only delete their own files (or all if superadmin)
348
+
349
+ ### URL Types
350
+
351
+ **1. Signed URLs** (for regular users):
352
+ - ✅ Temporary access with expiration time (default 1 hour)
353
+ - ✅ Work independently of RLS policies
354
+ - ✅ Safe to share with end users
355
+ - ✅ No authentication required
356
+
357
+ **2. Public URLs** (for superadmin backend, private buckets):
358
+ - ✅ Permanent URL, never expires
359
+ - ✅ Requires Supabase service role key in Authorization header
360
+ - ✅ Bypasses RLS policies (service role has admin access)
361
+ - ⚠️ **NEVER expose service role key on frontend**
362
+ - ✅ Use only on secure backend
363
+
364
+ **Example public URL usage on backend:**
365
+ ```javascript
366
+ // Get URL (can be done anywhere)
367
+ const publicUrl = getPublicUrl(filePath, bucket);
368
+
369
+ // Backend only - access with service key!
370
+ const response = await fetch(publicUrl, {
371
+ headers: {
372
+ 'Authorization': `Bearer ${process.env.SUPABASE_SERVICE_ROLE_KEY}`
373
+ }
374
+ });
375
+ ```
376
+
377
+ ## Buckets
378
+
379
+ Three pre-configured buckets are available:
380
+
381
+ - `user-files` (DEFAULT_BUCKET) - for general user files
382
+ - `documents` (DOCUMENTS_BUCKET) - for documents
383
+ - `images` (IMAGES_BUCKET) - for images
384
+
385
+ All buckets are configured as private (`public: false`) with identical security policies.
386
+
387
+ ## Limitations
388
+
389
+ By default, Supabase has the following limitations:
390
+
391
+ - Maximum file size: **50 MB** (can be increased in project settings)
392
+ - Total storage limit depends on your pricing plan
393
+
394
+ ## Error Handling
395
+
396
+ All functions return results with error handling:
397
+
398
+ ```typescript
399
+ const result = await uploadFile({...});
400
+
401
+ if (result.success) {
402
+ // Successful upload
403
+ console.log(result.path, result.signedUrl);
404
+ } else {
405
+ // Handle error
406
+ console.error(result.error);
407
+ }
408
+ ```
409
+
410
+ ## Data Types
411
+
412
+ ### UploadFileOptions
413
+
414
+ ```typescript
415
+ interface UploadFileOptions {
416
+ file: File | Blob; // File to upload
417
+ fileName: string; // File name
418
+ bucket?: string; // Bucket (default DEFAULT_BUCKET)
419
+ userId: string; // User ID (required)
420
+ contentType?: string; // File MIME type
421
+ cacheControl?: string; // Cache-Control header (default '3600')
422
+ upsert?: boolean; // Overwrite existing file (default false)
423
+ }
424
+ ```
425
+
426
+ ### UploadFileResult
427
+
428
+ ```typescript
429
+ interface UploadFileResult {
430
+ success: boolean; // Operation status
431
+ publicUrl?: string; // Public URL (use with caution)
432
+ signedUrl?: string; // Signed URL (recommended)
433
+ path?: string; // File path in storage
434
+ error?: string; // Error message
435
+ }
436
+ ```
437
+
438
+ ### GetFileUrlOptions
439
+
440
+ ```typescript
441
+ interface GetFileUrlOptions {
442
+ path: string; // File path
443
+ bucket?: string; // Bucket (default DEFAULT_BUCKET)
444
+ expiresIn?: number; // URL expiration in seconds (default 3600)
445
+ }
446
+ ```
447
+
448
+ ## FAQ
449
+
450
+ ### How to modify superadmin check logic?
451
+
452
+ Edit the `is_super_admin()` function in `supabase-storage-setup.sql` before execution:
453
+
454
+ ```sql
455
+ CREATE OR REPLACE FUNCTION public.is_super_admin(user_id uuid)
456
+ RETURNS boolean AS $$
457
+ BEGIN
458
+ -- Your check logic
459
+ RETURN EXISTS (
460
+ SELECT 1
461
+ FROM public.your_users_table
462
+ WHERE id = user_id
463
+ AND your_role_field = 'admin'
464
+ );
465
+ END;
466
+ $$ LANGUAGE plpgsql SECURITY DEFINER;
467
+ ```
468
+
469
+ ### How to create a public bucket?
470
+
471
+ If you need a public bucket (without RLS), create it with `public: true`:
472
+
473
+ ```sql
474
+ INSERT INTO storage.buckets (id, name, public)
475
+ VALUES ('public-images', 'public-images', true);
476
+ ```
477
+
478
+ ### How to increase maximum file size?
479
+
480
+ 1. Open Supabase Dashboard
481
+ 2. Go to Settings → Storage
482
+ 3. Change `File Size Limit`
483
+
484
+ ### Should I save file paths in the database?
485
+
486
+ Yes, it's recommended to save the `path` from upload result in your database for future file access.
487
+
488
+ ## Support
489
+
490
+ If you have questions or issues, please create an issue in the project repository.
@@ -0,0 +1,76 @@
1
+ # Storage Module - Quick Start 🚀
2
+
3
+ ## 1️⃣ Setup (one time)
4
+
5
+ ```bash
6
+ # Copy env.example to .env and fill in your values
7
+ # See env.example in repository root for all available variables
8
+
9
+ # .env
10
+ SUPABASE_URL=https://xxx.supabase.co
11
+ SUPABASE_PUBLIC_KEY=eyJxxx...
12
+ ```
13
+
14
+ Execute SQL in Supabase Dashboard:
15
+ ```bash
16
+ scripts/supabase-storage-setup.sql
17
+ ```
18
+
19
+ ## 2️⃣ Import
20
+
21
+ ```tsx
22
+ import {
23
+ useFileUpload,
24
+ useUserFiles
25
+ } from 'squarefi-bff-api-module';
26
+ ```
27
+
28
+ ## 3️⃣ Usage
29
+
30
+ ### Upload File
31
+
32
+ ```tsx
33
+ function MyComponent({ userId }) {
34
+ const { upload, uploading } = useFileUpload({ userId });
35
+
36
+ return (
37
+ <input
38
+ type="file"
39
+ onChange={(e) => upload(e.target.files[0])}
40
+ disabled={uploading}
41
+ />
42
+ );
43
+ }
44
+ ```
45
+
46
+ ### File List
47
+
48
+ ```tsx
49
+ function FileList({ userId }) {
50
+ const { files, deleteOne } = useUserFiles({
51
+ userId,
52
+ autoLoad: true,
53
+ autoGenerateUrls: true
54
+ });
55
+
56
+ return (
57
+ <ul>
58
+ {files.map(file => (
59
+ <li key={file.id}>
60
+ <a href={file.signedUrl}>{file.name}</a>
61
+ <button onClick={() => deleteOne(file.name)}>🗑️</button>
62
+ </li>
63
+ ))}
64
+ </ul>
65
+ );
66
+ }
67
+ ```
68
+
69
+ ## 4️⃣ Done! ✅
70
+
71
+ All files automatically:
72
+ - ✅ Organized by `{userId}/filename`
73
+ - ✅ Protected by RLS policies
74
+ - ✅ Accessible only to owner
75
+
76
+ 📚 **More details**: [FRONTEND_STORAGE_GUIDE.md](./FRONTEND_STORAGE_GUIDE.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squarefi-bff-api-module",
3
- "version": "1.30.9",
3
+ "version": "1.31.0",
4
4
  "description": "Squarefi BFF API client module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",