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,223 @@
1
+ -- ============================================
2
+ -- Supabase Storage Setup
3
+ -- ============================================
4
+ -- Создание бакетов и настройка политик безопасности для файлового хранилища
5
+ -- Политики обеспечивают доступ к файлам только для их создателей и суперадминов
6
+
7
+ -- ============================================
8
+ -- 1. Создание бакетов
9
+ -- ============================================
10
+
11
+ -- Основной бакет для пользовательских файлов
12
+ INSERT INTO storage.buckets (id, name, public)
13
+ VALUES ('user-files', 'user-files', false)
14
+ ON CONFLICT (id) DO NOTHING;
15
+
16
+ -- Бакет для документов
17
+ INSERT INTO storage.buckets (id, name, public)
18
+ VALUES ('documents', 'documents', false)
19
+ ON CONFLICT (id) DO NOTHING;
20
+
21
+ -- Бакет для изображений
22
+ INSERT INTO storage.buckets (id, name, public)
23
+ VALUES ('images', 'images', false)
24
+ ON CONFLICT (id) DO NOTHING;
25
+
26
+ -- ============================================
27
+ -- 2. Функция для проверки прав суперадмина
28
+ -- ============================================
29
+ -- Создаем функцию для проверки, является ли пользователь суперадмином
30
+ -- Примечание: измените логику в зависимости от вашей схемы пользователей
31
+
32
+ CREATE OR REPLACE FUNCTION public.is_super_admin(user_id uuid)
33
+ RETURNS boolean AS $$
34
+ BEGIN
35
+ -- Пример: проверяем наличие записи в таблице profiles с ролью 'super_admin'
36
+ -- Измените эту логику в соответствии с вашей схемой данных
37
+ RETURN EXISTS (
38
+ SELECT 1
39
+ FROM public.profiles
40
+ WHERE id = user_id
41
+ AND role = 'super_admin'
42
+ );
43
+
44
+ -- Альтернативный вариант: проверка по email
45
+ -- RETURN EXISTS (
46
+ -- SELECT 1
47
+ -- FROM auth.users
48
+ -- WHERE id = user_id
49
+ -- AND email IN ('admin@example.com', 'superadmin@example.com')
50
+ -- );
51
+ END;
52
+ $$ LANGUAGE plpgsql SECURITY DEFINER;
53
+
54
+ -- ============================================
55
+ -- 3. Политики для бакета 'user-files'
56
+ -- ============================================
57
+
58
+ -- Пользователи могут загружать файлы только в свою папку
59
+ CREATE POLICY "Users can upload files to their own folder"
60
+ ON storage.objects
61
+ FOR INSERT
62
+ TO authenticated
63
+ WITH CHECK (
64
+ bucket_id = 'user-files'
65
+ AND (storage.foldername(name))[1] = auth.uid()::text
66
+ );
67
+
68
+ -- Пользователи могут просматривать свои файлы или если они суперадмины
69
+ CREATE POLICY "Users can view their own files or super admins can view all"
70
+ ON storage.objects
71
+ FOR SELECT
72
+ TO authenticated
73
+ USING (
74
+ bucket_id = 'user-files'
75
+ AND (
76
+ (storage.foldername(name))[1] = auth.uid()::text
77
+ OR public.is_super_admin(auth.uid())
78
+ )
79
+ );
80
+
81
+ -- Пользователи могут обновлять свои файлы или суперадмины могут обновлять любые
82
+ CREATE POLICY "Users can update their own files or super admins can update all"
83
+ ON storage.objects
84
+ FOR UPDATE
85
+ TO authenticated
86
+ USING (
87
+ bucket_id = 'user-files'
88
+ AND (
89
+ (storage.foldername(name))[1] = auth.uid()::text
90
+ OR public.is_super_admin(auth.uid())
91
+ )
92
+ );
93
+
94
+ -- Пользователи могут удалять свои файлы или суперадмины могут удалять любые
95
+ CREATE POLICY "Users can delete their own files or super admins can delete all"
96
+ ON storage.objects
97
+ FOR DELETE
98
+ TO authenticated
99
+ USING (
100
+ bucket_id = 'user-files'
101
+ AND (
102
+ (storage.foldername(name))[1] = auth.uid()::text
103
+ OR public.is_super_admin(auth.uid())
104
+ )
105
+ );
106
+
107
+ -- ============================================
108
+ -- 4. Политики для бакета 'documents'
109
+ -- ============================================
110
+
111
+ -- Аналогичные политики для бакета documents
112
+ CREATE POLICY "Users can upload documents to their own folder"
113
+ ON storage.objects
114
+ FOR INSERT
115
+ TO authenticated
116
+ WITH CHECK (
117
+ bucket_id = 'documents'
118
+ AND (storage.foldername(name))[1] = auth.uid()::text
119
+ );
120
+
121
+ CREATE POLICY "Users can view their own documents or super admins can view all"
122
+ ON storage.objects
123
+ FOR SELECT
124
+ TO authenticated
125
+ USING (
126
+ bucket_id = 'documents'
127
+ AND (
128
+ (storage.foldername(name))[1] = auth.uid()::text
129
+ OR public.is_super_admin(auth.uid())
130
+ )
131
+ );
132
+
133
+ CREATE POLICY "Users can update their own documents or super admins can update all"
134
+ ON storage.objects
135
+ FOR UPDATE
136
+ TO authenticated
137
+ USING (
138
+ bucket_id = 'documents'
139
+ AND (
140
+ (storage.foldername(name))[1] = auth.uid()::text
141
+ OR public.is_super_admin(auth.uid())
142
+ )
143
+ );
144
+
145
+ CREATE POLICY "Users can delete their own documents or super admins can delete all"
146
+ ON storage.objects
147
+ FOR DELETE
148
+ TO authenticated
149
+ USING (
150
+ bucket_id = 'documents'
151
+ AND (
152
+ (storage.foldername(name))[1] = auth.uid()::text
153
+ OR public.is_super_admin(auth.uid())
154
+ )
155
+ );
156
+
157
+ -- ============================================
158
+ -- 5. Политики для бакета 'images'
159
+ -- ============================================
160
+
161
+ -- Аналогичные политики для бакета images
162
+ CREATE POLICY "Users can upload images to their own folder"
163
+ ON storage.objects
164
+ FOR INSERT
165
+ TO authenticated
166
+ WITH CHECK (
167
+ bucket_id = 'images'
168
+ AND (storage.foldername(name))[1] = auth.uid()::text
169
+ );
170
+
171
+ CREATE POLICY "Users can view their own images or super admins can view all"
172
+ ON storage.objects
173
+ FOR SELECT
174
+ TO authenticated
175
+ USING (
176
+ bucket_id = 'images'
177
+ AND (
178
+ (storage.foldername(name))[1] = auth.uid()::text
179
+ OR public.is_super_admin(auth.uid())
180
+ )
181
+ );
182
+
183
+ CREATE POLICY "Users can update their own images or super admins can update all"
184
+ ON storage.objects
185
+ FOR UPDATE
186
+ TO authenticated
187
+ USING (
188
+ bucket_id = 'images'
189
+ AND (
190
+ (storage.foldername(name))[1] = auth.uid()::text
191
+ OR public.is_super_admin(auth.uid())
192
+ )
193
+ );
194
+
195
+ CREATE POLICY "Users can delete their own images or super admins can delete all"
196
+ ON storage.objects
197
+ FOR DELETE
198
+ TO authenticated
199
+ USING (
200
+ bucket_id = 'images'
201
+ AND (
202
+ (storage.foldername(name))[1] = auth.uid()::text
203
+ OR public.is_super_admin(auth.uid())
204
+ )
205
+ );
206
+
207
+ -- ============================================
208
+ -- 6. Включение RLS
209
+ -- ============================================
210
+
211
+ -- Убедитесь, что RLS включен для таблицы storage.objects
212
+ ALTER TABLE storage.objects ENABLE ROW LEVEL SECURITY;
213
+
214
+ -- ============================================
215
+ -- ПРИМЕЧАНИЯ
216
+ -- ============================================
217
+ -- 1. Замените функцию is_super_admin() на вашу реализацию проверки прав
218
+ -- 2. Структура папок: {userId}/{fileName}
219
+ -- 3. Политики работают только для authenticated пользователей
220
+ -- 4. Для публичного доступа нужно создать отдельные бакеты с public = true
221
+ -- 5. Подписанные URL (signed URLs) работают независимо от RLS политик
222
+
223
+
@@ -1,2 +1,3 @@
1
1
  export * from './useCalc';
