diff --git a/.env.development b/.env.development index 18f715025..602ca4065 100644 --- a/.env.development +++ b/.env.development @@ -36,6 +36,9 @@ S3_REGION=us-east-1 S3_ACCESS_KEY=minioadmin S3_SECRET_KEY=minioadmin +# Unified Storage (single bucket for all apps) +MANACORE_STORAGE_PUBLIC_URL=http://localhost:9000/manacore-storage + # ============================================ # MANA-CORE-AUTH SERVICE # ============================================ @@ -127,9 +130,7 @@ PICTURE_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/picture # Replicate API Token for AI image generation PICTURE_REPLICATE_API_TOKEN=r8_QlvkstNhIc6NBX1ktpQ6ibvzOE2d2UQ1Emamd -# Storage Configuration (uses MinIO locally, Hetzner in production) -# Uses shared S3_* variables from above - no project-specific override needed for local dev -PICTURE_STORAGE_PUBLIC_URL=http://localhost:9000/picture-storage +# Storage: Uses unified manacore-storage bucket (see MANACORE_STORAGE_PUBLIC_URL above) # Credit System (staging only - freemium: 3 free images, then credits) PICTURE_APP_ID=picture-app @@ -148,8 +149,7 @@ NUTRIPHI_DATABASE_URL=postgresql://nutriphi:nutriphi_dev_password@localhost:5435 NUTRIPHI_APP_ID=nutriphi NUTRIPHI_GEMINI_API_KEY=your-gemini-api-key-here -# S3 Storage (uses MinIO locally via shared S3_* variables, Hetzner in production) -NUTRIPHI_S3_PUBLIC_URL=http://localhost:9000/nutriphi-storage +# Storage: Uses unified manacore-storage bucket # ============================================ # ZITARE PROJECT @@ -180,9 +180,7 @@ VOXEL_LAVA_API_URL=http://localhost:3010 CONTACTS_BACKEND_PORT=3015 CONTACTS_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/contacts -# S3 Storage for contact photos -CONTACTS_S3_BUCKET=contacts-photos -CONTACTS_S3_PUBLIC_URL=http://localhost:9000/contacts-photos +# Storage: Uses unified manacore-storage bucket # Google OAuth for contacts import # Get credentials from https://console.cloud.google.com/apis/credentials @@ -204,7 +202,6 @@ CALENDAR_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/calendar STORAGE_BACKEND_PORT=3016 STORAGE_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/storage -STORAGE_S3_PUBLIC_URL=http://localhost:9000/storage-storage STORAGE_MAX_FILE_SIZE=104857600 STORAGE_MAX_FILES_PER_UPLOAD=10 @@ -265,7 +262,6 @@ FINANCE_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/finance INVENTORY_BACKEND_PORT=3020 INVENTORY_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/inventory -INVENTORY_S3_PUBLIC_URL=http://localhost:9000/inventory-storage # ============================================ # TECHBASE PROJECT diff --git a/.github/workflows/cd-staging.yml b/.github/workflows/cd-staging.yml index 4af222aed..15e10a72f 100644 --- a/.github/workflows/cd-staging.yml +++ b/.github/workflows/cd-staging.yml @@ -98,29 +98,36 @@ jobs: POSTGRES_PORT=5432 POSTGRES_DB=manacore POSTGRES_USER=postgres - POSTGRES_PASSWORD=${{ secrets.STAGING_POSTGRES_PASSWORD }} + POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }} # Redis - Configuration REDIS_HOST=redis REDIS_PORT=6379 - REDIS_PASSWORD=${{ secrets.STAGING_REDIS_PASSWORD }} + REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }} # Mana Core Auth - Configuration MANA_SERVICE_URL=http://mana-core-auth:3001 - JWT_SECRET=${{ secrets.STAGING_JWT_SECRET }} - JWT_PUBLIC_KEY=${{ secrets.STAGING_JWT_PUBLIC_KEY }} - JWT_PRIVATE_KEY=${{ secrets.STAGING_JWT_PRIVATE_KEY }} + JWT_SECRET=${{ secrets.JWT_SECRET }} + JWT_PUBLIC_KEY=${{ secrets.JWT_PUBLIC_KEY }} + JWT_PRIVATE_KEY=${{ secrets.JWT_PRIVATE_KEY }} # Supabase - SUPABASE_URL=${{ secrets.STAGING_SUPABASE_URL }} - SUPABASE_ANON_KEY=${{ secrets.STAGING_SUPABASE_ANON_KEY }} - SUPABASE_SERVICE_ROLE_KEY=${{ secrets.STAGING_SUPABASE_SERVICE_ROLE_KEY }} + SUPABASE_URL=${{ secrets.SUPABASE_URL }} + SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }} + SUPABASE_SERVICE_ROLE_KEY=${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} # Azure OpenAI - AZURE_OPENAI_ENDPOINT=${{ secrets.STAGING_AZURE_OPENAI_ENDPOINT }} - AZURE_OPENAI_API_KEY=${{ secrets.STAGING_AZURE_OPENAI_API_KEY }} + AZURE_OPENAI_ENDPOINT=${{ secrets.AZURE_OPENAI_ENDPOINT }} + AZURE_OPENAI_API_KEY=${{ secrets.AZURE_OPENAI_API_KEY }} AZURE_OPENAI_API_VERSION=2024-12-01-preview + # Hetzner Object Storage (S3-compatible) + S3_ENDPOINT=${{ secrets.S3_ENDPOINT }} + S3_REGION=${{ secrets.S3_REGION }} + S3_ACCESS_KEY=${{ secrets.S3_ACCESS_KEY }} + S3_SECRET_KEY=${{ secrets.S3_SECRET_KEY }} + MANACORE_STORAGE_PUBLIC_URL=${{ secrets.MANACORE_STORAGE_PUBLIC_URL }} + # Environment NODE_ENV=staging EOF diff --git a/apps/contacts/apps/backend/src/photo/photo.service.ts b/apps/contacts/apps/backend/src/photo/photo.service.ts index 0fad07162..eae3f2145 100644 --- a/apps/contacts/apps/backend/src/photo/photo.service.ts +++ b/apps/contacts/apps/backend/src/photo/photo.service.ts @@ -4,19 +4,19 @@ import { DATABASE_CONNECTION } from '../db/database.module'; import { Database } from '../db/connection'; import { contacts } from '../db/schema'; import { - createContactsStorage, - generateUserFileKey, + createUnifiedStorage, getContentType, validateFileSize, validateFileExtension, IMAGE_EXTENSIONS, + APPS, } from '@manacore/shared-storage'; const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB @Injectable() export class PhotoService { - private storage = createContactsStorage(); + private storage = createUnifiedStorage(); constructor(@Inject(DATABASE_CONNECTION) private db: Database) {} @@ -66,19 +66,22 @@ export class PhotoService { } } - // Generate unique key for the new photo + // Generate unique key for the new photo: {userId}/contacts/{contactId}.{ext} const filename = `${contactId}.${extension}`; - const key = generateUserFileKey(userId, filename); + const key = `${userId}/${APPS.CONTACTS}/${filename}`; // Upload to S3 const contentType = getContentType(filename); - await this.storage.upload(key, file.buffer, { + const result = await this.storage.upload(key, file.buffer, { contentType, public: true, }); - // Generate the URL (for MinIO, construct it manually) - const photoUrl = `http://localhost:9000/contacts-storage/${key}`; + // Get URL from storage client or construct manually + const photoUrl = + result.url || + this.storage.getPublicUrl(key) || + `${process.env.MANACORE_STORAGE_PUBLIC_URL || 'http://localhost:9000/manacore-storage'}/${key}`; // Update contact with photo URL await this.db @@ -125,8 +128,12 @@ export class PhotoService { } private extractKeyFromUrl(url: string): string | null { - // Extract key from URLs like http://localhost:9000/contacts-storage/users/xxx/file.jpg - const match = url.match(/contacts-storage\/(.+)$/); - return match ? match[1] : null; + // Extract key from URLs like http://localhost:9000/manacore-storage/userId/contacts/file.jpg + // Also support old format: http://localhost:9000/contacts-storage/users/xxx/file.jpg + const unifiedMatch = url.match(/manacore-storage\/(.+)$/); + if (unifiedMatch) return unifiedMatch[1]; + + const legacyMatch = url.match(/contacts-storage\/(.+)$/); + return legacyMatch ? legacyMatch[1] : null; } } diff --git a/apps/picture/apps/backend/src/upload/storage.service.ts b/apps/picture/apps/backend/src/upload/storage.service.ts index 72e2eabf3..b068fb9c8 100644 --- a/apps/picture/apps/backend/src/upload/storage.service.ts +++ b/apps/picture/apps/backend/src/upload/storage.service.ts @@ -1,11 +1,6 @@ import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { - createPictureStorage, - StorageClient, - generateUserFileKey, - getContentType, -} from '@manacore/shared-storage'; +import { createUnifiedStorage, StorageClient, APPS } from '@manacore/shared-storage'; export type StorageMode = 's3'; @@ -18,15 +13,15 @@ export class StorageService implements OnModuleInit { constructor(private configService: ConfigService) { // Get public URL from config this.publicUrl = this.configService.get( - 'STORAGE_PUBLIC_URL', - 'http://localhost:9000/picture-storage' + 'MANACORE_STORAGE_PUBLIC_URL', + 'http://localhost:9000/manacore-storage' ); } onModuleInit() { - // Initialize storage client - this.storage = createPictureStorage(this.publicUrl); - this.logger.log(`Storage initialized with @manacore/shared-storage (bucket: picture-storage)`); + // Initialize unified storage client + this.storage = createUnifiedStorage(this.publicUrl); + this.logger.log(`Storage initialized with @manacore/shared-storage (bucket: manacore-storage)`); } async uploadFile( @@ -38,7 +33,8 @@ export class StorageService implements OnModuleInit { const timestamp = Date.now(); const randomId = Math.random().toString(36).substring(2, 10); const ext = filename.split('.').pop() || 'jpg'; - const storagePath = `${userId}/${timestamp}-${randomId}.${ext}`; + // Path: {userId}/picture/{timestamp}-{randomId}.{ext} + const storagePath = `${userId}/${APPS.PICTURE}/${timestamp}-${randomId}.${ext}`; const result = await this.storage.upload(storagePath, buffer, { contentType, @@ -82,7 +78,8 @@ export class StorageService implements OnModuleInit { async uploadBoardThumbnail(boardId: string, dataUrl: string): Promise { const base64Data = dataUrl.replace(/^data:image\/\w+;base64,/, ''); const buffer = Buffer.from(base64Data, 'base64'); - const storagePath = `boards/${boardId}/thumbnail-${Date.now()}.png`; + // Path: boards/picture/{boardId}/thumbnail-{timestamp}.png + const storagePath = `boards/${APPS.PICTURE}/${boardId}/thumbnail-${Date.now()}.png`; const result = await this.storage.upload(storagePath, buffer, { contentType: 'image/png', diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 9175eee05..3563eab26 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -76,18 +76,9 @@ services: entrypoint: > /bin/sh -c " mc alias set myminio http://minio:9000 $${MINIO_ROOT_USER:-minioadmin} $${MINIO_ROOT_PASSWORD:-minioadmin}; - mc mb --ignore-existing myminio/picture-storage; - mc mb --ignore-existing myminio/chat-storage; - mc mb --ignore-existing myminio/manadeck-storage; - mc mb --ignore-existing myminio/nutriphi-storage; - mc mb --ignore-existing myminio/presi-storage; - mc mb --ignore-existing myminio/calendar-storage; - mc mb --ignore-existing myminio/contacts-storage; - mc mb --ignore-existing myminio/storage-storage; - mc mb --ignore-existing myminio/inventory-storage; - mc anonymous set download myminio/picture-storage; - mc anonymous set download myminio/inventory-storage; - echo 'Buckets created successfully'; + mc mb --ignore-existing myminio/manacore-storage; + mc anonymous set download myminio/manacore-storage; + echo 'Bucket manacore-storage created successfully'; exit 0; " networks: diff --git a/packages/shared-storage/README.md b/packages/shared-storage/README.md index d74710b7b..564d4e8f9 100644 --- a/packages/shared-storage/README.md +++ b/packages/shared-storage/README.md @@ -2,53 +2,71 @@ S3-compatible object storage client for the Manacore monorepo. Uses MinIO for local development and Hetzner Object Storage in production. +## Architecture + +All apps use a **single unified bucket** with folder structure: + +``` +manacore-storage/ +├── {userId}/ +│ ├── picture/... # Picture app files +│ ├── chat/... # Chat attachments +│ ├── manadeck/... # Card assets +│ ├── contacts/... # Contact avatars +│ └── ... +``` + ## Setup ### Local Development -1. Start MinIO with Docker: - ```bash +# Start MinIO with Docker pnpm docker:up + +# MinIO Console: http://localhost:9001 +# Username: minioadmin +# Password: minioadmin ``` -2. Access MinIO Console at http://localhost:9001 - - Username: `minioadmin` - - Password: `minioadmin` +### Production (Hetzner Object Storage) -### Pre-created Buckets +1. Create Hetzner Object Storage in [Hetzner Cloud Console](https://console.hetzner.cloud/) +2. Generate S3 credentials (Access Key + Secret Key) +3. Run the setup script: -The following buckets are automatically created: - -| Bucket | Project | Purpose | -|--------|---------|---------| -| `picture-storage` | Picture | Generated AI images | -| `chat-storage` | Chat | User file uploads | -| `manadeck-storage` | ManaDeck | Card/deck assets | -| `nutriphi-storage` | NutriPhi | Meal photos | -| `presi-storage` | Presi | Presentation slides | -| `calendar-storage` | Calendar | Calendar attachments | -| `contacts-storage` | Contacts | Contact avatars/files | -| `storage-storage` | Storage | Cloud drive files | +```bash +export S3_ENDPOINT="https://fsn1.your-objectstorage.com" +export S3_ACCESS_KEY="your-access-key" +export S3_SECRET_KEY="your-secret-key" +./scripts/setup-hetzner-storage.sh +``` ## Usage -### Basic Usage - ```typescript -import { createPictureStorage, generateUserFileKey, getContentType } from '@manacore/shared-storage'; +import { + createUnifiedStorage, + generateStorageKey, + getContentType, + APPS, +} from '@manacore/shared-storage'; -// Create client for Picture project -const storage = createPictureStorage(); +// Create storage client +const storage = createUnifiedStorage(); + +// Generate a key for a user's file +const key = generateStorageKey('user-123', APPS.PICTURE, 'photo.jpg'); +// => 'user-123/picture/a1b2c3d4-uuid.jpg' // Upload a file -const key = generateUserFileKey('user-123', 'avatar.png'); const result = await storage.upload(key, imageBuffer, { - contentType: getContentType('avatar.png'), + contentType: getContentType('photo.jpg'), public: true, }); -console.log(result.url); // http://localhost:9000/picture-storage/users/user-123/uuid.png +console.log(result.url); +// => 'http://localhost:9000/manacore-storage/user-123/picture/uuid.jpg' // Download a file const buffer = await storage.download(key); @@ -56,42 +74,63 @@ const buffer = await storage.download(key); // Delete a file await storage.delete(key); -// List files -const files = await storage.list('users/user-123/'); +// List files for a user's app +const files = await storage.list('user-123/picture/'); // Generate presigned URLs const uploadUrl = await storage.getUploadUrl('temp/upload.png', { expiresIn: 3600 }); const downloadUrl = await storage.getDownloadUrl(key, { expiresIn: 3600 }); ``` -### Custom Configuration +## Available Apps ```typescript -import { createStorageClient, BUCKETS } from '@manacore/shared-storage'; +import { APPS } from '@manacore/shared-storage'; -// Override default config -const storage = createStorageClient(BUCKETS.PICTURE, { - endpoint: 'https://fsn1.your-objectstorage.com', - region: 'fsn1', - accessKeyId: process.env.HETZNER_ACCESS_KEY, - secretAccessKey: process.env.HETZNER_SECRET_KEY, - forcePathStyle: false, -}); +APPS.PICTURE // 'picture' +APPS.CHAT // 'chat' +APPS.MANADECK // 'manadeck' +APPS.NUTRIPHI // 'nutriphi' +APPS.PRESI // 'presi' +APPS.CALENDAR // 'calendar' +APPS.CONTACTS // 'contacts' +APPS.STORAGE // 'storage' +APPS.MAIL // 'mail' +APPS.INVENTORY // 'inventory' +APPS.MANACORE // 'manacore' ``` -### Available Factory Functions +## Key Generation Utilities ```typescript import { - createPictureStorage, - createChatStorage, - createManaDeckStorage, - createNutriPhiStorage, - createPresiStorage, - createCalendarStorage, - createContactsStorage, - createStorageStorage, + generateStorageKey, + generateFileKey, + generateUserFileKey, + getContentType, + validateFileSize, + validateFileExtension, + IMAGE_EXTENSIONS, } from '@manacore/shared-storage'; + +// Recommended: App-scoped key +generateStorageKey('user-123', 'picture', 'photo.jpg'); +// => 'user-123/picture/uuid.jpg' + +// With subfolder +generateStorageKey('user-123', 'chat', 'doc.pdf', 'attachments'); +// => 'user-123/chat/attachments/uuid.pdf' + +// Generic file key +generateFileKey('photo.jpg', 'uploads', '2024'); +// => 'uploads/2024/uuid.jpg' + +// Get MIME type +getContentType('image.png'); // => 'image/png' + +// Validate file +validateFileSize(fileSize, 10); // max 10MB +validateFileExtension('photo.jpg', IMAGE_EXTENSIONS); ``` ## Environment Variables @@ -105,59 +144,25 @@ S3_ENDPOINT=http://localhost:9000 S3_REGION=us-east-1 S3_ACCESS_KEY=minioadmin S3_SECRET_KEY=minioadmin +MANACORE_STORAGE_PUBLIC_URL=http://localhost:9000/manacore-storage ``` -### Production (Hetzner Object Storage) +### Production (Hetzner) ```env S3_ENDPOINT=https://fsn1.your-objectstorage.com S3_REGION=fsn1 S3_ACCESS_KEY=your-access-key S3_SECRET_KEY=your-secret-key - -# Optional: public URLs for CDN access -PICTURE_STORAGE_PUBLIC_URL=https://picture-storage.fsn1.your-objectstorage.com -NUTRIPHI_S3_PUBLIC_URL=https://nutriphi-storage.fsn1.your-objectstorage.com -``` - -## Utilities - -```typescript -import { - generateFileKey, - generateUserFileKey, - getContentType, - validateFileSize, - validateFileExtension, - IMAGE_EXTENSIONS, - DOCUMENT_EXTENSIONS, -} from '@manacore/shared-storage'; - -// Generate unique file key -const key = generateFileKey('photo.jpg', 'uploads', '2024'); -// => 'uploads/2024/uuid.jpg' - -// User-scoped key -const userKey = generateUserFileKey('user-123', 'avatar.png', 'avatars'); -// => 'users/user-123/avatars/uuid.png' - -// Get MIME type -const contentType = getContentType('image.png'); // => 'image/png' - -// Validate file -const isValidSize = validateFileSize(fileSize, 10); // max 10MB -const isValidType = validateFileExtension('photo.jpg', IMAGE_EXTENSIONS); +MANACORE_STORAGE_PUBLIC_URL=https://manacore-storage.fsn1.your-objectstorage.com ``` ## Docker Commands ```bash -# Start all infrastructure (Postgres, Redis, MinIO) +# Start infrastructure (Postgres, Redis, MinIO) pnpm docker:up -# Start only database services (no MinIO) -pnpm docker:up:db - # View MinIO logs docker logs manacore-minio diff --git a/packages/shared-storage/src/factory.ts b/packages/shared-storage/src/factory.ts index c8a529587..ec8242d0b 100644 --- a/packages/shared-storage/src/factory.ts +++ b/packages/shared-storage/src/factory.ts @@ -1,6 +1,6 @@ import { StorageClient } from './client'; -import { BUCKETS } from './types'; -import type { StorageConfig, BucketConfig, BucketName } from './types'; +import { UNIFIED_BUCKET, APPS } from './types'; +import type { StorageConfig, BucketConfig, AppName } from './types'; /** * Environment variable names for storage configuration @@ -30,7 +30,6 @@ const MINIO_DEFAULTS: StorageConfig = { export function getStorageConfig(): StorageConfig { const isDev = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV; - // Use environment variables if available, otherwise use MinIO defaults return { endpoint: process.env[ENV_KEYS.ENDPOINT] ?? (isDev ? MINIO_DEFAULTS.endpoint : ''), region: process.env[ENV_KEYS.REGION] ?? MINIO_DEFAULTS.region, @@ -45,7 +44,7 @@ export function getStorageConfig(): StorageConfig { * Create a storage client for a specific bucket */ export function createStorageClient( - bucket: BucketName | BucketConfig, + bucket: string | BucketConfig, config?: Partial ): StorageClient { const storageConfig = { @@ -55,7 +54,6 @@ export function createStorageClient( const bucketConfig: BucketConfig = typeof bucket === 'string' ? { name: bucket } : bucket; - // Validate configuration if (!storageConfig.endpoint) { throw new Error('S3_ENDPOINT is required for storage configuration'); } @@ -67,83 +65,31 @@ export function createStorageClient( } /** - * Create a storage client for the Picture project + * Create the unified storage client for all Manacore apps + * + * Uses a single bucket with folder structure: {userId}/{appName}/... + * + * @example + * import { createUnifiedStorage, generateStorageKey, APPS } from '@manacore/shared-storage'; + * + * const storage = createUnifiedStorage(); + * + * // Upload for a specific user and app + * const key = generateStorageKey('user-123', APPS.PICTURE, 'photo.jpg'); + * await storage.upload(key, imageBuffer, { contentType: 'image/jpeg', public: true }); + * + * // List all files for a user in an app + * const files = await storage.list('user-123/picture/'); */ -export function createPictureStorage(publicUrl?: string): StorageClient { +export function createUnifiedStorage(publicUrl?: string): StorageClient { return createStorageClient({ - name: BUCKETS.PICTURE, - publicUrl: publicUrl ?? process.env.PICTURE_STORAGE_PUBLIC_URL, + name: UNIFIED_BUCKET, + publicUrl: publicUrl ?? process.env.MANACORE_STORAGE_PUBLIC_URL, }); } /** - * Create a storage client for the Chat project + * Re-export constants and types for convenience */ -export function createChatStorage(): StorageClient { - return createStorageClient({ name: BUCKETS.CHAT }); -} - -/** - * Create a storage client for the ManaDeck project - */ -export function createManaDeckStorage(): StorageClient { - return createStorageClient({ name: BUCKETS.MANADECK }); -} - -/** - * Create a storage client for the NutriPhi project - */ -export function createNutriPhiStorage(publicUrl?: string): StorageClient { - return createStorageClient({ - name: BUCKETS.NUTRIPHI, - publicUrl: publicUrl ?? process.env.NUTRIPHI_S3_PUBLIC_URL, - }); -} - -/** - * Create a storage client for the Presi project - */ -export function createPresiStorage(): StorageClient { - return createStorageClient({ name: BUCKETS.PRESI }); -} - -/** - * Create a storage client for the Calendar project - */ -export function createCalendarStorage(): StorageClient { - return createStorageClient({ name: BUCKETS.CALENDAR }); -} - -/** - * Create a storage client for the Contacts project - */ -export function createContactsStorage(): StorageClient { - return createStorageClient({ name: BUCKETS.CONTACTS }); -} - -/** - * Create a storage client for the Storage project (cloud drive) - */ -export function createStorageStorage(publicUrl?: string): StorageClient { - return createStorageClient({ - name: BUCKETS.STORAGE, - publicUrl: publicUrl ?? process.env.STORAGE_S3_PUBLIC_URL, - }); -} - -/** - * Create a storage client for the Mail project - */ -export function createMailStorage(): StorageClient { - return createStorageClient({ name: BUCKETS.MAIL }); -} - -/** - * Create a storage client for the Inventory project - */ -export function createInventoryStorage(publicUrl?: string): StorageClient { - return createStorageClient({ - name: BUCKETS.INVENTORY, - publicUrl: publicUrl ?? process.env.INVENTORY_S3_PUBLIC_URL, - }); -} +export { UNIFIED_BUCKET, APPS }; +export type { AppName }; diff --git a/packages/shared-storage/src/index.ts b/packages/shared-storage/src/index.ts index f3ef3c5be..911d41d97 100644 --- a/packages/shared-storage/src/index.ts +++ b/packages/shared-storage/src/index.ts @@ -4,23 +4,17 @@ export { StorageClient } from './client'; // Factory functions export { createStorageClient, + createUnifiedStorage, getStorageConfig, - createPictureStorage, - createChatStorage, - createManaDeckStorage, - createNutriPhiStorage, - createPresiStorage, - createCalendarStorage, - createContactsStorage, - createStorageStorage, - createMailStorage, - createInventoryStorage, + UNIFIED_BUCKET, + APPS, } from './factory'; // Utilities export { generateFileKey, generateUserFileKey, + generateStorageKey, getContentType, validateFileSize, validateFileExtension, @@ -31,13 +25,12 @@ export { } from './utils'; // Types -export { - BUCKETS, - type StorageConfig, - type BucketConfig, - type BucketName, - type UploadOptions, - type PresignedUrlOptions, - type UploadResult, - type FileInfo, +export type { + StorageConfig, + BucketConfig, + AppName, + UploadOptions, + PresignedUrlOptions, + UploadResult, + FileInfo, } from './types'; diff --git a/packages/shared-storage/src/types.ts b/packages/shared-storage/src/types.ts index 5fa3e43c2..d2216c261 100644 --- a/packages/shared-storage/src/types.ts +++ b/packages/shared-storage/src/types.ts @@ -73,19 +73,26 @@ export interface FileInfo { } /** - * Predefined bucket names for each project + * Unified bucket name for all Manacore storage + * Structure: manacore-storage/{userId}/{appName}/... */ -export const BUCKETS = { - PICTURE: 'picture-storage', - CHAT: 'chat-storage', - MANADECK: 'manadeck-storage', - NUTRIPHI: 'nutriphi-storage', - PRESI: 'presi-storage', - CALENDAR: 'calendar-storage', - CONTACTS: 'contacts-storage', - STORAGE: 'storage-storage', - MAIL: 'mail-storage', - INVENTORY: 'inventory-storage', +export const UNIFIED_BUCKET = 'manacore-storage'; + +/** + * App identifiers for folder structure within the unified bucket + */ +export const APPS = { + PICTURE: 'picture', + CHAT: 'chat', + MANADECK: 'manadeck', + NUTRIPHI: 'nutriphi', + PRESI: 'presi', + CALENDAR: 'calendar', + CONTACTS: 'contacts', + STORAGE: 'storage', + MAIL: 'mail', + INVENTORY: 'inventory', + MANACORE: 'manacore', } as const; -export type BucketName = (typeof BUCKETS)[keyof typeof BUCKETS]; +export type AppName = (typeof APPS)[keyof typeof APPS]; diff --git a/packages/shared-storage/src/utils.ts b/packages/shared-storage/src/utils.ts index 6a8612a4f..91aa3f74f 100644 --- a/packages/shared-storage/src/utils.ts +++ b/packages/shared-storage/src/utils.ts @@ -1,5 +1,6 @@ import { randomUUID } from 'crypto'; import { extname } from 'path'; +import type { AppName } from './types'; /** * Generate a unique file key with optional folder structure @@ -23,6 +24,30 @@ export function generateFileKey(filename: string, ...folders: string[]): string return key; } +/** + * Generate a storage key for the unified bucket structure + * + * @example + * generateStorageKey('user-123', 'picture', 'photo.jpg') + * // => 'user-123/picture/a1b2c3d4-e5f6-7890-abcd-ef1234567890.jpg' + * + * generateStorageKey('user-123', 'chat', 'document.pdf', 'attachments') + * // => 'user-123/chat/attachments/a1b2c3d4-e5f6-7890-abcd-ef1234567890.pdf' + */ +export function generateStorageKey( + userId: string, + appName: AppName | string, + filename: string, + ...subfolders: string[] +): string { + const ext = extname(filename); + const uuid = randomUUID(); + const file = `${uuid}${ext}`; + + const parts = [userId, appName, ...subfolders, file]; + return parts.join('/'); +} + /** * Generate a user-scoped file key *