Spaces:
Paused
Paused
Download Services/supabaseStorageService.js from imkrish/campverse-backend: direct link, hf CLI and curl.
- Browser
- Download file 18.3 kB
-
https://huggingface.co/spaces/imkrish/campverse-backend/resolve/main/Services/supabaseStorageService.js
- Command line
-
hf download hf://spaces/imkrish/campverse-backend/Services/supabaseStorageService.js
-
curl -L -o supabaseStorageService.js https://huggingface.co/spaces/imkrish/campverse-backend/resolve/main/Services/supabaseStorageService.js
18.3 kB
| /** | |
| * Supabase Storage Service | |
| * Handles file uploads to Supabase Storage with organized folder structure | |
| * | |
| * Folder Structure: | |
| * CampVerse/ | |
| * ├── events/ | |
| * │ ├── logos/ | |
| * │ └── banners/ | |
| * ├── users/ | |
| * │ ├── profiles/ | |
| * │ └── documents/ | |
| * │ ├── id-cards/ | |
| * │ └── permissions/ | |
| * ├── certificates/ | |
| * │ └── {eventId}/ | |
| * └── institutions/ | |
| * └── logos/ | |
| */ | |
| const { createClient } = require('@supabase/supabase-js'); | |
| const path = require('path'); | |
| const { randomUUID } = require('crypto'); | |
| const { logger } = require('../Middleware/errorHandler'); | |
| class SupabaseStorageService { | |
| constructor() { | |
| this.supabaseUrl = process.env.SUPABASE_URL; | |
| this.supabaseAnonKey = process.env.SUPABASE_ANON_KEY; | |
| this.supabaseServiceKey = process.env.SUPABASE_SERVICE_KEY; // For admin operations | |
| this.bucketName = process.env.SUPABASE_BUCKET_NAME || 'campverse-storage'; | |
| this.baseFolder = 'CampVerse'; | |
| this.initialized = false; | |
| if (this.supabaseUrl && this.supabaseAnonKey) { | |
| // Client for authenticated operations | |
| this.supabase = createClient(this.supabaseUrl, this.supabaseAnonKey); | |
| // Admin client for operations that bypass RLS (if service key provided) | |
| if (this.supabaseServiceKey) { | |
| this.supabaseAdmin = createClient(this.supabaseUrl, this.supabaseServiceKey, { | |
| auth: { | |
| autoRefreshToken: false, | |
| persistSession: false | |
| } | |
| }); | |
| } | |
| this.initialized = true; | |
| logger.info('Supabase Storage Service initialized with authentication'); | |
| } else { | |
| logger.error('Supabase credentials missing. Please set SUPABASE_URL and SUPABASE_ANON_KEY'); | |
| } | |
| } | |
| /** | |
| * Create authenticated Supabase client for a specific user | |
| * This method would need to be called with proper JWT token | |
| */ | |
| createAuthenticatedClient(userToken) { | |
| if (!userToken) { | |
| return this.supabaseAdmin || this.supabase; // Fallback to admin or anon client | |
| } | |
| const authClient = createClient(this.supabaseUrl, this.supabaseAnonKey); | |
| authClient.auth.setSession({ access_token: userToken, refresh_token: null }); | |
| return authClient; | |
| } | |
| /** | |
| * Generate organized file path with timestamp and unique ID | |
| */ | |
| generateFilePath(category, subcategory, filename, userId = null) { | |
| const timestamp = Date.now(); | |
| const uniqueId = randomUUID().substring(0, 8); | |
| const fileExtension = path.extname(filename); | |
| const baseName = path.basename(filename, fileExtension); | |
| const cleanFilename = `${timestamp}_${uniqueId}_${baseName}${fileExtension}`; | |
| if (userId) { | |
| return `${this.baseFolder}/${category}/${userId}/${subcategory}/${cleanFilename}`; | |
| } | |
| return `${this.baseFolder}/${category}/${subcategory}/${cleanFilename}`; | |
| } | |
| /** | |
| * Upload file to Supabase Storage (Private) | |
| */ | |
| async uploadFile(fileBuffer, filename, filePath, mimetype, metadata = {}, _userToken = null) { | |
| try { | |
| if (!this.initialized) { | |
| throw new Error('Supabase Storage Service not initialized'); | |
| } | |
| // Use admin client for upload to bypass RLS, or authenticated client | |
| const client = this.supabaseAdmin || this.supabase; | |
| const { data, error } = await client.storage | |
| .from(this.bucketName) | |
| .upload(filePath, fileBuffer, { | |
| contentType: mimetype, | |
| metadata: { | |
| originalName: filename, | |
| uploadedAt: new Date().toISOString(), | |
| ...metadata | |
| } | |
| }); | |
| if (error) { | |
| throw new Error(`Supabase upload error: ${error.message}`); | |
| } | |
| // For private storage, generate a signed URL instead of public URL | |
| const { data: signedUrlData, error: urlError } = await client.storage | |
| .from(this.bucketName) | |
| .createSignedUrl(filePath, 365 * 24 * 60 * 60); // 1 year expiry | |
| if (urlError) { | |
| logger.warn(`Could not generate signed URL for ${filePath}, falling back to path`); | |
| // Return the path for later URL generation | |
| return { | |
| url: filePath, // Store path instead of URL | |
| path: filePath, | |
| filename: data.path, | |
| size: fileBuffer.length, | |
| mimetype, | |
| isPrivate: true | |
| }; | |
| } | |
| logger.info(`File uploaded to Supabase successfully: ${filePath}`); | |
| return { | |
| url: signedUrlData.signedUrl, | |
| path: filePath, | |
| filename: data.path, | |
| size: fileBuffer.length, | |
| mimetype, | |
| isPrivate: true | |
| }; | |
| } catch (error) { | |
| logger.error(`Failed to upload file to Supabase: ${filePath}`, error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Generate signed URL for private file access | |
| */ | |
| async generateSignedUrl(filePath, expiresIn = 3600) { | |
| try { | |
| if (!this.initialized) { | |
| throw new Error('Supabase Storage Service not initialized'); | |
| } | |
| const client = this.supabaseAdmin || this.supabase; | |
| const { data, error } = await client.storage | |
| .from(this.bucketName) | |
| .createSignedUrl(filePath, expiresIn); | |
| if (error) { | |
| throw new Error(`Failed to generate signed URL: ${error.message}`); | |
| } | |
| return data.signedUrl; | |
| } catch (error) { | |
| logger.error(`Failed to generate signed URL for: ${filePath}`, error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Upload event image (logo or banner) | |
| */ | |
| async uploadEventImage(fileBuffer, filename, type, mimetype) { | |
| try { | |
| // Validate file type | |
| const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; | |
| if (!allowedTypes.includes(mimetype)) { | |
| throw new Error(`Invalid file type: ${mimetype}. Allowed types: ${allowedTypes.join(', ')}`); | |
| } | |
| const filePath = this.generateFilePath('events', `${type}s`, filename); | |
| const metadata = { | |
| category: 'event', | |
| type, | |
| fileType: 'image' | |
| }; | |
| const result = await this.uploadFile(fileBuffer, filename, filePath, mimetype, metadata); | |
| logger.info(`Event ${type} uploaded to Supabase successfully: ${filename}`); | |
| return result.url; // Return URL for backward compatibility | |
| } catch (error) { | |
| logger.error(`Failed to upload event ${type} to Supabase:`, error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Upload profile photo | |
| */ | |
| async uploadProfilePhoto(fileBuffer, filename, userId, mimetype) { | |
| try { | |
| // Validate file type | |
| const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; | |
| if (!allowedTypes.includes(mimetype)) { | |
| throw new Error(`Invalid file type: ${mimetype}. Allowed types: ${allowedTypes.join(', ')}`); | |
| } | |
| const filePath = this.generateFilePath('users', 'profiles', filename, userId); | |
| const metadata = { | |
| category: 'profile', | |
| userId, | |
| fileType: 'image' | |
| }; | |
| const result = await this.uploadFile(fileBuffer, filename, filePath, mimetype, metadata); | |
| logger.info(`Profile photo uploaded to Supabase successfully for user: ${userId}`); | |
| return result.url; // Return URL for backward compatibility | |
| } catch (error) { | |
| logger.error(`Failed to upload profile photo to Supabase for user ${userId}:`, error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Upload user document (ID card, event permission, etc.) | |
| */ | |
| async uploadUserDocument(fileBuffer, filename, documentType, userId, mimetype) { | |
| try { | |
| // Validate file type | |
| const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'application/pdf']; | |
| if (!allowedTypes.includes(mimetype)) { | |
| throw new Error(`Invalid file type: ${mimetype}. Allowed types: ${allowedTypes.join(', ')}`); | |
| } | |
| const filePath = this.generateFilePath('users', `documents/${documentType}`, filename, userId); | |
| const metadata = { | |
| category: 'user_document', | |
| documentType, | |
| userId, | |
| fileType: mimetype.includes('image') ? 'image' : 'document' | |
| }; | |
| const result = await this.uploadFile(fileBuffer, filename, filePath, mimetype, metadata); | |
| logger.info(`User document uploaded to Supabase successfully: ${documentType} for user ${userId}`); | |
| return result.url; | |
| } catch (error) { | |
| logger.error('Failed to upload user document to Supabase:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Upload certificate | |
| */ | |
| async uploadCertificate(fileBuffer, filename, eventId, userId, mimetype) { | |
| try { | |
| // Validate file type | |
| const allowedTypes = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png']; | |
| if (!allowedTypes.includes(mimetype)) { | |
| throw new Error(`Invalid file type: ${mimetype}. Allowed types: ${allowedTypes.join(', ')}`); | |
| } | |
| const filePath = this.generateFilePath('certificates', eventId, filename, userId); | |
| const metadata = { | |
| category: 'certificate', | |
| eventId, | |
| userId, | |
| fileType: mimetype.includes('pdf') ? 'pdf' : 'image' | |
| }; | |
| const result = await this.uploadFile(fileBuffer, filename, filePath, mimetype, metadata); | |
| logger.info(`Certificate uploaded to Supabase successfully for event ${eventId}, user ${userId}`); | |
| return result.url; | |
| } catch (error) { | |
| logger.error('Failed to upload certificate to Supabase:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Upload certificate template | |
| */ | |
| async uploadCertificateTemplate(fileBuffer, filename, eventId, templateType, mimetype) { | |
| try { | |
| // Validate file type | |
| const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png']; | |
| if (!allowedTypes.includes(mimetype)) { | |
| throw new Error(`Invalid file type: ${mimetype}. Allowed types: ${allowedTypes.join(', ')}`); | |
| } | |
| const filePath = `${this.baseFolder}/certificates/assets/${eventId}/templates/${templateType}_${Date.now()}_${filename}`; | |
| const metadata = { | |
| category: 'certificate-template', | |
| eventId, | |
| templateType, | |
| fileType: 'image' | |
| }; | |
| const result = await this.uploadFile(fileBuffer, filename, filePath, mimetype, metadata); | |
| logger.info(`Certificate template uploaded to Supabase successfully for event ${eventId}, type: ${templateType}`); | |
| return result; | |
| } catch (error) { | |
| logger.error('Failed to upload certificate template to Supabase:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Upload certificate logo (organization logo) | |
| */ | |
| async uploadCertificateLogo(fileBuffer, filename, eventId, logoType, mimetype) { | |
| try { | |
| // Validate file type | |
| const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']; | |
| if (!allowedTypes.includes(mimetype)) { | |
| throw new Error(`Invalid file type: ${mimetype}. Allowed types: ${allowedTypes.join(', ')}`); | |
| } | |
| const filePath = `${this.baseFolder}/certificates/assets/${eventId}/logos/${logoType}_${Date.now()}_${filename}`; | |
| const metadata = { | |
| category: 'certificate-logo', | |
| eventId, | |
| logoType, | |
| fileType: 'image' | |
| }; | |
| const result = await this.uploadFile(fileBuffer, filename, filePath, mimetype, metadata); | |
| logger.info(`Certificate logo uploaded to Supabase successfully for event ${eventId}, type: ${logoType}`); | |
| return result; | |
| } catch (error) { | |
| logger.error('Failed to upload certificate logo to Supabase:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Upload certificate signature | |
| */ | |
| async uploadCertificateSignature(fileBuffer, filename, eventId, signatureType, mimetype) { | |
| try { | |
| // Validate file type | |
| const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']; | |
| if (!allowedTypes.includes(mimetype)) { | |
| throw new Error(`Invalid file type: ${mimetype}. Allowed types: ${allowedTypes.join(', ')}`); | |
| } | |
| const filePath = `${this.baseFolder}/certificates/assets/${eventId}/signatures/${signatureType}_${Date.now()}_${filename}`; | |
| const metadata = { | |
| category: 'certificate-signature', | |
| eventId, | |
| signatureType, | |
| fileType: 'image' | |
| }; | |
| const result = await this.uploadFile(fileBuffer, filename, filePath, mimetype, metadata); | |
| logger.info(`Certificate signature uploaded to Supabase successfully for event ${eventId}, type: ${signatureType}`); | |
| return result; | |
| } catch (error) { | |
| logger.error('Failed to upload certificate signature to Supabase:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Upload institution logo | |
| */ | |
| async uploadInstitutionLogo(fileBuffer, filename, institutionId, mimetype) { | |
| try { | |
| // Validate file type | |
| const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; | |
| if (!allowedTypes.includes(mimetype)) { | |
| throw new Error(`Invalid file type: ${mimetype}. Allowed types: ${allowedTypes.join(', ')}`); | |
| } | |
| const filePath = this.generateFilePath('institutions', 'logos', filename, institutionId); | |
| const metadata = { | |
| category: 'institution', | |
| institutionId, | |
| fileType: 'image' | |
| }; | |
| const result = await this.uploadFile(fileBuffer, filename, filePath, mimetype, metadata); | |
| logger.info(`Institution logo uploaded to Supabase successfully for institution: ${institutionId}`); | |
| return result.url; | |
| } catch (error) { | |
| logger.error('Failed to upload institution logo to Supabase:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Delete file from Supabase Storage | |
| */ | |
| async deleteFile(fileUrl) { | |
| try { | |
| if (!this.initialized) { | |
| throw new Error('Supabase Storage Service not initialized'); | |
| } | |
| if (!fileUrl) { | |
| logger.warn('No file URL provided for deletion'); | |
| return false; | |
| } | |
| logger.info(`Starting deletion for URL: ${fileUrl}`); | |
| // Extract filename from the URL - works for both public and signed URLs | |
| let filename = null; | |
| // For Supabase URLs, the filename is at the end of the path before any query parameters | |
| if (fileUrl.includes(this.supabaseUrl)) { | |
| // Remove query parameters (like ?token=...) | |
| const urlWithoutQuery = fileUrl.split('?')[0]; | |
| // Get the last part of the path (the filename) | |
| const pathParts = urlWithoutQuery.split('/'); | |
| filename = pathParts[pathParts.length - 1]; | |
| logger.info(`Extracted filename: ${filename}`); | |
| // Since we know the file structure, we can find the user ID from the URL | |
| const userMatch = fileUrl.match(/\/users\/([^/]+)\/profiles\//); | |
| if (userMatch && userMatch[1]) { | |
| const userId = userMatch[1]; | |
| const filePath = `CampVerse/users/${userId}/profiles/${filename}`; | |
| logger.info(`Constructed file path: ${filePath}`); | |
| // Use admin client for deletion to bypass RLS | |
| const client = this.supabaseAdmin || this.supabase; | |
| const { error } = await client.storage | |
| .from(this.bucketName) | |
| .remove([filePath]); | |
| if (error) { | |
| logger.error(`Deletion failed: ${error.message}`); | |
| return false; | |
| } | |
| logger.info(`File deleted successfully: ${filePath}`); | |
| return true; | |
| } | |
| } | |
| logger.warn(`Could not extract file information from URL: ${fileUrl}`); | |
| return false; | |
| } catch (error) { | |
| logger.error(`Failed to delete file from Supabase: ${fileUrl}`, error); | |
| return false; // Don't throw - deletion failures shouldn't break the app | |
| } | |
| } | |
| /** | |
| * List files in a folder | |
| */ | |
| async listFiles(folderPath) { | |
| try { | |
| if (!this.initialized) { | |
| throw new Error('Supabase Storage Service not initialized'); | |
| } | |
| const { data, error } = await this.supabase.storage | |
| .from(this.bucketName) | |
| .list(`${this.baseFolder}/${folderPath}`); | |
| if (error) { | |
| throw new Error(`Supabase list error: ${error.message}`); | |
| } | |
| const fileList = data.map(file => { | |
| const fullPath = `${this.baseFolder}/${folderPath}/${file.name}`; | |
| const { data: urlData } = this.supabase.storage | |
| .from(this.bucketName) | |
| .getPublicUrl(fullPath); | |
| return { | |
| name: file.name, | |
| path: fullPath, | |
| url: urlData.publicUrl, | |
| size: file.metadata?.size || 0, | |
| contentType: file.metadata?.mimetype || 'unknown', | |
| timeCreated: file.created_at, | |
| updated: file.updated_at | |
| }; | |
| }); | |
| logger.info(`Listed ${fileList.length} files in Supabase folder: ${folderPath}`); | |
| return fileList; | |
| } catch (error) { | |
| logger.error(`Failed to list files in Supabase folder: ${folderPath}`, error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Health check | |
| */ | |
| async healthCheck() { | |
| try { | |
| if (!this.initialized) { | |
| return { | |
| status: 'error', | |
| message: 'Supabase Storage service not initialized', | |
| initialized: false | |
| }; | |
| } | |
| // Test bucket access by trying to list files | |
| const { error } = await this.supabase.storage | |
| .from(this.bucketName) | |
| .list('', { limit: 1 }); | |
| if (error) { | |
| return { | |
| status: 'error', | |
| message: `Supabase Storage error: ${error.message}`, | |
| bucket: this.bucketName, | |
| initialized: this.initialized | |
| }; | |
| } | |
| return { | |
| status: 'healthy', | |
| message: 'Supabase Storage service is operational', | |
| bucket: this.bucketName, | |
| initialized: this.initialized | |
| }; | |
| } catch (error) { | |
| return { | |
| status: 'error', | |
| message: error.message, | |
| bucket: this.bucketName, | |
| initialized: this.initialized | |
| }; | |
| } | |
| } | |
| } | |
| // Create singleton instance | |
| const supabaseStorageService = new SupabaseStorageService(); | |
| module.exports = { | |
| SupabaseStorageService, | |
| supabaseStorageService | |
| }; | |