2
2
  export * from './useSupabaseSubscription';
3
+ export * from './useFileUpload';
@@ -0,0 +1,129 @@
1
+ import { useState, useCallback } from 'react';
2
+ import { uploadFile, UploadFileOptions, UploadFileResult } from '../utils/fileStorage';
3
+
4
+ interface UseFileUploadOptions {
5
+ bucket: string;
6
+ folder?: string; // Папка внутри бакета (например, 'documents', 'images/avatars'). Создается автоматически, если не существует
7
+ authToken?: string; // JWT token для авторизации
8
+ onSuccess?: (result: UploadFileResult) => void;
9
+ onError?: (error: string) => void;
10
+ }
11
+
12
+ interface UseFileUploadReturn {
13
+ upload: (file: File, fileName?: string) => Promise<UploadFileResult>;
14
+ uploading: boolean;
15
+ progress: number;
16
+ error: string | null;
17
+ result: UploadFileResult | null;
18
+ reset: () => void;
19
+ }
20
+
21
+ /**
22
+ * React хук для загрузки файлов в Supabase Storage
23
+ *
24
+ * Папки создаются автоматически при загрузке файла, если их не существует.
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * // Загрузка в корень бакета
29
+ * const { upload, uploading, error, result } = useFileUpload({
30
+ * bucket: 'user-files',
31
+ * onSuccess: (result) => console.log('Загружено:', result.path),
32
+ * });
33
+ *
34
+ * // Загрузка в конкретную папку (папка создастся автоматически)
35
+ * const { upload } = useFileUpload({
36
+ * bucket: 'documents',
37
+ * folder: 'invoices', // файл будет загружен в invoices/
38
+ * });
39
+ *
40
+ * // Загрузка во вложенную папку (все папки создадутся автоматически)
41
+ * const { upload } = useFileUpload({
42
+ * bucket: 'images',
43
+ * folder: 'avatars/2024', // файл будет загружен в avatars/2024/
44
+ * });
45
+ *
46
+ * const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
47
+ * const file = e.target.files?.[0];
48
+ * if (file) await upload(file);
49
+ * };
50
+ * ```
51
+ */
52
+ export const useFileUpload = (options: UseFileUploadOptions): UseFileUploadReturn => {
53
+ const { bucket, folder, authToken, onSuccess, onError } = options;
54
+
55
+ const [uploading, setUploading] = useState(false);
56
+ const [progress, setProgress] = useState(0);
57
+ const [error, setError] = useState<string | null>(null);
58
+ const [result, setResult] = useState<UploadFileResult | null>(null);
59
+
60
+ const upload = useCallback(
61
+ async (file: File, customFileName?: string): Promise<UploadFileResult> => {
62
+ setUploading(true);
63
+ setProgress(0);
64
+ setError(null);
65
+ setResult(null);
66
+
67
+ try {
68
+ // Симулируем прогресс (Supabase не предоставляет реальный progress)
69
+ const progressInterval = setInterval(() => {
70
+ setProgress((prev) => Math.min(prev + 10, 90));
71
+ }, 100);
72
+
73
+ const uploadOptions: UploadFileOptions = {
74
+ file,
75
+ fileName: customFileName || `${Date.now()}-${file.name}`,
76
+ bucket,
77
+ folder,
78
+ contentType: file.type,
79
+ authToken,
80
+ };
81
+
82
+ const uploadResult = await uploadFile(uploadOptions);
83
+
84
+ clearInterval(progressInterval);
85
+ setProgress(100);
86
+ setResult(uploadResult);
87
+
88
+ if (uploadResult.success) {
89
+ onSuccess?.(uploadResult);
90
+ } else {
91
+ const errorMsg = uploadResult.error || 'Ошибка загрузки файла';
92
+ setError(errorMsg);
93
+ onError?.(errorMsg);
94
+ }
95
+
96
+ return uploadResult;
97
+ } catch (err) {
98
+ const errorMsg = err instanceof Error ? err.message : 'Непредвиденная ошибка';
99
+ setError(errorMsg);
100
+ onError?.(errorMsg);
101
+ setProgress(0);
102
+
103
+ return {
104
+ success: false,
105
+ error: errorMsg,
106
+ };
107
+ } finally {
108
+ setUploading(false);
109
+ }
110
+ },
111
+ [bucket, folder, authToken, onSuccess, onError],
112
+ );
113
+
114
+ const reset = useCallback(() => {
115
+ setUploading(false);
116
+ setProgress(0);
117
+ setError(null);
118
+ setResult(null);
119
+ }, []);
120
+
121
+ return {
122
+ upload,
123
+ uploading,
124
+ progress,
125
+ error,
126
+ result,
127
+ reset,
128
+ };
129
+ };
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { squarefi_bff_api_client } from './api';
2
2
  export * from './utils/apiClientFactory';
3
3
  export * from './utils/tokensFactory';
4
+ export * from './utils/fileStorage';
4
5
  export * from './constants';
5
6
  export * from './hooks';
6
7
  // Also export types if you have any