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.
- package/CHANGELOG.md +296 -1
- package/FIXED_RLS_ERROR.md +146 -0
- package/QUICK_TEST.md +127 -0
- package/README.md +87 -10
- package/STORAGE_MODULE_SUMMARY.md +228 -0
- package/TEST_INSTRUCTIONS.md +122 -0
- package/dist/api/types/types.d.ts +2 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useFileUpload.d.ts +18 -3
- package/dist/hooks/useFileUpload.js +19 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/utils/fileStorage.d.ts +8 -4
- package/dist/utils/fileStorage.js +8 -4
- package/docs/AUTH_TOKEN_USAGE.md +290 -0
- package/docs/BACKEND_SERVICE_URL.md +334 -0
- package/docs/FRONTEND_STORAGE_GUIDE.md +529 -0
- package/docs/READY_TO_USE_COMPONENT.tsx +395 -0
- package/docs/STORAGE_MODULE.md +490 -0
- package/docs/STORAGE_QUICK_START.md +76 -0
- package/package.json +1 -1
- package/scripts/supabase-storage-setup.sql +223 -0
- package/src/api/types/types.ts +2 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useFileUpload.ts +129 -0
- package/src/index.ts +1 -0
- package/src/utils/fileStorage.ts +367 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { supabaseClient } from './supabase';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Типы для работы с файловым хранилищем
|
|
5
|
+
*/
|
|
6
|
+
export interface UploadFileOptions {
|
|
7
|
+
file: File | Blob;
|
|
8
|
+
fileName: string;
|
|
9
|
+
bucket: string;
|
|
10
|
+
folder?: string; // Папка внутри бакета (например, 'documents', 'images/avatars'). Создается автоматически, если не существует
|
|
11
|
+
contentType?: string;
|
|
12
|
+
cacheControl?: string;
|
|
13
|
+
upsert?: boolean;
|
|
14
|
+
authToken?: string; // JWT token для авторизации
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface UploadFileResult {
|
|
18
|
+
success: boolean;
|
|
19
|
+
publicUrl?: string;
|
|
20
|
+
signedUrl?: string;
|
|
21
|
+
path?: string;
|
|
22
|
+
error?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface GetFileUrlOptions {
|
|
26
|
+
path: string;
|
|
27
|
+
bucket: string;
|
|
28
|
+
expiresIn?: number; // в секундах
|
|
29
|
+
authToken?: string; // JWT token для авторизации
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Названия бакетов по умолчанию
|
|
34
|
+
*/
|
|
35
|
+
export const DEFAULT_BUCKET = 'user-files';
|
|
36
|
+
export const DOCUMENTS_BUCKET = 'documents';
|
|
37
|
+
export const IMAGES_BUCKET = 'images';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Загружает файл в Supabase Storage
|
|
41
|
+
* Файл сохраняется по пути: {folder}/{fileName} или {fileName}
|
|
42
|
+
*
|
|
43
|
+
* Папки создаются автоматически при загрузке файла, если их не существует.
|
|
44
|
+
* Можно указывать вложенные папки через слэш: 'images/avatars/2024'
|
|
45
|
+
*
|
|
46
|
+
* @param options - параметры загрузки файла
|
|
47
|
+
* @param options.folder - опциональная папка внутри бакета (например, 'documents', 'images/avatars')
|
|
48
|
+
* @returns результат загрузки с ссылкой на файл
|
|
49
|
+
*/
|
|
50
|
+
export const uploadFile = async (options: UploadFileOptions): Promise<UploadFileResult> => {
|
|
51
|
+
const { file, fileName, bucket, folder, contentType, cacheControl = '3600', upsert = false, authToken } = options;
|
|
52
|
+
|
|
53
|
+
if (!supabaseClient) {
|
|
54
|
+
return {
|
|
55
|
+
success: false,
|
|
56
|
+
error: 'Supabase client is not initialized',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// Если передан authToken, создаем клиент с токеном
|
|
62
|
+
let client = supabaseClient;
|
|
63
|
+
if (authToken) {
|
|
64
|
+
const { createClient } = await import('@supabase/supabase-js');
|
|
65
|
+
client = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_PUBLIC_KEY!, {
|
|
66
|
+
global: {
|
|
67
|
+
headers: {
|
|
68
|
+
Authorization: `Bearer ${authToken}`,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Формируем путь к файлу: folder/fileName или fileName
|
|
75
|
+
const filePath = folder ? `${folder}/${fileName}` : fileName;
|
|
76
|
+
|
|
77
|
+
const { data, error } = await client.storage.from(bucket).upload(filePath, file, {
|
|
78
|
+
contentType,
|
|
79
|
+
cacheControl,
|
|
80
|
+
upsert,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (error) {
|
|
84
|
+
console.error('Error uploading file:', error);
|
|
85
|
+
return {
|
|
86
|
+
success: false,
|
|
87
|
+
error: error.message,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Получаем публичный URL
|
|
92
|
+
const { data: urlData } = client.storage.from(bucket).getPublicUrl(data.path);
|
|
93
|
+
|
|
94
|
+
// Получаем подписанный URL (действителен 1 час по умолчанию)
|
|
95
|
+
const { data: signedUrlData, error: signedUrlError } = await client.storage
|
|
96
|
+
.from(bucket)
|
|
97
|
+
.createSignedUrl(data.path, 3600);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
success: true,
|
|
101
|
+
publicUrl: urlData.publicUrl,
|
|
102
|
+
signedUrl: signedUrlError ? undefined : signedUrlData.signedUrl,
|
|
103
|
+
path: data.path,
|
|
104
|
+
};
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('Unexpected error uploading file:', error);
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Получает подписанный URL для доступа к файлу
|
|
116
|
+
*
|
|
117
|
+
* @param options - параметры получения URL
|
|
118
|
+
* @returns подписанный URL или null при ошибке
|
|
119
|
+
*/
|
|
120
|
+
export const getSignedUrl = async (options: GetFileUrlOptions): Promise<string | null> => {
|
|
121
|
+
const { path, bucket = DEFAULT_BUCKET, expiresIn = 3600, authToken } = options;
|
|
122
|
+
|
|
123
|
+
if (!supabaseClient) {
|
|
124
|
+
console.error('Supabase client is not initialized');
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
// Если передан authToken, создаем клиент с токеном
|
|
130
|
+
let client = supabaseClient;
|
|
131
|
+
if (authToken) {
|
|
132
|
+
const { createClient } = await import('@supabase/supabase-js');
|
|
133
|
+
client = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_PUBLIC_KEY!, {
|
|
134
|
+
global: {
|
|
135
|
+
headers: {
|
|
136
|
+
Authorization: `Bearer ${authToken}`,
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const { data, error } = await client.storage.from(bucket).createSignedUrl(path, expiresIn);
|
|
143
|
+
|
|
144
|
+
if (error) {
|
|
145
|
+
console.error('Error creating signed URL:', error);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return data.signedUrl;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error('Unexpected error creating signed URL:', error);
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Получает публичный URL для файла
|
|
158
|
+
*
|
|
159
|
+
* Для ПРИВАТНЫХ бакетов:
|
|
160
|
+
* - URL постоянный (не истекает)
|
|
161
|
+
* - Требует Authorization header с service role key для доступа
|
|
162
|
+
* - Используется на backend для суперадмина
|
|
163
|
+
*
|
|
164
|
+
* Для ПУБЛИЧНЫХ бакетов:
|
|
165
|
+
* - URL доступен всем без аутентификации
|
|
166
|
+
*
|
|
167
|
+
* @example Backend usage for private buckets:
|
|
168
|
+
* ```typescript
|
|
169
|
+
* const url = getPublicUrl(filePath, bucket);
|
|
170
|
+
*
|
|
171
|
+
* // Access with service role key:
|
|
172
|
+
* fetch(url, {
|
|
173
|
+
* headers: {
|
|
174
|
+
* 'Authorization': `Bearer ${SUPABASE_SERVICE_ROLE_KEY}`
|
|
175
|
+
* }
|
|
176
|
+
* })
|
|
177
|
+
* ```
|
|
178
|
+
*
|
|
179
|
+
* @param path - путь к файлу
|
|
180
|
+
* @param bucket - название бакета
|
|
181
|
+
* @returns постоянный URL
|
|
182
|
+
*/
|
|
183
|
+
export const getPublicUrl = (path: string, bucket: string = DEFAULT_BUCKET): string | null => {
|
|
184
|
+
if (!supabaseClient) {
|
|
185
|
+
console.error('Supabase client is not initialized');
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const { data } = supabaseClient.storage.from(bucket).getPublicUrl(path);
|
|
190
|
+
return data.publicUrl;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Удаляет файл из хранилища
|
|
195
|
+
*
|
|
196
|
+
* @param path - путь к файлу
|
|
197
|
+
* @param bucket - название бакета
|
|
198
|
+
* @param authToken - JWT token для авторизации
|
|
199
|
+
* @returns true при успешном удалении
|
|
200
|
+
*/
|
|
201
|
+
export const deleteFile = async (
|
|
202
|
+
path: string,
|
|
203
|
+
bucket: string = DEFAULT_BUCKET,
|
|
204
|
+
authToken?: string,
|
|
205
|
+
): Promise<boolean> => {
|
|
206
|
+
if (!supabaseClient) {
|
|
207
|
+
console.error('Supabase client is not initialized');
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
let client = supabaseClient;
|
|
213
|
+
if (authToken) {
|
|
214
|
+
const { createClient } = await import('@supabase/supabase-js');
|
|
215
|
+
client = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_PUBLIC_KEY!, {
|
|
216
|
+
global: {
|
|
217
|
+
headers: {
|
|
218
|
+
Authorization: `Bearer ${authToken}`,
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const { error } = await client.storage.from(bucket).remove([path]);
|
|
225
|
+
|
|
226
|
+
if (error) {
|
|
227
|
+
console.error('Error deleting file:', error);
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return true;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
console.error('Unexpected error deleting file:', error);
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Удаляет несколько файлов из хранилища
|
|
240
|
+
*
|
|
241
|
+
* @param paths - массив путей к файлам
|
|
242
|
+
* @param bucket - название бакета
|
|
243
|
+
* @param authToken - JWT token для авторизации
|
|
244
|
+
* @returns true при успешном удалении всех файлов
|
|
245
|
+
*/
|
|
246
|
+
export const deleteFiles = async (
|
|
247
|
+
paths: string[],
|
|
248
|
+
bucket: string = DEFAULT_BUCKET,
|
|
249
|
+
authToken?: string,
|
|
250
|
+
): Promise<boolean> => {
|
|
251
|
+
if (!supabaseClient) {
|
|
252
|
+
console.error('Supabase client is not initialized');
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
let client = supabaseClient;
|
|
258
|
+
if (authToken) {
|
|
259
|
+
const { createClient } = await import('@supabase/supabase-js');
|
|
260
|
+
client = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_PUBLIC_KEY!, {
|
|
261
|
+
global: {
|
|
262
|
+
headers: {
|
|
263
|
+
Authorization: `Bearer ${authToken}`,
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const { error } = await client.storage.from(bucket).remove(paths);
|
|
270
|
+
|
|
271
|
+
if (error) {
|
|
272
|
+
console.error('Error deleting files:', error);
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return true;
|
|
277
|
+
} catch (error) {
|
|
278
|
+
console.error('Unexpected error deleting files:', error);
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Получает список файлов пользователя
|
|
285
|
+
*
|
|
286
|
+
* @param userId - ID пользователя
|
|
287
|
+
* @param bucket - название бакета
|
|
288
|
+
* @param authToken - JWT token для авторизации
|
|
289
|
+
* @returns список файлов или пустой массив при ошибке
|
|
290
|
+
*/
|
|
291
|
+
export const listUserFiles = async (userId: string, bucket: string = DEFAULT_BUCKET, authToken?: string) => {
|
|
292
|
+
if (!supabaseClient) {
|
|
293
|
+
console.error('Supabase client is not initialized');
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
let client = supabaseClient;
|
|
299
|
+
if (authToken) {
|
|
300
|
+
const { createClient } = await import('@supabase/supabase-js');
|
|
301
|
+
client = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_PUBLIC_KEY!, {
|
|
302
|
+
global: {
|
|
303
|
+
headers: {
|
|
304
|
+
Authorization: `Bearer ${authToken}`,
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const { data, error } = await client.storage.from(bucket).list(userId);
|
|
311
|
+
|
|
312
|
+
if (error) {
|
|
313
|
+
console.error('Error listing files:', error);
|
|
314
|
+
return [];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return data || [];
|
|
318
|
+
} catch (error) {
|
|
319
|
+
console.error('Unexpected error listing files:', error);
|
|
320
|
+
return [];
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Скачивает файл из хранилища
|
|
326
|
+
*
|
|
327
|
+
* @param path - путь к файлу
|
|
328
|
+
* @param bucket - название бакета
|
|
329
|
+
* @param authToken - JWT token для авторизации
|
|
330
|
+
* @returns Blob файла или null при ошибке
|
|
331
|
+
*/
|
|
332
|
+
export const downloadFile = async (
|
|
333
|
+
path: string,
|
|
334
|
+
bucket: string = DEFAULT_BUCKET,
|
|
335
|
+
authToken?: string,
|
|
336
|
+
): Promise<Blob | null> => {
|
|
337
|
+
if (!supabaseClient) {
|
|
338
|
+
console.error('Supabase client is not initialized');
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
let client = supabaseClient;
|
|
344
|
+
if (authToken) {
|
|
345
|
+
const { createClient } = await import('@supabase/supabase-js');
|
|
346
|
+
client = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_PUBLIC_KEY!, {
|
|
347
|
+
global: {
|
|
348
|
+
headers: {
|
|
349
|
+
Authorization: `Bearer ${authToken}`,
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const { data, error } = await client.storage.from(bucket).download(path);
|
|
356
|
+
|
|
357
|
+
if (error) {
|
|
358
|
+
console.error('Error downloading file:', error);
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return data;
|
|
363
|
+
} catch (error) {
|
|
364
|
+
console.error('Unexpected error downloading file:', error);
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
};
|