squarefi-bff-api-module 1.30.10 → 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,290 @@
1
+ # Using Storage Module with Authentication
2
+
3
+ ## Problem
4
+ When you see error: **"new row violates row-level security policy"**
5
+
6
+ This means:
7
+ - ✅ RLS policies are working (good!)
8
+ - ❌ User is not authenticated
9
+
10
+ ## Solution: Pass authToken
11
+
12
+ All storage functions now accept an optional `authToken` parameter.
13
+
14
+ ## How to Get Auth Token
15
+
16
+ ### From Supabase Auth
17
+
18
+ ```typescript
19
+ import { supabaseClient } from 'squarefi-bff-api-module';
20
+
21
+ // After user login
22
+ const { data: { session } } = await supabaseClient.auth.getSession();
23
+ const authToken = session?.access_token;
24
+ ```
25
+
26
+ ### From Your Own Auth System
27
+
28
+ ```typescript
29
+ // From JWT token in localStorage/cookies
30
+ const authToken = localStorage.getItem('access_token');
31
+
32
+ // Or from your auth context
33
+ const { accessToken } = useAuth();
34
+ ```
35
+
36
+ ## Usage Examples
37
+
38
+ ### Direct API Usage
39
+
40
+ ```typescript
41
+ import { uploadFile } from 'squarefi-bff-api-module';
42
+
43
+ const authToken = 'your-jwt-token-here';
44
+
45
+ const result = await uploadFile({
46
+ file: myFile,
47
+ fileName: 'document.pdf',
48
+ userId: 'user-123',
49
+ authToken, // ← Передаем токен здесь!
50
+ });
51
+
52
+ if (result.success) {
53
+ console.log('Uploaded:', result.path);
54
+ }
55
+ ```
56
+
57
+ ### With React Hooks
58
+
59
+ ```typescript
60
+ import { useFileUpload, useUserFiles } from 'squarefi-bff-api-module';
61
+
62
+ function MyComponent() {
63
+ // Get token from your auth
64
+ const { accessToken } = useAuth(); // your auth hook
65
+
66
+ const { upload, uploading } = useFileUpload({
67
+ userId: 'user-123',
68
+ authToken: accessToken, // ← Передаем токен здесь!
69
+ });
70
+
71
+ const { files } = useUserFiles({
72
+ userId: 'user-123',
73
+ authToken: accessToken, // ← И здесь!
74
+ autoLoad: true,
75
+ });
76
+
77
+ return (
78
+ <div>
79
+ <input type="file" onChange={(e) => upload(e.target.files[0])} />
80
+ {files.map(f => <div key={f.id}>{f.name}</div>)}
81
+ </div>
82
+ );
83
+ }
84
+ ```
85
+
86
+ ### Complete Example with Supabase Auth
87
+
88
+ ```typescript
89
+ import React, { useEffect, useState } from 'react';
90
+ import { supabaseClient, useFileUpload } from 'squarefi-bff-api-module';
91
+
92
+ function AuthenticatedFileUpload() {
93
+ const [authToken, setAuthToken] = useState<string | null>(null);
94
+ const [userId, setUserId] = useState<string | null>(null);
95
+
96
+ useEffect(() => {
97
+ // Get current session
98
+ supabaseClient?.auth.getSession().then(({ data: { session } }) => {
99
+ setAuthToken(session?.access_token || null);
100
+ setUserId(session?.user?.id || null);
101
+ });
102
+
103
+ // Listen for auth changes
104
+ const { data: authListener } = supabaseClient?.auth.onAuthStateChange(
105
+ (_event, session) => {
106
+ setAuthToken(session?.access_token || null);
107
+ setUserId(session?.user?.id || null);
108
+ }
109
+ ) || {};
110
+
111
+ return () => {
112
+ authListener?.subscription.unsubscribe();
113
+ };
114
+ }, []);
115
+
116
+ const { upload, uploading, error } = useFileUpload({
117
+ userId: userId || '',
118
+ authToken: authToken || undefined,
119
+ onSuccess: (result) => {
120
+ console.log('File uploaded:', result.path);
121
+ },
122
+ });
123
+
124
+ if (!authToken) {
125
+ return <div>Please log in first</div>;
126
+ }
127
+
128
+ return (
129
+ <div>
130
+ <input
131
+ type="file"
132
+ onChange={(e) => upload(e.target.files?.[0])}
133
+ disabled={uploading}
134
+ />
135
+ {error && <p style={{color: 'red'}}>{error}</p>}
136
+ </div>
137
+ );
138
+ }
139
+ ```
140
+
141
+ ## All Functions Support authToken
142
+
143
+ ### Upload
144
+
145
+ ```typescript
146
+ uploadFile({ file, fileName, userId, authToken });
147
+ ```
148
+
149
+ ### Get Signed URL
150
+
151
+ ```typescript
152
+ getSignedUrl({ path, expiresIn, authToken });
153
+ ```
154
+
155
+ ### List Files
156
+
157
+ ```typescript
158
+ listUserFiles(userId, bucket, authToken);
159
+ ```
160
+
161
+ ### Delete File
162
+
163
+ ```typescript
164
+ deleteFile(filePath, bucket, authToken);
165
+ ```
166
+
167
+ ### Delete Multiple Files
168
+
169
+ ```typescript
170
+ deleteFiles(filePaths, bucket, authToken);
171
+ ```
172
+
173
+ ### Download File
174
+
175
+ ```typescript
176
+ downloadFile(filePath, bucket, authToken);
177
+ ```
178
+
179
+ ## Testing in Browser
180
+
181
+ Update `test-storage.html`:
182
+
183
+ ```javascript
184
+ // Add auth token input
185
+ <input type="text" id="authToken" placeholder="Auth Token (JWT)" style="margin: 10px 0;">
186
+
187
+ // In upload function:
188
+ const authToken = document.getElementById('authToken').value;
189
+
190
+ const { data, error } = await supabaseClient.storage
191
+ .from(DEFAULT_BUCKET)
192
+ .upload(filePath, file);
193
+
194
+ // Or create authenticated client:
195
+ if (authToken) {
196
+ const { createClient } = window.supabase;
197
+ const authenticatedClient = createClient(SUPABASE_URL, SUPABASE_PUBLIC_KEY, {
198
+ global: {
199
+ headers: {
200
+ Authorization: `Bearer ${authToken}`
201
+ }
202
+ }
203
+ });
204
+
205
+ // Use authenticatedClient for upload
206
+ }
207
+ ```
208
+
209
+ ## How to Get Test Token
210
+
211
+ ### Option 1: From Browser Console
212
+
213
+ ```javascript
214
+ // After logging in to your app
215
+ const session = await supabaseClient.auth.getSession();
216
+ console.log('Token:', session.data.session.access_token);
217
+ // Copy this token and use it in test
218
+ ```
219
+
220
+ ### Option 2: Create Test User
221
+
222
+ ```sql
223
+ -- In Supabase SQL Editor
224
+ -- Get user ID after signup
225
+ SELECT id FROM auth.users WHERE email = 'test@example.com';
226
+ ```
227
+
228
+ Then login via API and get token.
229
+
230
+ ## RLS Policies Explanation
231
+
232
+ The policies check `auth.uid()` which comes from the JWT token:
233
+
234
+ ```sql
235
+ -- User can upload to their own folder
236
+ CREATE POLICY "Users can upload" ON storage.objects
237
+ FOR INSERT TO authenticated
238
+ WITH CHECK (
239
+ bucket_id = 'user-files'
240
+ AND (storage.foldername(name))[1] = auth.uid()::text
241
+ );
242
+ ```
243
+
244
+ Without `authToken`, `auth.uid()` is NULL → policy fails.
245
+
246
+ With `authToken`, `auth.uid()` returns user ID from token → policy passes if user ID matches folder.
247
+
248
+ ## Common Issues
249
+
250
+ ### Still getting RLS error?
251
+
252
+ 1. ✅ Check token is valid (not expired)
253
+ 2. ✅ Check userId matches token's user ID
254
+ 3. ✅ Check file path starts with correct userId: `{userId}/filename`
255
+ 4. ✅ Verify SQL policies are correctly set up
256
+
257
+ ### Token expired?
258
+
259
+ ```typescript
260
+ // Refresh token
261
+ const { data, error } = await supabaseClient.auth.refreshSession();
262
+ if (data.session) {
263
+ const newToken = data.session.access_token;
264
+ // Use new token
265
+ }
266
+ ```
267
+
268
+ ### Wrong user ID?
269
+
270
+ ```typescript
271
+ // Get user ID from token
272
+ const { data: { user } } = await supabaseClient.auth.getUser();
273
+ console.log('User ID:', user?.id);
274
+ // Use this ID for file paths
275
+ ```
276
+
277
+ ## Summary
278
+
279
+ ✅ **Always pass `authToken` when using private buckets**
280
+ ✅ **Token must match the user ID in file path**
281
+ ✅ **Get token from Supabase Auth or your auth system**
282
+ ✅ **All functions support optional `authToken` parameter**
283
+
284
+ ## Related Files
285
+
286
+ - `src/utils/fileStorage.ts` - Core functions with authToken support
287
+ - `src/hooks/useFileUpload.ts` - React hook with authToken
288
+ - `src/hooks/useUserFiles.ts` - React hook with authToken
289
+
290
+
@@ -0,0 +1,334 @@
1
+ # Backend Public URL Usage
2
+
3
+ Guide for superadmin backend access to user files using permanent public URLs with service role key.
4
+
5
+ ## Overview
6
+
7
+ For backend applications that need permanent access to files (e.g., admin dashboards, automated processing), use **Public URLs** from `getPublicUrl()` with Supabase service role key.
8
+
9
+ ## Quick Example
10
+
11
+ ```typescript
12
+ import { getPublicUrl, DEFAULT_BUCKET } from 'squarefi-bff-api-module';
13
+
14
+ // 1. Get permanent URL (can be done on frontend or backend)
15
+ const filePath = 'user-123/document.pdf';
16
+ const publicUrl = getPublicUrl(filePath, DEFAULT_BUCKET);
17
+
18
+ console.log(publicUrl);
19
+ // Output: https://xxx.supabase.co/storage/v1/object/public/user-files/user-123/document.pdf
20
+
21
+ // 2. Access file on backend with service role key
22
+ const response = await fetch(publicUrl, {
23
+ headers: {
24
+ 'Authorization': `Bearer ${process.env.SUPABASE_SERVICE_ROLE_KEY}`
25
+ }
26
+ });
27
+
28
+ if (response.ok) {
29
+ const fileBlob = await response.blob();
30
+ // Process file...
31
+ }
32
+ ```
33
+
34
+ ## Key Differences
35
+
36
+ ### Signed URL vs Public URL
37
+
38
+ ```typescript
39
+ // Signed URL - expires after 1 hour, no auth required
40
+ const signedUrl = await getSignedUrl({
41
+ path: 'user-123/file.pdf',
42
+ expiresIn: 3600
43
+ });
44
+ // Use case: temporary link for end user
45
+
46
+ // Public URL - permanent, requires service role key
47
+ const publicUrl = getPublicUrl('user-123/file.pdf');
48
+ // Use case: backend processing, admin access
49
+ // Must use: fetch(publicUrl, { headers: { Authorization: Bearer SERVICE_KEY } })
50
+ ```
51
+
52
+ ## Security Requirements
53
+
54
+ ### ⚠️ Critical: Service Role Key
55
+
56
+ - **NEVER expose** `SUPABASE_SERVICE_ROLE_KEY` on frontend
57
+ - Store in secure environment variables
58
+ - Use only on backend/server
59
+ - This key **bypasses all RLS policies**
60
+
61
+ ### Environment Setup
62
+
63
+ ```bash
64
+ # Copy env.example to .env and fill in your values
65
+ # See env.example in repository root for all available variables
66
+
67
+ # Backend only - NEVER commit to git
68
+ SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
69
+ ```
70
+
71
+ ## Usage Examples
72
+
73
+ ### Node.js/Express Backend
74
+
75
+ ```javascript
76
+ import express from 'express';
77
+ import { getPublicUrl, DEFAULT_BUCKET } from 'squarefi-bff-api-module';
78
+
79
+ const app = express();
80
+
81
+ // Admin endpoint to access any user file
82
+ app.get('/admin/files/:userId/:fileName', async (req, res) => {
83
+ const { userId, fileName } = req.params;
84
+ const filePath = `${userId}/${fileName}`;
85
+
86
+ // Get permanent URL
87
+ const publicUrl = getPublicUrl(filePath, DEFAULT_BUCKET);
88
+
89
+ // Fetch with service key
90
+ const response = await fetch(publicUrl, {
91
+ headers: {
92
+ 'Authorization': `Bearer ${process.env.SUPABASE_SERVICE_ROLE_KEY}`
93
+ }
94
+ });
95
+
96
+ if (!response.ok) {
97
+ return res.status(404).json({ error: 'File not found' });
98
+ }
99
+
100
+ // Stream file to client
101
+ const buffer = await response.arrayBuffer();
102
+ res.setHeader('Content-Type', response.headers.get('content-type'));
103
+ res.send(Buffer.from(buffer));
104
+ });
105
+ ```
106
+
107
+ ### Next.js API Route
108
+
109
+ ```typescript
110
+ // pages/api/admin/file/[userId]/[fileName].ts
111
+ import { NextApiRequest, NextApiResponse } from 'next';
112
+ import { getPublicUrl, DEFAULT_BUCKET } from 'squarefi-bff-api-module';
113
+
114
+ export default async function handler(req: NextApiRequest, res: NextApiResponse) {
115
+ const { userId, fileName } = req.query;
116
+ const filePath = `${userId}/${fileName}`;
117
+
118
+ const publicUrl = getPublicUrl(filePath, DEFAULT_BUCKET);
119
+
120
+ const response = await fetch(publicUrl, {
121
+ headers: {
122
+ 'Authorization': `Bearer ${process.env.SUPABASE_SERVICE_ROLE_KEY}`
123
+ }
124
+ });
125
+
126
+ if (!response.ok) {
127
+ return res.status(404).json({ error: 'File not found' });
128
+ }
129
+
130
+ const buffer = await response.arrayBuffer();
131
+ res.setHeader('Content-Type', response.headers.get('content-type') || 'application/octet-stream');
132
+ res.send(Buffer.from(buffer));
133
+ }
134
+ ```
135
+
136
+ ### Background Job/Worker
137
+
138
+ ```typescript
139
+ import { getPublicUrl, DOCUMENTS_BUCKET } from 'squarefi-bff-api-module';
140
+
141
+ async function processUserDocument(userId: string, fileName: string) {
142
+ const filePath = `${userId}/${fileName}`;
143
+ const publicUrl = getPublicUrl(filePath, DOCUMENTS_BUCKET);
144
+
145
+ // Download file
146
+ const response = await fetch(publicUrl, {
147
+ headers: {
148
+ 'Authorization': `Bearer ${process.env.SUPABASE_SERVICE_ROLE_KEY}`
149
+ }
150
+ });
151
+
152
+ if (!response.ok) {
153
+ throw new Error(`Failed to download file: ${response.statusText}`);
154
+ }
155
+
156
+ const fileContent = await response.text();
157
+
158
+ // Process file...
159
+ console.log('Processing document for user:', userId);
160
+
161
+ // Your processing logic here
162
+ }
163
+
164
+ // Run in worker/cron job
165
+ await processUserDocument('user-123', 'invoice.pdf');
166
+ ```
167
+
168
+ ### Admin Dashboard
169
+
170
+ ```typescript
171
+ // Frontend gets the URL, backend fetches the file
172
+ import { getPublicUrl, DEFAULT_BUCKET } from 'squarefi-bff-api-module';
173
+
174
+ // Frontend component
175
+ function AdminFileViewer({ userId, fileName }) {
176
+ const [fileUrl, setFileUrl] = useState(null);
177
+
178
+ useEffect(() => {
179
+ // Get the URL (can be done on frontend)
180
+ const publicUrl = getPublicUrl(`${userId}/${fileName}`, DEFAULT_BUCKET);
181
+
182
+ // Call your backend to proxy the request
183
+ fetch('/api/admin/proxy-file', {
184
+ method: 'POST',
185
+ headers: { 'Content-Type': 'application/json' },
186
+ body: JSON.stringify({ publicUrl })
187
+ })
188
+ .then(res => res.blob())
189
+ .then(blob => {
190
+ const url = URL.createObjectURL(blob);
191
+ setFileUrl(url);
192
+ });
193
+ }, [userId, fileName]);
194
+
195
+ return fileUrl ? <img src={fileUrl} alt="File" /> : <p>Loading...</p>;
196
+ }
197
+
198
+ // Backend proxy endpoint (keeps service key secure)
199
+ app.post('/api/admin/proxy-file', async (req, res) => {
200
+ const { publicUrl } = req.body;
201
+
202
+ const response = await fetch(publicUrl, {
203
+ headers: {
204
+ 'Authorization': `Bearer ${process.env.SUPABASE_SERVICE_ROLE_KEY}`
205
+ }
206
+ });
207
+
208
+ const buffer = await response.arrayBuffer();
209
+ res.setHeader('Content-Type', response.headers.get('content-type'));
210
+ res.send(Buffer.from(buffer));
211
+ });
212
+ ```
213
+
214
+ ## Best Practices
215
+
216
+ ### ✅ DO
217
+
218
+ - Store service role key in secure environment variables
219
+ - Access public URLs with service key only on backend
220
+ - Validate user permissions before accessing files
221
+ - Log all admin file access for audit trail
222
+ - Use proper error handling
223
+
224
+ ### ❌ DON'T
225
+
226
+ - Never expose service role key on frontend
227
+ - Don't commit service key to version control
228
+ - Don't use service key on client-side (URL itself is fine, but don't add auth header)
229
+ - Don't skip audit logging
230
+ - Don't share service key across environments (use different keys for dev/prod)
231
+
232
+ ## Error Handling
233
+
234
+ ```typescript
235
+ async function downloadFileWithServiceKey(publicUrl: string) {
236
+ try {
237
+ const response = await fetch(publicUrl, {
238
+ headers: {
239
+ 'Authorization': `Bearer ${process.env.SUPABASE_SERVICE_ROLE_KEY}`
240
+ }
241
+ });
242
+
243
+ if (!response.ok) {
244
+ if (response.status === 404) {
245
+ throw new Error('File not found');
246
+ }
247
+ if (response.status === 401) {
248
+ throw new Error('Invalid service key');
249
+ }
250
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
251
+ }
252
+
253
+ return await response.blob();
254
+ } catch (error) {
255
+ console.error('Failed to download file:', error);
256
+ throw error;
257
+ }
258
+ }
259
+ ```
260
+
261
+ ## Audit Logging Example
262
+
263
+ ```typescript
264
+ import { getPublicUrl } from 'squarefi-bff-api-module';
265
+
266
+ async function auditedFileAccess(
267
+ adminUserId: string,
268
+ targetUserId: string,
269
+ fileName: string
270
+ ) {
271
+ // Log access
272
+ await logAudit({
273
+ action: 'FILE_ACCESS',
274
+ adminId: adminUserId,
275
+ targetUserId,
276
+ fileName,
277
+ timestamp: new Date(),
278
+ ip: req.ip
279
+ });
280
+
281
+ // Get and access file
282
+ const publicUrl = getPublicUrl(`${targetUserId}/${fileName}`);
283
+
284
+ const response = await fetch(publicUrl, {
285
+ headers: {
286
+ 'Authorization': `Bearer ${process.env.SUPABASE_SERVICE_ROLE_KEY}`
287
+ }
288
+ });
289
+
290
+ return response;
291
+ }
292
+ ```
293
+
294
+ ## Troubleshooting
295
+
296
+ ### File Not Found (404)
297
+
298
+ ```typescript
299
+ // Check if file path is correct
300
+ const correctPath = `${userId}/${fileName}`; // ✅
301
+ const wrongPath = `${fileName}`; // ❌
302
+ ```
303
+
304
+ ### Unauthorized (401)
305
+
306
+ - Verify `SUPABASE_SERVICE_ROLE_KEY` is set correctly
307
+ - Check if key is valid in Supabase Dashboard → Settings → API
308
+
309
+ ### CORS Issues
310
+
311
+ If accessing from browser (not recommended but possible):
312
+
313
+ ```typescript
314
+ // In Supabase Dashboard → Storage → Policies
315
+ // Make sure CORS is configured if needed
316
+ ```
317
+
318
+ ## Summary
319
+
320
+ | Aspect | Details |
321
+ |--------|---------|
322
+ | **Function** | `getPublicUrl(path, bucket)` |
323
+ | **Returns** | Permanent URL string |
324
+ | **Authentication** | Required: `Authorization: Bearer SERVICE_ROLE_KEY` (for private buckets) |
325
+ | **Expiration** | Never expires |
326
+ | **Use Case** | Backend/admin access to private buckets |
327
+ | **Security** | ⚠️ NEVER expose service key on frontend |
328
+
329
+ ## Related Documentation
330
+
331
+ - [Full Storage Module Docs](./STORAGE_MODULE.md)
332
+ - [Frontend Guide](./FRONTEND_STORAGE_GUIDE.md)
333
+ - [Quick Start](./STORAGE_QUICK_START.md)
334
+