diff --git a/MANACORE-TODOS.md b/MANACORE-TODOS.md index 42d6d0bbe..299b1d3d4 100644 --- a/MANACORE-TODOS.md +++ b/MANACORE-TODOS.md @@ -91,9 +91,9 @@ - [x] Credits-Seite: Stripe Checkout Integration - [x] Loading-States und Toast-Benachrichtigungen -**Noch offen:** +**Rechnungs-PDFs:** -- [ ] Rechnungs-PDF generieren +- [x] Stripe Invoice PDFs werden automatisch über Webhooks synchronisiert (`invoicePdfUrl`) --- @@ -167,9 +167,9 @@ Archivierte Apps (memoro, storyteller) wurden bereits entfernt. | Feature | Backend | Frontend | Priorität | | ----------------- | ------- | -------- | --------- | -| Profil bearbeiten | ✅ | ❌ | Hoch | -| Passwort ändern | ✅ | ❌ | Hoch | -| Konto löschen | ✅ | ❌ | Mittel | +| Profil bearbeiten | ✅ | ✅ | Hoch | +| Passwort ändern | ✅ | ✅ | Hoch | +| Konto löschen | ✅ | ✅ | Mittel | | Avatar hochladen | ✅ | ❌ | Niedrig | | 2FA aktivieren | ❌ | ❌ | Niedrig | @@ -186,7 +186,14 @@ Archivierte Apps (memoro, storyteller) wurden bereits entfernt. - [x] Profil-Edit Modal erstellt (`EditProfileModal.svelte`) - [x] Passwort-Ändern Dialog erstellt (`ChangePasswordModal.svelte`) - [x] Konto-Löschung mit Bestätigung (`DeleteAccountModal.svelte`) -- [ ] Avatar-Upload mit S3/MinIO Integration (noch offen) + +**Avatar-Upload (Implementiert 2026-02-13):** + +- [x] Storage-Modul für S3/MinIO (`services/mana-core-auth/src/storage/`) +- [x] Presigned URL Endpoint: `POST /api/v1/storage/avatar/upload-url` +- [x] Direct Upload Endpoint: `POST /api/v1/storage/avatar` +- [x] `manacore-storage` Bucket konfiguriert +- [ ] Frontend-Integration (EditProfileModal) noch offen --- @@ -257,7 +264,8 @@ GET /api/v1/subscriptions/invoices # Rechnungen - [x] Stripe Checkout Integration für Subscriptions - [x] Billing Portal Integration - [x] Rechnungsübersicht -- [ ] Stripe Price IDs in DB eintragen (nach Stripe-Setup) +- [x] Subscription-Plans Seed-Script erstellt (`pnpm db:seed:plans`) +- [ ] Stripe Products/Prices erstellen und ENV-Variablen setzen --- @@ -406,4 +414,4 @@ Diese Tasks können schnell erledigt werden: --- -_Zuletzt aktualisiert: 2026-02-13 (Profile + Subscription + Credits Frontend)_ +_Zuletzt aktualisiert: 2026-02-13 (Avatar Storage Backend + Subscription Plans Seed)_ diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index ab76cc4d9..dba83f7ac 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -76,6 +76,7 @@ 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/manacore-storage; mc mb --ignore-existing myminio/picture-storage; mc mb --ignore-existing myminio/chat-storage; mc mb --ignore-existing myminio/manadeck-storage; @@ -87,6 +88,7 @@ services: mc mb --ignore-existing myminio/inventory-storage; mc mb --ignore-existing myminio/planta-storage; mc mb --ignore-existing myminio/projectdoc-storage; + mc anonymous set download myminio/manacore-storage; mc anonymous set download myminio/picture-storage; mc anonymous set download myminio/planta-storage; mc anonymous set download myminio/inventory-storage; diff --git a/packages/shared-storage/src/factory.ts b/packages/shared-storage/src/factory.ts index c8a529587..b3e903b8a 100644 --- a/packages/shared-storage/src/factory.ts +++ b/packages/shared-storage/src/factory.ts @@ -66,6 +66,16 @@ export function createStorageClient( return new StorageClient(storageConfig, bucketConfig); } +/** + * Create a storage client for the Mana Core Auth project (avatars, etc.) + */ +export function createManaCoreStorage(publicUrl?: string): StorageClient { + return createStorageClient({ + name: BUCKETS.MANACORE, + publicUrl: publicUrl ?? process.env.MANACORE_STORAGE_PUBLIC_URL, + }); +} + /** * Create a storage client for the Picture project */ diff --git a/packages/shared-storage/src/index.ts b/packages/shared-storage/src/index.ts index f3ef3c5be..4fae89495 100644 --- a/packages/shared-storage/src/index.ts +++ b/packages/shared-storage/src/index.ts @@ -5,6 +5,7 @@ export { StorageClient } from './client'; export { createStorageClient, getStorageConfig, + createManaCoreStorage, createPictureStorage, createChatStorage, createManaDeckStorage, diff --git a/packages/shared-storage/src/types.ts b/packages/shared-storage/src/types.ts index 5fa3e43c2..42c10eee0 100644 --- a/packages/shared-storage/src/types.ts +++ b/packages/shared-storage/src/types.ts @@ -76,6 +76,7 @@ export interface FileInfo { * Predefined bucket names for each project */ export const BUCKETS = { + MANACORE: 'manacore-storage', PICTURE: 'picture-storage', CHAT: 'chat-storage', MANADECK: 'manadeck-storage', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 112cb4eae..3fef0c2d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -196,7 +196,7 @@ importers: version: 0.5.21 ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-loader: specifier: ^9.5.1 version: 9.5.4(typescript@5.9.3)(webpack@5.100.2) @@ -2371,7 +2371,7 @@ importers: version: 0.5.21 ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-loader: specifier: ^9.5.1 version: 9.5.4(typescript@5.9.3)(webpack@5.100.2) @@ -5656,7 +5656,7 @@ importers: version: 1.57.0 jest: specifier: ^29.0.0 - version: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + version: 29.7.0(@types/node@24.10.1) vitest: specifier: ^3.0.0 version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) @@ -5803,7 +5803,7 @@ importers: version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -5819,6 +5819,9 @@ importers: '@google/generative-ai': specifier: ^0.24.1 version: 0.24.1 + '@manacore/shared-storage': + specifier: workspace:* + version: link:../../packages/shared-storage '@nestjs/axios': specifier: ^4.0.1 version: 4.0.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2) @@ -5843,6 +5846,9 @@ importers: '@nestjs/throttler': specifier: ^6.2.1 version: 6.4.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(reflect-metadata@0.2.2) + '@types/multer': + specifier: ^2.0.0 + version: 2.0.0 axios: specifier: ^1.7.2 version: 1.13.2 @@ -5885,6 +5891,9 @@ importers: jsonwebtoken: specifier: ^9.0.2 version: 9.0.3 + multer: + specifier: ^1.4.5-lts.1 + version: 1.4.5-lts.2 nanoid: specifier: ^5.0.9 version: 5.1.6 @@ -5918,7 +5927,7 @@ importers: devDependencies: '@nestjs/cli': specifier: ^11.0.0 - version: 11.0.12(@types/node@22.19.1) + version: 11.0.12(@types/node@22.19.1)(esbuild@0.19.12) '@nestjs/schematics': specifier: ^11.0.0 version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3) @@ -5978,10 +5987,10 @@ importers: version: 7.1.4 ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-loader: specifier: ^9.5.1 - version: 9.5.4(typescript@5.9.3)(webpack@5.100.2) + version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -6090,7 +6099,7 @@ importers: version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -6280,7 +6289,7 @@ importers: version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -6353,7 +6362,7 @@ importers: version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -27388,7 +27397,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(g2vconqrtzzmzlh6ymhbjirn5e) + expo-router: 6.0.15(5ll7ovd7i5kd7vxhny3dgbs3xy) react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -28696,7 +28705,7 @@ snapshots: jest-util: 30.2.0 slash: 3.0.0 - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))': + '@jest/core@29.7.0': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -28710,7 +28719,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest-config: 29.7.0(@types/node@22.19.1) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -28731,7 +28740,7 @@ snapshots: - supports-color - ts-node - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))': + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -28745,7 +28754,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest-config: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -29406,7 +29415,7 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/cli@11.0.12(@types/node@22.19.1)': + '@nestjs/cli@11.0.12(@types/node@22.19.1)(esbuild@0.19.12)': dependencies: '@angular-devkit/core': 19.2.19(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.19(chokidar@4.0.3) @@ -29417,14 +29426,14 @@ snapshots: chokidar: 4.0.3 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.100.2) + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)) glob: 12.0.0 node-emoji: 1.11.0 ora: 5.4.1 tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 typescript: 5.9.3 - webpack: 5.100.2 + webpack: 5.100.2(esbuild@0.19.12) webpack-node-externals: 3.0.0 transitivePeerDependencies: - '@types/node' @@ -33063,6 +33072,19 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 + '@testing-library/react-native@13.3.3(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + jest-matcher-utils: 30.2.0 + picocolors: 1.1.1 + pretty-format: 30.2.0 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-test-renderer: 19.1.0(react@19.1.0) + redent: 3.0.0 + optionalDependencies: + jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + optional: true + '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: jest-matcher-utils: 30.2.0 @@ -36483,13 +36505,13 @@ snapshots: - supports-color - ts-node - create-jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + create-jest@29.7.0(@types/node@24.10.1): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest-config: 29.7.0(@types/node@24.10.1) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -39270,6 +39292,53 @@ snapshots: - react-native - supports-color + expo-router@6.0.15(5ll7ovd7i5kd7vxhny3dgbs3xy): + dependencies: + '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@expo/schema-utils': 0.1.7 + '@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.1.0) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-navigation/bottom-tabs': 7.8.6(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@react-navigation/native': 7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@react-navigation/native-stack': 7.8.0(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + client-only: 0.0.1 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) + expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-server: 1.0.4 + fast-deep-equal: 3.1.3 + invariant: 2.2.4 + nanoid: 3.3.11 + query-string: 7.1.3 + react: 19.1.0 + react-fast-compare: 3.2.2 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-native-is-edge-to-edge: 1.2.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-safe-area-context: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-screens: 4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + semver: 7.6.3 + server-only: 0.0.1 + sf-symbols-typescript: 2.1.0 + shallowequal: 1.1.0 + use-latest-callback: 0.2.6(react@19.1.0) + vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + optionalDependencies: + '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@testing-library/react-native': 13.3.3(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + react-dom: 19.1.0(react@19.1.0) + react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.19.12)) + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + - '@types/react' + - '@types/react-dom' + - supports-color + optional: true + expo-router@6.0.15(6hayu32hencph7rqfkncbd2qum): dependencies: '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) @@ -40430,6 +40499,23 @@ snapshots: typescript: 5.7.2 webpack: 5.97.1 + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)): + dependencies: + '@babel/code-frame': 7.27.1 + chalk: 4.1.2 + chokidar: 4.0.3 + cosmiconfig: 8.3.6(typescript@5.9.3) + deepmerge: 4.3.1 + fs-extra: 10.1.0 + memfs: 3.5.3 + minimatch: 3.1.2 + node-abort-controller: 3.1.1 + schema-utils: 3.3.0 + semver: 7.7.3 + tapable: 2.3.0 + typescript: 5.9.3 + webpack: 5.100.2(esbuild@0.19.12) + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)): dependencies: '@babel/code-frame': 7.27.1 @@ -40447,23 +40533,6 @@ snapshots: typescript: 5.9.3 webpack: 5.100.2(esbuild@0.27.0) - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.100.2): - dependencies: - '@babel/code-frame': 7.27.1 - chalk: 4.1.2 - chokidar: 4.0.3 - cosmiconfig: 8.3.6(typescript@5.9.3) - deepmerge: 4.3.1 - fs-extra: 10.1.0 - memfs: 3.5.3 - minimatch: 3.1.2 - node-abort-controller: 3.1.1 - schema-utils: 3.3.0 - semver: 7.7.3 - tapable: 2.3.0 - typescript: 5.9.3 - webpack: 5.100.2 - form-data-encoder@1.7.2: {} form-data@2.3.3: @@ -41875,16 +41944,16 @@ snapshots: - supports-color - ts-node - jest-cli@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + jest-cli@29.7.0(@types/node@24.10.1): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + create-jest: 29.7.0(@types/node@24.10.1) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest-config: 29.7.0(@types/node@24.10.1) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -41973,6 +42042,36 @@ snapshots: - ts-node optional: true + jest-config@29.7.0(@types/node@22.19.1): + dependencies: + '@babel/core': 7.28.5 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.28.5) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.19.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-config@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: '@babel/core': 7.28.5 @@ -42004,38 +42103,7 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): - dependencies: - '@babel/core': 7.28.5 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.28.5) - chalk: 4.1.2 - ci-info: 3.9.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 22.19.1 - ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-config@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + jest-config@29.7.0(@types/node@24.10.1): dependencies: '@babel/core': 7.28.5 '@jest/test-sequencer': 29.7.0 @@ -42061,7 +42129,6 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 24.10.1 - ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -42758,12 +42825,12 @@ snapshots: - supports-color - ts-node - jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + jest@29.7.0(@types/node@24.10.1): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest-cli: 29.7.0(@types/node@24.10.1) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -47155,6 +47222,16 @@ snapshots: webpack-sources: 3.3.3 optional: true + react-server-dom-webpack@19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.19.12)): + dependencies: + acorn-loose: 8.5.2 + neo-async: 2.6.2 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + webpack: 5.100.2(esbuild@0.19.12) + webpack-sources: 3.3.3 + optional: true + react-server-dom-webpack@19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)): dependencies: acorn-loose: 8.5.2 @@ -48665,6 +48742,17 @@ snapshots: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 + terser-webpack-plugin@5.3.14(esbuild@0.19.12)(webpack@5.100.2(esbuild@0.19.12)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + serialize-javascript: 6.0.2 + terser: 5.44.1 + webpack: 5.100.2(esbuild@0.19.12) + optionalDependencies: + esbuild: 0.19.12 + terser-webpack-plugin@5.3.14(esbuild@0.27.0)(webpack@5.100.2(esbuild@0.27.0)): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -48873,6 +48961,27 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.8 + jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.3 + type-fest: 4.41.0 + typescript: 5.9.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.28.5 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + babel-jest: 30.2.0(@babel/core@7.28.5) + esbuild: 0.19.12 + jest-util: 30.2.0 + ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 @@ -48894,25 +49003,15 @@ snapshots: esbuild: 0.27.0 jest-util: 30.2.0 - ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): + ts-loader@9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)): dependencies: - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - handlebars: 4.7.8 - jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 + chalk: 4.1.2 + enhanced-resolve: 5.18.3 + micromatch: 4.0.8 semver: 7.7.3 - type-fest: 4.41.0 + source-map: 0.7.6 typescript: 5.9.3 - yargs-parser: 21.1.1 - optionalDependencies: - '@babel/core': 7.28.5 - '@jest/transform': 30.2.0 - '@jest/types': 30.2.0 - babel-jest: 30.2.0(@babel/core@7.28.5) - jest-util: 30.2.0 + webpack: 5.100.2(esbuild@0.19.12) ts-loader@9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)): dependencies: @@ -50157,6 +50256,38 @@ snapshots: - esbuild - uglify-js + webpack@5.100.2(esbuild@0.19.12): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.15.0 + acorn-import-phases: 1.0.4(acorn@8.15.0) + browserslist: 4.28.0 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.18.3 + es-module-lexer: 1.7.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.1 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.0 + terser-webpack-plugin: 5.3.14(esbuild@0.19.12)(webpack@5.100.2(esbuild@0.19.12)) + watchpack: 2.4.4 + webpack-sources: 3.3.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + webpack@5.100.2(esbuild@0.27.0): dependencies: '@types/eslint-scope': 3.7.7 diff --git a/services/mana-core-auth/package.json b/services/mana-core-auth/package.json index cd0252ae6..c0fab6e53 100644 --- a/services/mana-core-auth/package.json +++ b/services/mana-core-auth/package.json @@ -21,10 +21,12 @@ "db:migrate": "tsx src/db/migrate.ts", "db:studio": "drizzle-kit studio", "db:seed:dev": "tsx src/db/seed-dev-user.ts", - "db:seed:oidc": "tsx src/db/seeds/seed-oidc-clients.ts" + "db:seed:oidc": "tsx src/db/seeds/seed-oidc-clients.ts", + "db:seed:plans": "tsx src/db/seeds/seed-subscription-plans.ts" }, "dependencies": { "@google/generative-ai": "^0.24.1", + "@manacore/shared-storage": "workspace:*", "@nestjs/axios": "^4.0.1", "@nestjs/common": "^10.4.15", "@nestjs/config": "^3.3.0", @@ -33,6 +35,7 @@ "@nestjs/schedule": "^4.1.2", "@nestjs/swagger": "^8.1.0", "@nestjs/throttler": "^6.2.1", + "@types/multer": "^2.0.0", "axios": "^1.7.2", "bcrypt": "^5.1.1", "better-auth": "^1.4.3", @@ -47,6 +50,7 @@ "helmet": "^8.0.0", "jose": "^6.1.2", "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", "nanoid": "^5.0.9", "nodemailer": "^7.0.12", "postgres": "^3.4.5", diff --git a/services/mana-core-auth/src/app.module.ts b/services/mana-core-auth/src/app.module.ts index 7da20621e..c89ff741c 100644 --- a/services/mana-core-auth/src/app.module.ts +++ b/services/mana-core-auth/src/app.module.ts @@ -12,6 +12,7 @@ import { FeedbackModule } from './feedback/feedback.module'; import { HealthModule } from './health/health.module'; import { ReferralsModule } from './referrals/referrals.module'; import { SettingsModule } from './settings/settings.module'; +import { StorageModule } from './storage/storage.module'; import { TagsModule } from './tags/tags.module'; import { MeModule } from './me/me.module'; import { SubscriptionsModule } from './subscriptions/subscriptions.module'; @@ -45,6 +46,7 @@ import { LoggerModule } from './common/logger'; HealthModule, ReferralsModule, SettingsModule, + StorageModule, TagsModule, MeModule, StripeModule, diff --git a/services/mana-core-auth/src/config/configuration.ts b/services/mana-core-auth/src/config/configuration.ts index aa3a852a8..7ab7dc08b 100644 --- a/services/mana-core-auth/src/config/configuration.ts +++ b/services/mana-core-auth/src/config/configuration.ts @@ -72,5 +72,9 @@ export default () => ({ geminiApiKey: env.GOOGLE_GENAI_API_KEY || '', }, + storage: { + publicUrl: env.MANACORE_STORAGE_PUBLIC_URL || '', + }, + baseUrl: env.BASE_URL || (isDevelopment() ? 'http://localhost:3001' : ''), }); diff --git a/services/mana-core-auth/src/config/env.validation.ts b/services/mana-core-auth/src/config/env.validation.ts index fa8967a35..62647724f 100644 --- a/services/mana-core-auth/src/config/env.validation.ts +++ b/services/mana-core-auth/src/config/env.validation.ts @@ -55,6 +55,9 @@ const envSchema = z.object({ // AI GOOGLE_GENAI_API_KEY: z.string().optional(), + // Storage + MANACORE_STORAGE_PUBLIC_URL: z.string().optional(), + // Base URL for callbacks BASE_URL: z.string().url().optional(), diff --git a/services/mana-core-auth/src/db/seeds/seed-subscription-plans.ts b/services/mana-core-auth/src/db/seeds/seed-subscription-plans.ts new file mode 100644 index 000000000..5370bd95c --- /dev/null +++ b/services/mana-core-auth/src/db/seeds/seed-subscription-plans.ts @@ -0,0 +1,184 @@ +/** + * Seed subscription plans with Stripe Price IDs + * + * This script creates/updates the default subscription plans in the database. + * Plans are idempotent - running multiple times won't create duplicates. + * + * Usage: + * pnpm db:seed:plans + * + * Prerequisites: + * 1. Create products and prices in Stripe Dashboard + * 2. Set STRIPE_* environment variables with the price IDs + * + * Stripe Products to create: + * - Mana Free (price: 0 EUR) + * - Mana Pro (prices: 9.99 EUR/month, 99 EUR/year) + * - Mana Enterprise (contact sales) + */ + +import 'dotenv/config'; +import postgres from 'postgres'; +import { drizzle } from 'drizzle-orm/postgres-js'; +import { eq } from 'drizzle-orm'; +import { plans } from '../schema/subscriptions.schema'; + +// Environment configuration +const DATABASE_URL = + process.env.DATABASE_URL || 'postgresql://manacore:manacore@localhost:5432/manacore_auth'; + +// Stripe Price IDs from environment (or defaults for development) +const STRIPE_CONFIG = { + // Free plan (no Stripe price needed) + FREE_PRODUCT_ID: process.env.STRIPE_FREE_PRODUCT_ID || '', + + // Pro plan + PRO_PRODUCT_ID: process.env.STRIPE_PRO_PRODUCT_ID || '', + PRO_PRICE_MONTHLY: process.env.STRIPE_PRO_PRICE_MONTHLY || '', // e.g., price_xxx + PRO_PRICE_YEARLY: process.env.STRIPE_PRO_PRICE_YEARLY || '', // e.g., price_xxx + + // Enterprise plan + ENTERPRISE_PRODUCT_ID: process.env.STRIPE_ENTERPRISE_PRODUCT_ID || '', + ENTERPRISE_PRICE_MONTHLY: process.env.STRIPE_ENTERPRISE_PRICE_MONTHLY || '', + ENTERPRISE_PRICE_YEARLY: process.env.STRIPE_ENTERPRISE_PRICE_YEARLY || '', +}; + +// Plan definitions +const PLANS = [ + { + name: 'Free', + description: 'Kostenlos starten mit grundlegenden Features', + monthlyCredits: 150, + priceMonthlyEuroCents: 0, + priceYearlyEuroCents: 0, + stripePriceIdMonthly: null, + stripePriceIdYearly: null, + stripeProductId: STRIPE_CONFIG.FREE_PRODUCT_ID || null, + features: [ + '150 Credits pro Monat', + '5 tägliche Gratis-Credits', + 'Zugang zu allen Apps', + 'Basis-Support', + ], + maxTeamMembers: null, + maxOrganizations: null, + isDefault: true, + isEnterprise: false, + sortOrder: 0, + }, + { + name: 'Pro', + description: 'Für Power-User mit mehr Credits und Features', + monthlyCredits: 2000, + priceMonthlyEuroCents: 999, // 9.99 EUR + priceYearlyEuroCents: 9900, // 99 EUR (2 months free) + stripePriceIdMonthly: STRIPE_CONFIG.PRO_PRICE_MONTHLY || null, + stripePriceIdYearly: STRIPE_CONFIG.PRO_PRICE_YEARLY || null, + stripeProductId: STRIPE_CONFIG.PRO_PRODUCT_ID || null, + features: [ + '2.000 Credits pro Monat', + '20 tägliche Gratis-Credits', + 'Prioritäts-Support', + 'Erweiterte AI-Modelle', + 'API-Zugang', + ], + maxTeamMembers: 5, + maxOrganizations: 3, + isDefault: false, + isEnterprise: false, + sortOrder: 1, + }, + { + name: 'Enterprise', + description: 'Für Teams und Unternehmen mit individuellen Anforderungen', + monthlyCredits: 10000, + priceMonthlyEuroCents: 4900, // 49 EUR + priceYearlyEuroCents: 49000, // 490 EUR (2 months free) + stripePriceIdMonthly: STRIPE_CONFIG.ENTERPRISE_PRICE_MONTHLY || null, + stripePriceIdYearly: STRIPE_CONFIG.ENTERPRISE_PRICE_YEARLY || null, + stripeProductId: STRIPE_CONFIG.ENTERPRISE_PRODUCT_ID || null, + features: [ + '10.000 Credits pro Monat', + 'Unbegrenzte tägliche Credits', + 'Dedizierter Account Manager', + 'SLA-garantierte Verfügbarkeit', + 'Custom AI-Modelle', + 'SSO / SAML Integration', + 'Admin-Dashboard', + ], + maxTeamMembers: null, // Unlimited + maxOrganizations: null, // Unlimited + isDefault: false, + isEnterprise: true, + sortOrder: 2, + }, +]; + +async function seedPlans() { + console.log('Seeding subscription plans...'); + console.log(`Database: ${DATABASE_URL.replace(/:[^@]+@/, ':***@')}`); + + const client = postgres(DATABASE_URL); + const db = drizzle(client); + + try { + for (const plan of PLANS) { + // Check if plan exists + const [existing] = await db.select().from(plans).where(eq(plans.name, plan.name)).limit(1); + + if (existing) { + // Update existing plan + await db + .update(plans) + .set({ + ...plan, + updatedAt: new Date(), + }) + .where(eq(plans.id, existing.id)); + console.log(`✓ Updated plan: ${plan.name}`); + } else { + // Insert new plan + await db.insert(plans).values({ + ...plan, + } as any); + console.log(`✓ Created plan: ${plan.name}`); + } + } + + // List all plans + const allPlans = await db.select().from(plans).orderBy(plans.sortOrder); + console.log('\nAll subscription plans:'); + console.table( + allPlans.map((p) => ({ + name: p.name, + credits: p.monthlyCredits, + monthly: `€${(p.priceMonthlyEuroCents / 100).toFixed(2)}`, + yearly: `€${(p.priceYearlyEuroCents / 100).toFixed(2)}`, + stripeMonthly: p.stripePriceIdMonthly || '(not set)', + stripeYearly: p.stripePriceIdYearly || '(not set)', + default: p.isDefault, + })) + ); + + console.log('\n✅ Subscription plans seeded successfully!'); + + if (!STRIPE_CONFIG.PRO_PRICE_MONTHLY || !STRIPE_CONFIG.PRO_PRICE_YEARLY) { + console.log('\n⚠️ Warning: Stripe Price IDs not configured.'); + console.log(' Set these environment variables:'); + console.log(' - STRIPE_PRO_PRODUCT_ID'); + console.log(' - STRIPE_PRO_PRICE_MONTHLY'); + console.log(' - STRIPE_PRO_PRICE_YEARLY'); + console.log(' - STRIPE_ENTERPRISE_PRODUCT_ID'); + console.log(' - STRIPE_ENTERPRISE_PRICE_MONTHLY'); + console.log(' - STRIPE_ENTERPRISE_PRICE_YEARLY'); + } + } catch (error) { + console.error('Error seeding plans:', error); + process.exit(1); + } finally { + await client.end(); + } +} + +// Run if called directly +seedPlans(); diff --git a/services/mana-core-auth/src/storage/index.ts b/services/mana-core-auth/src/storage/index.ts new file mode 100644 index 000000000..8a60e8b04 --- /dev/null +++ b/services/mana-core-auth/src/storage/index.ts @@ -0,0 +1,3 @@ +export { StorageModule } from './storage.module'; +export { StorageService } from './storage.service'; +export { StorageController } from './storage.controller'; diff --git a/services/mana-core-auth/src/storage/storage.controller.ts b/services/mana-core-auth/src/storage/storage.controller.ts new file mode 100644 index 000000000..e4a922543 --- /dev/null +++ b/services/mana-core-auth/src/storage/storage.controller.ts @@ -0,0 +1,113 @@ +import { + Controller, + Post, + Body, + UseGuards, + UseInterceptors, + UploadedFile, + BadRequestException, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiConsumes } from '@nestjs/swagger'; +import { StorageService } from './storage.service'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { CurrentUser } from '../common/decorators/current-user.decorator'; +import type { CurrentUserData } from '../common/decorators/current-user.decorator'; + +interface GetUploadUrlDto { + filename: string; +} + +@ApiTags('storage') +@Controller('storage') +@UseGuards(JwtAuthGuard) +@ApiBearerAuth('JWT-auth') +export class StorageController { + constructor(private readonly storageService: StorageService) {} + + /** + * Get a presigned URL for avatar upload + * + * Returns a presigned URL that the client can use to upload + * the avatar directly to S3/MinIO. This is the recommended approach + * for frontend uploads as it's more efficient. + */ + @Post('avatar/upload-url') + @ApiOperation({ + summary: 'Get presigned URL for avatar upload', + description: + 'Returns a presigned URL for direct upload to storage. Use this URL to PUT the file.', + }) + @ApiResponse({ + status: 200, + description: 'Returns presigned upload URL', + schema: { + type: 'object', + properties: { + uploadUrl: { type: 'string', description: 'PUT this URL with the file' }, + fileUrl: { type: 'string', description: 'Public URL after upload' }, + key: { type: 'string', description: 'Storage key' }, + expiresIn: { type: 'number', description: 'URL expires in seconds' }, + }, + }, + }) + @ApiResponse({ status: 400, description: 'Invalid file type or storage not configured' }) + async getAvatarUploadUrl( + @CurrentUser() user: CurrentUserData, + @Body() dto: GetUploadUrlDto + ): Promise<{ + uploadUrl: string; + fileUrl: string; + key: string; + expiresIn: number; + }> { + if (!dto.filename) { + throw new BadRequestException('filename is required'); + } + + return this.storageService.getAvatarUploadUrl(user.userId, dto.filename); + } + + /** + * Upload avatar directly (multipart/form-data) + * + * Alternative to presigned URLs. The file is uploaded to the backend + * which then uploads it to S3/MinIO. Simpler but less efficient for + * large files. + */ + @Post('avatar') + @UseInterceptors( + FileInterceptor('file', { + limits: { + fileSize: 5 * 1024 * 1024, // 5MB + }, + }) + ) + @ApiConsumes('multipart/form-data') + @ApiOperation({ + summary: 'Upload avatar directly', + description: 'Upload avatar file directly to the server', + }) + @ApiResponse({ + status: 201, + description: 'Avatar uploaded successfully', + schema: { + type: 'object', + properties: { + url: { type: 'string', description: 'Public URL of the uploaded avatar' }, + key: { type: 'string', description: 'Storage key' }, + }, + }, + }) + @ApiResponse({ status: 400, description: 'Invalid file type or size' }) + async uploadAvatar( + @CurrentUser() user: CurrentUserData, + @UploadedFile() file: Express.Multer.File + ): Promise<{ url: string; key: string }> { + if (!file) { + throw new BadRequestException('No file uploaded'); + } + + return this.storageService.uploadAvatar(user.userId, file.buffer, file.originalname); + } +} diff --git a/services/mana-core-auth/src/storage/storage.module.ts b/services/mana-core-auth/src/storage/storage.module.ts new file mode 100644 index 000000000..3b34c65d1 --- /dev/null +++ b/services/mana-core-auth/src/storage/storage.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { StorageService } from './storage.service'; +import { StorageController } from './storage.controller'; + +@Module({ + imports: [ConfigModule], + controllers: [StorageController], + providers: [StorageService], + exports: [StorageService], +}) +export class StorageModule {} diff --git a/services/mana-core-auth/src/storage/storage.service.ts b/services/mana-core-auth/src/storage/storage.service.ts new file mode 100644 index 000000000..69782c823 --- /dev/null +++ b/services/mana-core-auth/src/storage/storage.service.ts @@ -0,0 +1,176 @@ +import { Injectable, Logger, BadRequestException, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { + createManaCoreStorage, + generateUserFileKey, + getContentType, + validateFileSize, + validateFileExtension, + IMAGE_EXTENSIONS, +} from '@manacore/shared-storage'; +import type { StorageClient } from '@manacore/shared-storage'; + +const MAX_AVATAR_SIZE = 5 * 1024 * 1024; // 5MB + +@Injectable() +export class StorageService implements OnModuleInit { + private readonly logger = new Logger(StorageService.name); + private storage: StorageClient | null = null; + private readonly publicUrl: string | undefined; + + constructor(private readonly configService: ConfigService) { + this.publicUrl = this.configService.get('storage.publicUrl'); + } + + async onModuleInit() { + try { + this.storage = createManaCoreStorage(this.publicUrl); + this.logger.log('Storage service initialized'); + } catch (error) { + this.logger.warn( + 'Storage service not configured - avatar uploads will be disabled', + error instanceof Error ? error.message : undefined + ); + } + } + + /** + * Check if storage is available + */ + isAvailable(): boolean { + return this.storage !== null; + } + + /** + * Generate a presigned URL for avatar upload + * + * @param userId - User ID + * @param filename - Original filename + * @returns Presigned upload URL and the final file URL + */ + async getAvatarUploadUrl( + userId: string, + filename: string + ): Promise<{ + uploadUrl: string; + fileUrl: string; + key: string; + expiresIn: number; + }> { + if (!this.storage) { + throw new BadRequestException('Storage service is not configured'); + } + + // Validate file extension + const ext = filename.split('.').pop()?.toLowerCase(); + if (!ext || !validateFileExtension(filename, IMAGE_EXTENSIONS)) { + throw new BadRequestException(`Invalid file type. Allowed: ${IMAGE_EXTENSIONS.join(', ')}`); + } + + // Generate unique key for avatar + const key = `avatars/${userId}/${Date.now()}.${ext}`; + const contentType = getContentType(filename); + + // Get presigned upload URL (1 hour expiry) + const expiresIn = 3600; + const uploadUrl = await this.storage.getUploadUrl(key, { + expiresIn, + }); + + // Construct the final public URL + const fileUrl = await this.getPublicUrl(key); + + this.logger.debug('Generated avatar upload URL', { userId, key }); + + return { + uploadUrl, + fileUrl, + key, + expiresIn, + }; + } + + /** + * Upload avatar directly (for server-side uploads) + * + * @param userId - User ID + * @param buffer - File buffer + * @param filename - Original filename + * @returns Public URL of the uploaded avatar + */ + async uploadAvatar( + userId: string, + buffer: Buffer, + filename: string + ): Promise<{ url: string; key: string }> { + if (!this.storage) { + throw new BadRequestException('Storage service is not configured'); + } + + // Validate file extension + if (!validateFileExtension(filename, IMAGE_EXTENSIONS)) { + throw new BadRequestException(`Invalid file type. Allowed: ${IMAGE_EXTENSIONS.join(', ')}`); + } + + // Validate file size + if (!validateFileSize(buffer.length, MAX_AVATAR_SIZE)) { + throw new BadRequestException( + `File too large. Maximum size: ${MAX_AVATAR_SIZE / 1024 / 1024}MB` + ); + } + + // Generate unique key for avatar + const ext = filename.split('.').pop()?.toLowerCase() || 'jpg'; + const key = `avatars/${userId}/${Date.now()}.${ext}`; + + // Upload file + const result = await this.storage.upload(key, buffer, { + contentType: getContentType(filename), + public: true, + cacheControl: 'public, max-age=31536000', // 1 year cache + }); + + const url = result.url || (await this.getPublicUrl(key)); + + this.logger.log('Avatar uploaded', { userId, key }); + + return { url, key }; + } + + /** + * Delete avatar + * + * @param key - Storage key of the avatar + */ + async deleteAvatar(key: string): Promise { + if (!this.storage) { + throw new BadRequestException('Storage service is not configured'); + } + + await this.storage.delete(key); + this.logger.log('Avatar deleted', { key }); + } + + /** + * Get public URL for a key + */ + private async getPublicUrl(key: string): Promise { + if (!this.storage) { + throw new BadRequestException('Storage service is not configured'); + } + + // If we have a configured public URL, use it + if (this.publicUrl) { + return `${this.publicUrl}/${key}`; + } + + // Check if the storage has a public URL configured + const publicUrl = this.storage.getPublicUrl(key); + if (publicUrl) { + return publicUrl; + } + + // Otherwise, get a presigned URL for reading + return this.storage.getDownloadUrl(key, { expiresIn: 86400 * 365 }); // 1 year + } +}