mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
feat: add email service and storage module + fix runtime env vars
## Runtime Environment Fix - Updated all web app hooks.server.ts to use $env/dynamic/private - This allows Docker containers to inject env vars at runtime - Updated docker-compose.staging.yml with HTTPS staging domains - Fixes Mixed Content errors when accessing staging via domains ## New Features - Added email service to mana-core-auth for sending emails - Added storage module to chat backend 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
6239cc7749
commit
3fa7b027aa
17 changed files with 1225 additions and 78 deletions
|
|
@ -57,6 +57,12 @@ STRIPE_SECRET_KEY=sk_test_YOUR_KEY
|
||||||
STRIPE_PUBLISHABLE_KEY=pk_test_YOUR_KEY
|
STRIPE_PUBLISHABLE_KEY=pk_test_YOUR_KEY
|
||||||
STRIPE_WEBHOOK_SECRET=whsec_YOUR_SECRET
|
STRIPE_WEBHOOK_SECRET=whsec_YOUR_SECRET
|
||||||
|
|
||||||
|
# Email (Brevo/SendinBlue) - GDPR compliant EU provider
|
||||||
|
# Get your API key from https://app.brevo.com/settings/keys/api
|
||||||
|
BREVO_API_KEY=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
|
||||||
|
BREVO_FROM_EMAIL=noreply@manacore.app
|
||||||
|
BREVO_FROM_NAME=Mana Core
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# CHAT PROJECT
|
# CHAT PROJECT
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
|
||||||
|
|
@ -27,15 +27,18 @@
|
||||||
"@google/generative-ai": "^0.24.1",
|
"@google/generative-ai": "^0.24.1",
|
||||||
"@manacore/shared-errors": "workspace:*",
|
"@manacore/shared-errors": "workspace:*",
|
||||||
"@manacore/shared-nestjs-auth": "workspace:*",
|
"@manacore/shared-nestjs-auth": "workspace:*",
|
||||||
|
"@manacore/shared-storage": "workspace:*",
|
||||||
"@nestjs/common": "^10.4.15",
|
"@nestjs/common": "^10.4.15",
|
||||||
"@nestjs/config": "^3.3.0",
|
"@nestjs/config": "^3.3.0",
|
||||||
"@nestjs/core": "^10.4.15",
|
"@nestjs/core": "^10.4.15",
|
||||||
"@nestjs/platform-express": "^10.4.15",
|
"@nestjs/platform-express": "^10.4.15",
|
||||||
|
"@types/multer": "^1.4.11",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"drizzle-kit": "^0.30.2",
|
"drizzle-kit": "^0.30.2",
|
||||||
"drizzle-orm": "^0.38.3",
|
"drizzle-orm": "^0.38.3",
|
||||||
|
"multer": "^1.4.5-lts.1",
|
||||||
"openai": "^4.77.0",
|
"openai": "^4.77.0",
|
||||||
"postgres": "^3.4.5",
|
"postgres": "^3.4.5",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { SpaceModule } from './space/space.module';
|
||||||
import { DocumentModule } from './document/document.module';
|
import { DocumentModule } from './document/document.module';
|
||||||
import { ModelModule } from './model/model.module';
|
import { ModelModule } from './model/model.module';
|
||||||
import { HealthModule } from './health/health.module';
|
import { HealthModule } from './health/health.module';
|
||||||
|
import { StorageModule } from './storage/storage.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -23,6 +24,7 @@ import { HealthModule } from './health/health.module';
|
||||||
DocumentModule,
|
DocumentModule,
|
||||||
ModelModule,
|
ModelModule,
|
||||||
HealthModule,
|
HealthModule,
|
||||||
|
StorageModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|
|
||||||
3
apps/chat/apps/backend/src/storage/index.ts
Normal file
3
apps/chat/apps/backend/src/storage/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './storage.module';
|
||||||
|
export * from './storage.service';
|
||||||
|
export * from './storage.controller';
|
||||||
137
apps/chat/apps/backend/src/storage/storage.controller.ts
Normal file
137
apps/chat/apps/backend/src/storage/storage.controller.ts
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Post,
|
||||||
|
Get,
|
||||||
|
Delete,
|
||||||
|
Param,
|
||||||
|
Body,
|
||||||
|
UseInterceptors,
|
||||||
|
UploadedFile,
|
||||||
|
BadRequestException,
|
||||||
|
NotFoundException,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
|
import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth';
|
||||||
|
import { StorageService } from './storage.service';
|
||||||
|
|
||||||
|
interface PresignedUploadRequest {
|
||||||
|
filename: string;
|
||||||
|
folder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Controller('api/storage')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
export class StorageController {
|
||||||
|
constructor(private readonly storageService: StorageService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload a file directly
|
||||||
|
*/
|
||||||
|
@Post('upload')
|
||||||
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
|
async uploadFile(
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
@UploadedFile() file: Express.Multer.File,
|
||||||
|
@Body('folder') folder?: string
|
||||||
|
) {
|
||||||
|
if (!file) {
|
||||||
|
throw new BadRequestException('No file provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.storageService.uploadFile(
|
||||||
|
user.userId,
|
||||||
|
file.originalname,
|
||||||
|
file.buffer,
|
||||||
|
{
|
||||||
|
folder,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a presigned URL for client-side upload
|
||||||
|
*/
|
||||||
|
@Post('presigned-upload')
|
||||||
|
async getPresignedUpload(
|
||||||
|
@CurrentUser() user: CurrentUserData,
|
||||||
|
@Body() body: PresignedUploadRequest
|
||||||
|
) {
|
||||||
|
if (!body.filename) {
|
||||||
|
throw new BadRequestException('Filename is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.storageService.getPresignedUploadUrl(user.userId, body.filename, {
|
||||||
|
folder: body.folder,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a presigned URL for downloading
|
||||||
|
*/
|
||||||
|
@Get('download/:key(*)')
|
||||||
|
async getDownloadUrl(@CurrentUser() user: CurrentUserData, @Param('key') key: string) {
|
||||||
|
// Ensure user can only access their own files
|
||||||
|
if (!key.startsWith(`users/${user.userId}/`)) {
|
||||||
|
throw new NotFoundException('File not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = await this.storageService.fileExists(key);
|
||||||
|
if (!exists) {
|
||||||
|
throw new NotFoundException('File not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = await this.storageService.getPresignedDownloadUrl(key);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: { url },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a file
|
||||||
|
*/
|
||||||
|
@Delete(':key(*)')
|
||||||
|
async deleteFile(@CurrentUser() user: CurrentUserData, @Param('key') key: string) {
|
||||||
|
// Ensure user can only delete their own files
|
||||||
|
if (!key.startsWith(`users/${user.userId}/`)) {
|
||||||
|
throw new NotFoundException('File not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = await this.storageService.fileExists(key);
|
||||||
|
if (!exists) {
|
||||||
|
throw new NotFoundException('File not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.storageService.deleteFile(key);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'File deleted',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List user's files
|
||||||
|
*/
|
||||||
|
@Get('list')
|
||||||
|
async listFiles(@CurrentUser() user: CurrentUserData, @Body('folder') folder?: string) {
|
||||||
|
const files = await this.storageService.listUserFiles(user.userId, folder);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: { files },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
10
apps/chat/apps/backend/src/storage/storage.module.ts
Normal file
10
apps/chat/apps/backend/src/storage/storage.module.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { StorageService } from './storage.service';
|
||||||
|
import { StorageController } from './storage.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [StorageController],
|
||||||
|
providers: [StorageService],
|
||||||
|
exports: [StorageService],
|
||||||
|
})
|
||||||
|
export class StorageModule {}
|
||||||
152
apps/chat/apps/backend/src/storage/storage.service.ts
Normal file
152
apps/chat/apps/backend/src/storage/storage.service.ts
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
createChatStorage,
|
||||||
|
generateUserFileKey,
|
||||||
|
getContentType,
|
||||||
|
validateFileSize,
|
||||||
|
validateFileExtension,
|
||||||
|
IMAGE_EXTENSIONS,
|
||||||
|
DOCUMENT_EXTENSIONS,
|
||||||
|
AUDIO_EXTENSIONS,
|
||||||
|
} from '@manacore/shared-storage';
|
||||||
|
import type { StorageClient, UploadResult } from '@manacore/shared-storage';
|
||||||
|
|
||||||
|
export interface FileUploadResult {
|
||||||
|
key: string;
|
||||||
|
url?: string;
|
||||||
|
contentType: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PresignedUploadData {
|
||||||
|
uploadUrl: string;
|
||||||
|
key: string;
|
||||||
|
expiresIn: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
|
||||||
|
const ALLOWED_EXTENSIONS = [...IMAGE_EXTENSIONS, ...DOCUMENT_EXTENSIONS, ...AUDIO_EXTENSIONS];
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class StorageService {
|
||||||
|
private readonly logger = new Logger(StorageService.name);
|
||||||
|
private storage: StorageClient | null = null;
|
||||||
|
|
||||||
|
private getStorage(): StorageClient {
|
||||||
|
if (!this.storage) {
|
||||||
|
this.storage = createChatStorage();
|
||||||
|
}
|
||||||
|
return this.storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload a file to storage
|
||||||
|
*/
|
||||||
|
async uploadFile(
|
||||||
|
userId: string,
|
||||||
|
filename: string,
|
||||||
|
data: Buffer,
|
||||||
|
options?: { folder?: string; public?: boolean }
|
||||||
|
): Promise<FileUploadResult> {
|
||||||
|
// Validate file size (MAX_FILE_SIZE is in bytes)
|
||||||
|
if (!validateFileSize(data.length, MAX_FILE_SIZE / (1024 * 1024))) {
|
||||||
|
throw new Error(`File size exceeds maximum allowed (${MAX_FILE_SIZE / (1024 * 1024)}MB)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file extension
|
||||||
|
if (!validateFileExtension(filename, ALLOWED_EXTENSIONS)) {
|
||||||
|
throw new Error(
|
||||||
|
`File type not allowed. Allowed extensions: ${ALLOWED_EXTENSIONS.join(', ')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = getContentType(filename);
|
||||||
|
const key = generateUserFileKey(userId, filename, options?.folder);
|
||||||
|
|
||||||
|
const storage = this.getStorage();
|
||||||
|
const result: UploadResult = await storage.upload(key, data, {
|
||||||
|
contentType,
|
||||||
|
public: options?.public ?? false,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`File uploaded: ${key} (${data.length} bytes)`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: result.key,
|
||||||
|
url: result.url,
|
||||||
|
contentType,
|
||||||
|
size: data.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a presigned URL for uploading (client-side upload)
|
||||||
|
*/
|
||||||
|
async getPresignedUploadUrl(
|
||||||
|
userId: string,
|
||||||
|
filename: string,
|
||||||
|
options?: { folder?: string; expiresIn?: number }
|
||||||
|
): Promise<PresignedUploadData> {
|
||||||
|
// Validate file extension
|
||||||
|
if (!validateFileExtension(filename, ALLOWED_EXTENSIONS)) {
|
||||||
|
throw new Error(
|
||||||
|
`File type not allowed. Allowed extensions: ${ALLOWED_EXTENSIONS.join(', ')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = generateUserFileKey(userId, filename, options?.folder);
|
||||||
|
const expiresIn = options?.expiresIn ?? 3600; // 1 hour default
|
||||||
|
|
||||||
|
const storage = this.getStorage();
|
||||||
|
const uploadUrl = await storage.getUploadUrl(key, { expiresIn });
|
||||||
|
|
||||||
|
return {
|
||||||
|
uploadUrl,
|
||||||
|
key,
|
||||||
|
expiresIn,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a presigned URL for downloading
|
||||||
|
*/
|
||||||
|
async getPresignedDownloadUrl(key: string, expiresIn = 3600): Promise<string> {
|
||||||
|
const storage = this.getStorage();
|
||||||
|
return storage.getDownloadUrl(key, { expiresIn });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download a file from storage
|
||||||
|
*/
|
||||||
|
async downloadFile(key: string): Promise<Buffer> {
|
||||||
|
const storage = this.getStorage();
|
||||||
|
return storage.download(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a file from storage
|
||||||
|
*/
|
||||||
|
async deleteFile(key: string): Promise<void> {
|
||||||
|
const storage = this.getStorage();
|
||||||
|
await storage.delete(key);
|
||||||
|
this.logger.log(`File deleted: ${key}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a file exists
|
||||||
|
*/
|
||||||
|
async fileExists(key: string): Promise<boolean> {
|
||||||
|
const storage = this.getStorage();
|
||||||
|
return storage.exists(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List files for a user
|
||||||
|
*/
|
||||||
|
async listUserFiles(userId: string, folder?: string): Promise<string[]> {
|
||||||
|
const storage = this.getStorage();
|
||||||
|
const prefix = folder ? `users/${userId}/${folder}/` : `users/${userId}/`;
|
||||||
|
const files = await storage.list(prefix);
|
||||||
|
return files.map((f) => f.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
287
pnpm-lock.yaml
generated
287
pnpm-lock.yaml
generated
|
|
@ -325,6 +325,9 @@ importers:
|
||||||
'@manacore/shared-nestjs-auth':
|
'@manacore/shared-nestjs-auth':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../../packages/shared-nestjs-auth
|
version: link:../../../../packages/shared-nestjs-auth
|
||||||
|
'@manacore/shared-storage':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../../../packages/shared-storage
|
||||||
'@nestjs/common':
|
'@nestjs/common':
|
||||||
specifier: ^10.4.15
|
specifier: ^10.4.15
|
||||||
version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
|
@ -337,6 +340,9 @@ importers:
|
||||||
'@nestjs/platform-express':
|
'@nestjs/platform-express':
|
||||||
specifier: ^10.4.15
|
specifier: ^10.4.15
|
||||||
version: 10.4.20(@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)
|
version: 10.4.20(@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)
|
||||||
|
'@types/multer':
|
||||||
|
specifier: ^1.4.11
|
||||||
|
version: 1.4.13
|
||||||
class-transformer:
|
class-transformer:
|
||||||
specifier: ^0.5.1
|
specifier: ^0.5.1
|
||||||
version: 0.5.1
|
version: 0.5.1
|
||||||
|
|
@ -352,6 +358,9 @@ importers:
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.38.3
|
specifier: ^0.38.3
|
||||||
version: 0.38.4(@opentelemetry/api@1.9.0)(@types/react@19.2.7)(expo-sqlite@15.2.14(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))(kysely@0.28.8)(postgres@3.4.7)(react@19.1.0)
|
version: 0.38.4(@opentelemetry/api@1.9.0)(@types/react@19.2.7)(expo-sqlite@15.2.14(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))(kysely@0.28.8)(postgres@3.4.7)(react@19.1.0)
|
||||||
|
multer:
|
||||||
|
specifier: ^1.4.5-lts.1
|
||||||
|
version: 1.4.5-lts.2
|
||||||
openai:
|
openai:
|
||||||
specifier: ^4.77.0
|
specifier: ^4.77.0
|
||||||
version: 4.104.0(ws@8.18.3)(zod@3.25.76)
|
version: 4.104.0(ws@8.18.3)(zod@3.25.76)
|
||||||
|
|
@ -367,7 +376,7 @@ importers:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@nestjs/cli':
|
'@nestjs/cli':
|
||||||
specifier: ^10.4.9
|
specifier: ^10.4.9
|
||||||
version: 10.4.9(esbuild@0.27.0)
|
version: 10.4.9(esbuild@0.19.12)
|
||||||
'@nestjs/schematics':
|
'@nestjs/schematics':
|
||||||
specifier: ^10.2.3
|
specifier: ^10.2.3
|
||||||
version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3)
|
version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3)
|
||||||
|
|
@ -400,7 +409,7 @@ importers:
|
||||||
version: 0.5.21
|
version: 0.5.21
|
||||||
ts-loader:
|
ts-loader:
|
||||||
specifier: ^9.5.1
|
specifier: ^9.5.1
|
||||||
version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0))
|
version: 9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12))
|
||||||
ts-node:
|
ts-node:
|
||||||
specifier: ^10.9.2
|
specifier: ^10.9.2
|
||||||
version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3)
|
version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3)
|
||||||
|
|
@ -3454,7 +3463,7 @@ importers:
|
||||||
version: 9.39.1
|
version: 9.39.1
|
||||||
'@nestjs/cli':
|
'@nestjs/cli':
|
||||||
specifier: ^10.4.9
|
specifier: ^10.4.9
|
||||||
version: 10.4.9(esbuild@0.19.12)
|
version: 10.4.9(esbuild@0.27.0)
|
||||||
'@nestjs/schematics':
|
'@nestjs/schematics':
|
||||||
specifier: ^10.2.3
|
specifier: ^10.2.3
|
||||||
version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3)
|
version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3)
|
||||||
|
|
@ -3490,7 +3499,7 @@ importers:
|
||||||
version: 0.5.21
|
version: 0.5.21
|
||||||
ts-loader:
|
ts-loader:
|
||||||
specifier: ^9.5.1
|
specifier: ^9.5.1
|
||||||
version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12))
|
version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0))
|
||||||
ts-node:
|
ts-node:
|
||||||
specifier: ^10.9.2
|
specifier: ^10.9.2
|
||||||
version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3)
|
version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3)
|
||||||
|
|
@ -4189,6 +4198,9 @@ importers:
|
||||||
|
|
||||||
services/mana-core-auth:
|
services/mana-core-auth:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@getbrevo/brevo':
|
||||||
|
specifier: ^3.0.1
|
||||||
|
version: 3.0.1
|
||||||
'@google/generative-ai':
|
'@google/generative-ai':
|
||||||
specifier: ^0.24.1
|
specifier: ^0.24.1
|
||||||
version: 0.24.1
|
version: 0.24.1
|
||||||
|
|
@ -6817,6 +6829,9 @@ packages:
|
||||||
'@formatjs/intl-localematcher@0.6.2':
|
'@formatjs/intl-localematcher@0.6.2':
|
||||||
resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==}
|
resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==}
|
||||||
|
|
||||||
|
'@getbrevo/brevo@3.0.1':
|
||||||
|
resolution: {integrity: sha512-BS5hlgb9qPHhXqjV+VbEOciygnsEVZV8BgoX+JYpD+I+R9u3U05y/euqdmk8nATfcEUCUlQq+aWdWOWTF4cEjQ==}
|
||||||
|
|
||||||
'@google/genai@1.30.0':
|
'@google/genai@1.30.0':
|
||||||
resolution: {integrity: sha512-3MRcgczBFbUat1wIlZoLJ0vCCfXgm7Qxjh59cZi2X08RgWLtm9hKOspzp7TOg1TV2e26/MLxR2GR5yD5GmBV2w==}
|
resolution: {integrity: sha512-3MRcgczBFbUat1wIlZoLJ0vCCfXgm7Qxjh59cZi2X08RgWLtm9hKOspzp7TOg1TV2e26/MLxR2GR5yD5GmBV2w==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
@ -10736,6 +10751,9 @@ packages:
|
||||||
blake3-wasm@2.1.5:
|
blake3-wasm@2.1.5:
|
||||||
resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==}
|
resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==}
|
||||||
|
|
||||||
|
bluebird@3.7.2:
|
||||||
|
resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
|
||||||
|
|
||||||
blurhash@2.0.5:
|
blurhash@2.0.5:
|
||||||
resolution: {integrity: sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==}
|
resolution: {integrity: sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==}
|
||||||
|
|
||||||
|
|
@ -17286,6 +17304,9 @@ packages:
|
||||||
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
||||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||||
|
|
||||||
|
rewire@7.0.0:
|
||||||
|
resolution: {integrity: sha512-DyyNyzwMtGYgu0Zl/ya0PR/oaunM+VuCuBxCuhYJHHaV0V+YvYa3bBGxb5OZ71vndgmp1pYY8F4YOwQo1siRGw==}
|
||||||
|
|
||||||
rfdc@1.4.1:
|
rfdc@1.4.1:
|
||||||
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||||
|
|
||||||
|
|
@ -22294,7 +22315,7 @@ snapshots:
|
||||||
wrap-ansi: 7.0.0
|
wrap-ansi: 7.0.0
|
||||||
ws: 8.18.3
|
ws: 8.18.3
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
expo-router: 6.0.15(dux2nvtiztnejw7mxzfaajqvh4)
|
expo-router: 6.0.15(nttrd3tw67nnyhowcwgdzipb5e)
|
||||||
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)
|
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@modelcontextprotocol/sdk'
|
- '@modelcontextprotocol/sdk'
|
||||||
|
|
@ -23044,6 +23065,15 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@getbrevo/brevo@3.0.1':
|
||||||
|
dependencies:
|
||||||
|
axios: 1.13.2
|
||||||
|
bluebird: 3.7.2
|
||||||
|
rewire: 7.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- debug
|
||||||
|
- supports-color
|
||||||
|
|
||||||
'@google/genai@1.30.0':
|
'@google/genai@1.30.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
google-auth-library: 10.5.0
|
google-auth-library: 10.5.0
|
||||||
|
|
@ -23599,6 +23629,43 @@ snapshots:
|
||||||
- supports-color
|
- supports-color
|
||||||
- ts-node
|
- ts-node
|
||||||
|
|
||||||
|
'@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))':
|
||||||
|
dependencies:
|
||||||
|
'@jest/console': 30.2.0
|
||||||
|
'@jest/pattern': 30.0.1
|
||||||
|
'@jest/reporters': 30.2.0
|
||||||
|
'@jest/test-result': 30.2.0
|
||||||
|
'@jest/transform': 30.2.0
|
||||||
|
'@jest/types': 30.2.0
|
||||||
|
'@types/node': 22.19.1
|
||||||
|
ansi-escapes: 4.3.2
|
||||||
|
chalk: 4.1.2
|
||||||
|
ci-info: 4.3.1
|
||||||
|
exit-x: 0.2.2
|
||||||
|
graceful-fs: 4.2.11
|
||||||
|
jest-changed-files: 30.2.0
|
||||||
|
jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
||||||
|
jest-haste-map: 30.2.0
|
||||||
|
jest-message-util: 30.2.0
|
||||||
|
jest-regex-util: 30.0.1
|
||||||
|
jest-resolve: 30.2.0
|
||||||
|
jest-resolve-dependencies: 30.2.0
|
||||||
|
jest-runner: 30.2.0
|
||||||
|
jest-runtime: 30.2.0
|
||||||
|
jest-snapshot: 30.2.0
|
||||||
|
jest-util: 30.2.0
|
||||||
|
jest-validate: 30.2.0
|
||||||
|
jest-watcher: 30.2.0
|
||||||
|
micromatch: 4.0.8
|
||||||
|
pretty-format: 30.2.0
|
||||||
|
slash: 3.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- babel-plugin-macros
|
||||||
|
- esbuild-register
|
||||||
|
- supports-color
|
||||||
|
- ts-node
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))':
|
'@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jest/console': 30.2.0
|
'@jest/console': 30.2.0
|
||||||
|
|
@ -26660,6 +26727,19 @@ snapshots:
|
||||||
jest: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))
|
jest: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(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: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(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@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)))(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)':
|
'@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)))(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:
|
dependencies:
|
||||||
jest-matcher-utils: 30.2.0
|
jest-matcher-utils: 30.2.0
|
||||||
|
|
@ -29154,6 +29234,8 @@ snapshots:
|
||||||
|
|
||||||
blake3-wasm@2.1.5: {}
|
blake3-wasm@2.1.5: {}
|
||||||
|
|
||||||
|
bluebird@3.7.2: {}
|
||||||
|
|
||||||
blurhash@2.0.5: {}
|
blurhash@2.0.5: {}
|
||||||
|
|
||||||
body-parser@1.20.3:
|
body-parser@1.20.3:
|
||||||
|
|
@ -32550,6 +32632,53 @@ snapshots:
|
||||||
- supports-color
|
- supports-color
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
expo-router@6.0.15(nttrd3tw67nnyhowcwgdzipb5e):
|
||||||
|
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@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(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(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.97.1(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(qjp3usx4acoq47dkosl6pmu254):
|
expo-router@6.0.15(qjp3usx4acoq47dkosl6pmu254):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@expo/metro-runtime': 6.1.2(expo@54.0.13)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
'@expo/metro-runtime': 6.1.2(expo@54.0.13)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||||
|
|
@ -34673,6 +34802,26 @@ snapshots:
|
||||||
- ts-node
|
- ts-node
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
jest-cli@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)):
|
||||||
|
dependencies:
|
||||||
|
'@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
||||||
|
'@jest/test-result': 30.2.0
|
||||||
|
'@jest/types': 30.2.0
|
||||||
|
chalk: 4.1.2
|
||||||
|
exit-x: 0.2.2
|
||||||
|
import-local: 3.2.0
|
||||||
|
jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
||||||
|
jest-util: 30.2.0
|
||||||
|
jest-validate: 30.2.0
|
||||||
|
yargs: 17.7.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/node'
|
||||||
|
- babel-plugin-macros
|
||||||
|
- esbuild-register
|
||||||
|
- supports-color
|
||||||
|
- ts-node
|
||||||
|
optional: true
|
||||||
|
|
||||||
jest-cli@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)):
|
jest-cli@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)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
'@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
||||||
|
|
@ -34858,6 +35007,41 @@ snapshots:
|
||||||
- supports-color
|
- supports-color
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)):
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.28.5
|
||||||
|
'@jest/get-type': 30.1.0
|
||||||
|
'@jest/pattern': 30.0.1
|
||||||
|
'@jest/test-sequencer': 30.2.0
|
||||||
|
'@jest/types': 30.2.0
|
||||||
|
babel-jest: 30.2.0(@babel/core@7.28.5)
|
||||||
|
chalk: 4.1.2
|
||||||
|
ci-info: 4.3.1
|
||||||
|
deepmerge: 4.3.1
|
||||||
|
glob: 10.5.0
|
||||||
|
graceful-fs: 4.2.11
|
||||||
|
jest-circus: 30.2.0
|
||||||
|
jest-docblock: 30.2.0
|
||||||
|
jest-environment-node: 30.2.0
|
||||||
|
jest-regex-util: 30.0.1
|
||||||
|
jest-resolve: 30.2.0
|
||||||
|
jest-runner: 30.2.0
|
||||||
|
jest-util: 30.2.0
|
||||||
|
jest-validate: 30.2.0
|
||||||
|
micromatch: 4.0.8
|
||||||
|
parse-json: 5.2.0
|
||||||
|
pretty-format: 30.2.0
|
||||||
|
slash: 3.0.0
|
||||||
|
strip-json-comments: 3.1.1
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/node': 22.19.1
|
||||||
|
esbuild-register: 3.6.0(esbuild@0.19.12)
|
||||||
|
ts-node: 10.9.2(@types/node@22.19.1)(typescript@5.9.3)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- babel-plugin-macros
|
||||||
|
- supports-color
|
||||||
|
optional: true
|
||||||
|
|
||||||
jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)):
|
jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
|
|
@ -35527,6 +35711,20 @@ snapshots:
|
||||||
- ts-node
|
- ts-node
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)):
|
||||||
|
dependencies:
|
||||||
|
'@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
||||||
|
'@jest/types': 30.2.0
|
||||||
|
import-local: 3.2.0
|
||||||
|
jest-cli: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/node'
|
||||||
|
- babel-plugin-macros
|
||||||
|
- esbuild-register
|
||||||
|
- supports-color
|
||||||
|
- ts-node
|
||||||
|
optional: true
|
||||||
|
|
||||||
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)):
|
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)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
'@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
||||||
|
|
@ -39298,6 +39496,16 @@ snapshots:
|
||||||
webpack: 5.100.2(esbuild@0.27.0)
|
webpack: 5.100.2(esbuild@0.27.0)
|
||||||
webpack-sources: 3.3.3
|
webpack-sources: 3.3.3
|
||||||
|
|
||||||
|
react-server-dom-webpack@19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.97.1(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.97.1(esbuild@0.19.12)
|
||||||
|
webpack-sources: 3.3.3
|
||||||
|
optional: true
|
||||||
|
|
||||||
react-style-singleton@2.2.3(@types/react@19.2.7)(react@19.1.0):
|
react-style-singleton@2.2.3(@types/react@19.2.7)(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
get-nonce: 1.0.1
|
get-nonce: 1.0.1
|
||||||
|
|
@ -39634,6 +39842,12 @@ snapshots:
|
||||||
|
|
||||||
reusify@1.1.0: {}
|
reusify@1.1.0: {}
|
||||||
|
|
||||||
|
rewire@7.0.0:
|
||||||
|
dependencies:
|
||||||
|
eslint: 8.57.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
rfdc@1.4.1: {}
|
rfdc@1.4.1: {}
|
||||||
|
|
||||||
rimraf@2.6.3:
|
rimraf@2.6.3:
|
||||||
|
|
@ -40549,17 +40763,6 @@ snapshots:
|
||||||
ansi-escapes: 4.3.2
|
ansi-escapes: 4.3.2
|
||||||
supports-hyperlinks: 2.3.0
|
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.19.12)(webpack@5.97.1(esbuild@0.19.12)):
|
terser-webpack-plugin@5.3.14(esbuild@0.19.12)(webpack@5.97.1(esbuild@0.19.12)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
|
|
@ -40804,16 +41007,6 @@ snapshots:
|
||||||
babel-jest: 30.2.0(@babel/core@7.28.5)
|
babel-jest: 30.2.0(@babel/core@7.28.5)
|
||||||
jest-util: 30.2.0
|
jest-util: 30.2.0
|
||||||
|
|
||||||
ts-loader@9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)):
|
|
||||||
dependencies:
|
|
||||||
chalk: 4.1.2
|
|
||||||
enhanced-resolve: 5.18.3
|
|
||||||
micromatch: 4.0.8
|
|
||||||
semver: 7.7.3
|
|
||||||
source-map: 0.7.6
|
|
||||||
typescript: 5.9.3
|
|
||||||
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)):
|
ts-loader@9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
|
|
@ -40834,6 +41027,16 @@ snapshots:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
webpack: 5.100.2
|
webpack: 5.100.2
|
||||||
|
|
||||||
|
ts-loader@9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12)):
|
||||||
|
dependencies:
|
||||||
|
chalk: 4.1.2
|
||||||
|
enhanced-resolve: 5.18.3
|
||||||
|
micromatch: 4.0.8
|
||||||
|
semver: 7.7.3
|
||||||
|
source-map: 0.7.6
|
||||||
|
typescript: 5.9.3
|
||||||
|
webpack: 5.97.1(esbuild@0.19.12)
|
||||||
|
|
||||||
ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3):
|
ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@cspotcode/source-map-support': 0.8.1
|
'@cspotcode/source-map-support': 0.8.1
|
||||||
|
|
@ -41803,38 +42006,6 @@ snapshots:
|
||||||
- esbuild
|
- esbuild
|
||||||
- uglify-js
|
- 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):
|
webpack@5.100.2(esbuild@0.27.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/eslint-scope': 3.7.7
|
'@types/eslint-scope': 3.7.7
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,9 @@ const APP_CONFIGS = [
|
||||||
STRIPE_SECRET_KEY: (env) => env.STRIPE_SECRET_KEY,
|
STRIPE_SECRET_KEY: (env) => env.STRIPE_SECRET_KEY,
|
||||||
STRIPE_PUBLISHABLE_KEY: (env) => env.STRIPE_PUBLISHABLE_KEY,
|
STRIPE_PUBLISHABLE_KEY: (env) => env.STRIPE_PUBLISHABLE_KEY,
|
||||||
STRIPE_WEBHOOK_SECRET: (env) => env.STRIPE_WEBHOOK_SECRET,
|
STRIPE_WEBHOOK_SECRET: (env) => env.STRIPE_WEBHOOK_SECRET,
|
||||||
|
BREVO_API_KEY: (env) => env.BREVO_API_KEY || '',
|
||||||
|
BREVO_FROM_EMAIL: (env) => env.BREVO_FROM_EMAIL || 'noreply@manacore.app',
|
||||||
|
BREVO_FROM_NAME: (env) => env.BREVO_FROM_NAME || 'Mana Core',
|
||||||
CORS_ORIGINS: (env) => env.CORS_ORIGINS,
|
CORS_ORIGINS: (env) => env.CORS_ORIGINS,
|
||||||
CREDITS_SIGNUP_BONUS: (env) => env.CREDITS_SIGNUP_BONUS,
|
CREDITS_SIGNUP_BONUS: (env) => env.CREDITS_SIGNUP_BONUS,
|
||||||
CREDITS_DAILY_FREE: (env) => env.CREDITS_DAILY_FREE,
|
CREDITS_DAILY_FREE: (env) => env.CREDITS_DAILY_FREE,
|
||||||
|
|
@ -98,6 +101,12 @@ const APP_CONFIGS = [
|
||||||
GOOGLE_GENAI_API_KEY: (env) => env.GOOGLE_GENAI_API_KEY,
|
GOOGLE_GENAI_API_KEY: (env) => env.GOOGLE_GENAI_API_KEY,
|
||||||
MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL,
|
MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL,
|
||||||
DATABASE_URL: (env) => env.CHAT_DATABASE_URL,
|
DATABASE_URL: (env) => env.CHAT_DATABASE_URL,
|
||||||
|
// S3 Storage (MinIO local, Hetzner production)
|
||||||
|
S3_ENDPOINT: (env) => env.S3_ENDPOINT,
|
||||||
|
S3_REGION: (env) => env.S3_REGION,
|
||||||
|
S3_ACCESS_KEY: (env) => env.S3_ACCESS_KEY,
|
||||||
|
S3_SECRET_KEY: (env) => env.S3_SECRET_KEY,
|
||||||
|
CORS_ORIGINS: (env) => env.CORS_ORIGINS,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
"db:studio": "drizzle-kit studio"
|
"db:studio": "drizzle-kit studio"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@getbrevo/brevo": "^3.0.1",
|
||||||
"@google/generative-ai": "^0.24.1",
|
"@google/generative-ai": "^0.24.1",
|
||||||
"@nestjs/common": "^10.4.15",
|
"@nestjs/common": "^10.4.15",
|
||||||
"@nestjs/config": "^3.3.0",
|
"@nestjs/config": "^3.3.0",
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { APP_FILTER } from '@nestjs/core';
|
||||||
import configuration from './config/configuration';
|
import configuration from './config/configuration';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
import { CreditsModule } from './credits/credits.module';
|
import { CreditsModule } from './credits/credits.module';
|
||||||
|
import { EmailModule } from './email/email.module';
|
||||||
import { FeedbackModule } from './feedback/feedback.module';
|
import { FeedbackModule } from './feedback/feedback.module';
|
||||||
import { ReferralsModule } from './referrals/referrals.module';
|
import { ReferralsModule } from './referrals/referrals.module';
|
||||||
import { SettingsModule } from './settings/settings.module';
|
import { SettingsModule } from './settings/settings.module';
|
||||||
|
|
@ -27,6 +28,7 @@ import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
||||||
AiModule,
|
AiModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
CreditsModule,
|
CreditsModule,
|
||||||
|
EmailModule,
|
||||||
FeedbackModule,
|
FeedbackModule,
|
||||||
HealthModule,
|
HealthModule,
|
||||||
ReferralsModule,
|
ReferralsModule,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import { getDb } from '../db/connection';
|
||||||
import { organizations, members, invitations } from '../db/schema/organizations.schema';
|
import { organizations, members, invitations } from '../db/schema/organizations.schema';
|
||||||
import { users, sessions, accounts, verificationTokens, jwks } from '../db/schema/auth.schema';
|
import { users, sessions, accounts, verificationTokens, jwks } from '../db/schema/auth.schema';
|
||||||
import type { JWTPayloadContext } from './types/better-auth.types';
|
import type { JWTPayloadContext } from './types/better-auth.types';
|
||||||
|
import { sendPasswordResetEmail, sendOrganizationInvitationEmail } from '../email/email-sender';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JWT Custom Payload Interface
|
* JWT Custom Payload Interface
|
||||||
|
|
@ -96,19 +97,8 @@ export function createBetterAuth(databaseUrl: string) {
|
||||||
*
|
*
|
||||||
* @see https://www.better-auth.com/docs/authentication/email-password#password-reset
|
* @see https://www.better-auth.com/docs/authentication/email-password#password-reset
|
||||||
*/
|
*/
|
||||||
sendResetPassword: async ({ user, url, token }) => {
|
sendResetPassword: async ({ user, url }) => {
|
||||||
// TODO: Implement email sending service (e.g., Resend, SendGrid)
|
await sendPasswordResetEmail(user.email, url, user.name);
|
||||||
// For now, log the reset URL for development
|
|
||||||
console.log('[Password Reset] User:', user.email);
|
|
||||||
console.log('[Password Reset] Reset URL:', url);
|
|
||||||
console.log('[Password Reset] Token:', token);
|
|
||||||
|
|
||||||
// In production, send an email like:
|
|
||||||
// await sendEmail({
|
|
||||||
// to: user.email,
|
|
||||||
// subject: 'Reset your password',
|
|
||||||
// html: `<a href="${url}">Reset your password</a>`
|
|
||||||
// });
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -143,14 +133,16 @@ export function createBetterAuth(databaseUrl: string) {
|
||||||
|
|
||||||
// Email invitation handler
|
// Email invitation handler
|
||||||
async sendInvitationEmail(data) {
|
async sendInvitationEmail(data) {
|
||||||
const { email, organization } = data;
|
const { email, organization, inviter } = data;
|
||||||
|
const baseUrl = process.env.BASE_URL || 'http://localhost:3001';
|
||||||
|
const invitationUrl = `${baseUrl}/accept-invitation?id=${data.id}`;
|
||||||
|
|
||||||
// TODO: Implement email sending service
|
await sendOrganizationInvitationEmail(
|
||||||
console.log('TODO: Send invitation email', {
|
email,
|
||||||
to: email,
|
organization.name,
|
||||||
organization: organization.name,
|
invitationUrl,
|
||||||
invitationId: data.id,
|
inviter?.user?.name
|
||||||
});
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Custom roles and permissions
|
// Custom roles and permissions
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,12 @@ export default () => ({
|
||||||
publishableKey: process.env.STRIPE_PUBLISHABLE_KEY || '',
|
publishableKey: process.env.STRIPE_PUBLISHABLE_KEY || '',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
email: {
|
||||||
|
brevoApiKey: process.env.BREVO_API_KEY || '',
|
||||||
|
fromEmail: process.env.BREVO_FROM_EMAIL || 'noreply@manacore.app',
|
||||||
|
fromName: process.env.BREVO_FROM_NAME || 'Mana Core',
|
||||||
|
},
|
||||||
|
|
||||||
cors: {
|
cors: {
|
||||||
origin: process.env.CORS_ORIGINS?.split(',') || [
|
origin: process.env.CORS_ORIGINS?.split(',') || [
|
||||||
'http://localhost:3000',
|
'http://localhost:3000',
|
||||||
|
|
|
||||||
322
services/mana-core-auth/src/email/email-sender.ts
Normal file
322
services/mana-core-auth/src/email/email-sender.ts
Normal file
|
|
@ -0,0 +1,322 @@
|
||||||
|
/**
|
||||||
|
* Standalone email sender for Better Auth callbacks
|
||||||
|
*
|
||||||
|
* This module provides email sending functionality that can be used
|
||||||
|
* outside of the NestJS DI context (e.g., in Better Auth callbacks).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as brevo from '@getbrevo/brevo';
|
||||||
|
|
||||||
|
interface EmailConfig {
|
||||||
|
apiKey?: string;
|
||||||
|
fromEmail: string;
|
||||||
|
fromName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEmailConfig(): EmailConfig {
|
||||||
|
return {
|
||||||
|
apiKey: process.env.BREVO_API_KEY,
|
||||||
|
fromEmail: process.env.BREVO_FROM_EMAIL || 'noreply@manacore.app',
|
||||||
|
fromName: process.env.BREVO_FROM_NAME || 'Mana Core',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let apiInstance: brevo.TransactionalEmailsApi | null = null;
|
||||||
|
|
||||||
|
function getApiInstance(): brevo.TransactionalEmailsApi | null {
|
||||||
|
const config = getEmailConfig();
|
||||||
|
|
||||||
|
if (!config.apiKey) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!apiInstance) {
|
||||||
|
apiInstance = new brevo.TransactionalEmailsApi();
|
||||||
|
apiInstance.setApiKey(brevo.TransactionalEmailsApiApiKeys.apiKey, config.apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send password reset email
|
||||||
|
*/
|
||||||
|
export async function sendPasswordResetEmail(
|
||||||
|
email: string,
|
||||||
|
resetUrl: string,
|
||||||
|
userName?: string
|
||||||
|
): Promise<void> {
|
||||||
|
const config = getEmailConfig();
|
||||||
|
const api = getApiInstance();
|
||||||
|
|
||||||
|
if (!api) {
|
||||||
|
console.log('[DEV MODE] Password reset email would be sent to:', email);
|
||||||
|
console.log('[DEV MODE] Reset URL:', resetUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendSmtpEmail = new brevo.SendSmtpEmail();
|
||||||
|
sendSmtpEmail.sender = { email: config.fromEmail, name: config.fromName };
|
||||||
|
sendSmtpEmail.to = [{ email }];
|
||||||
|
sendSmtpEmail.subject = 'Reset your Mana Core password';
|
||||||
|
sendSmtpEmail.htmlContent = getPasswordResetTemplate(resetUrl, userName);
|
||||||
|
sendSmtpEmail.textContent = `
|
||||||
|
Reset your password
|
||||||
|
|
||||||
|
Hi${userName ? ` ${userName}` : ''},
|
||||||
|
|
||||||
|
You requested to reset your password. Click the link below to set a new password:
|
||||||
|
|
||||||
|
${resetUrl}
|
||||||
|
|
||||||
|
This link will expire in 1 hour.
|
||||||
|
|
||||||
|
If you didn't request this, you can safely ignore this email.
|
||||||
|
|
||||||
|
- The Mana Core Team
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.sendTransacEmail(sendSmtpEmail);
|
||||||
|
console.log(`Password reset email sent to ${email}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to send password reset email to ${email}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send organization invitation email
|
||||||
|
*/
|
||||||
|
export async function sendOrganizationInvitationEmail(
|
||||||
|
email: string,
|
||||||
|
organizationName: string,
|
||||||
|
invitationUrl: string,
|
||||||
|
inviterName?: string
|
||||||
|
): Promise<void> {
|
||||||
|
const config = getEmailConfig();
|
||||||
|
const api = getApiInstance();
|
||||||
|
|
||||||
|
if (!api) {
|
||||||
|
console.log('[DEV MODE] Invitation email would be sent to:', email);
|
||||||
|
console.log('[DEV MODE] Organization:', organizationName);
|
||||||
|
console.log('[DEV MODE] Invitation URL:', invitationUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendSmtpEmail = new brevo.SendSmtpEmail();
|
||||||
|
sendSmtpEmail.sender = { email: config.fromEmail, name: config.fromName };
|
||||||
|
sendSmtpEmail.to = [{ email }];
|
||||||
|
sendSmtpEmail.subject = `You've been invited to join ${organizationName} on Mana Core`;
|
||||||
|
sendSmtpEmail.htmlContent = getInvitationTemplate(organizationName, invitationUrl, inviterName);
|
||||||
|
sendSmtpEmail.textContent = `
|
||||||
|
You've been invited to ${organizationName}
|
||||||
|
|
||||||
|
Hi,
|
||||||
|
|
||||||
|
${inviterName ? `${inviterName} has` : 'You have been'} invited you to join ${organizationName} on Mana Core.
|
||||||
|
|
||||||
|
Click the link below to accept the invitation:
|
||||||
|
|
||||||
|
${invitationUrl}
|
||||||
|
|
||||||
|
This invitation will expire in 7 days.
|
||||||
|
|
||||||
|
- The Mana Core Team
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.sendTransacEmail(sendSmtpEmail);
|
||||||
|
console.log(`Invitation email sent to ${email}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to send invitation email to ${email}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send email verification email
|
||||||
|
*/
|
||||||
|
export async function sendVerificationEmail(
|
||||||
|
email: string,
|
||||||
|
verificationUrl: string,
|
||||||
|
userName?: string
|
||||||
|
): Promise<void> {
|
||||||
|
const config = getEmailConfig();
|
||||||
|
const api = getApiInstance();
|
||||||
|
|
||||||
|
if (!api) {
|
||||||
|
console.log('[DEV MODE] Verification email would be sent to:', email);
|
||||||
|
console.log('[DEV MODE] Verification URL:', verificationUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendSmtpEmail = new brevo.SendSmtpEmail();
|
||||||
|
sendSmtpEmail.sender = { email: config.fromEmail, name: config.fromName };
|
||||||
|
sendSmtpEmail.to = [{ email }];
|
||||||
|
sendSmtpEmail.subject = 'Verify your Mana Core email address';
|
||||||
|
sendSmtpEmail.htmlContent = getVerificationTemplate(verificationUrl, userName);
|
||||||
|
sendSmtpEmail.textContent = `
|
||||||
|
Verify your email address
|
||||||
|
|
||||||
|
Hi${userName ? ` ${userName}` : ''},
|
||||||
|
|
||||||
|
Please verify your email address by clicking the link below:
|
||||||
|
|
||||||
|
${verificationUrl}
|
||||||
|
|
||||||
|
This link will expire in 24 hours.
|
||||||
|
|
||||||
|
If you didn't create a Mana Core account, you can safely ignore this email.
|
||||||
|
|
||||||
|
- The Mana Core Team
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.sendTransacEmail(sendSmtpEmail);
|
||||||
|
console.log(`Verification email sent to ${email}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to send verification email to ${email}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPasswordResetTemplate(resetUrl: string, userName?: string): string {
|
||||||
|
return `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Reset your password</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
|
||||||
|
<table role="presentation" style="width: 100%; border-collapse: collapse;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px 20px;">
|
||||||
|
<table role="presentation" style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px; text-align: center;">
|
||||||
|
<h1 style="margin: 0 0 24px; font-size: 24px; font-weight: 600; color: #1a1a1a;">Reset your password</h1>
|
||||||
|
<p style="margin: 0 0 24px; font-size: 16px; line-height: 1.5; color: #4a4a4a;">
|
||||||
|
Hi${userName ? ` ${userName}` : ''},<br><br>
|
||||||
|
You requested to reset your password. Click the button below to set a new password:
|
||||||
|
</p>
|
||||||
|
<a href="${resetUrl}" style="display: inline-block; padding: 14px 32px; background-color: #6366f1; color: #ffffff; text-decoration: none; border-radius: 6px; font-weight: 500; font-size: 16px;">
|
||||||
|
Reset Password
|
||||||
|
</a>
|
||||||
|
<p style="margin: 24px 0 0; font-size: 14px; color: #6b7280;">
|
||||||
|
This link will expire in 1 hour.<br>
|
||||||
|
If you didn't request this, you can safely ignore this email.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 24px 40px; background-color: #f9fafb; border-top: 1px solid #e5e7eb; border-radius: 0 0 8px 8px;">
|
||||||
|
<p style="margin: 0; font-size: 12px; color: #9ca3af; text-align: center;">
|
||||||
|
© ${new Date().getFullYear()} Mana Core. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInvitationTemplate(
|
||||||
|
organizationName: string,
|
||||||
|
invitationUrl: string,
|
||||||
|
inviterName?: string
|
||||||
|
): string {
|
||||||
|
return `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Organization Invitation</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
|
||||||
|
<table role="presentation" style="width: 100%; border-collapse: collapse;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px 20px;">
|
||||||
|
<table role="presentation" style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px; text-align: center;">
|
||||||
|
<h1 style="margin: 0 0 24px; font-size: 24px; font-weight: 600; color: #1a1a1a;">You've been invited!</h1>
|
||||||
|
<p style="margin: 0 0 24px; font-size: 16px; line-height: 1.5; color: #4a4a4a;">
|
||||||
|
${inviterName ? `${inviterName} has` : 'You have been'} invited you to join <strong>${organizationName}</strong> on Mana Core.
|
||||||
|
</p>
|
||||||
|
<a href="${invitationUrl}" style="display: inline-block; padding: 14px 32px; background-color: #6366f1; color: #ffffff; text-decoration: none; border-radius: 6px; font-weight: 500; font-size: 16px;">
|
||||||
|
Accept Invitation
|
||||||
|
</a>
|
||||||
|
<p style="margin: 24px 0 0; font-size: 14px; color: #6b7280;">
|
||||||
|
This invitation will expire in 7 days.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 24px 40px; background-color: #f9fafb; border-top: 1px solid #e5e7eb; border-radius: 0 0 8px 8px;">
|
||||||
|
<p style="margin: 0; font-size: 12px; color: #9ca3af; text-align: center;">
|
||||||
|
© ${new Date().getFullYear()} Mana Core. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVerificationTemplate(verificationUrl: string, userName?: string): string {
|
||||||
|
return `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Verify your email</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
|
||||||
|
<table role="presentation" style="width: 100%; border-collapse: collapse;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px 20px;">
|
||||||
|
<table role="presentation" style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px; text-align: center;">
|
||||||
|
<h1 style="margin: 0 0 24px; font-size: 24px; font-weight: 600; color: #1a1a1a;">Verify your email</h1>
|
||||||
|
<p style="margin: 0 0 24px; font-size: 16px; line-height: 1.5; color: #4a4a4a;">
|
||||||
|
Hi${userName ? ` ${userName}` : ''},<br><br>
|
||||||
|
Thanks for signing up! Please verify your email address by clicking the button below:
|
||||||
|
</p>
|
||||||
|
<a href="${verificationUrl}" style="display: inline-block; padding: 14px 32px; background-color: #6366f1; color: #ffffff; text-decoration: none; border-radius: 6px; font-weight: 500; font-size: 16px;">
|
||||||
|
Verify Email
|
||||||
|
</a>
|
||||||
|
<p style="margin: 24px 0 0; font-size: 14px; color: #6b7280;">
|
||||||
|
This link will expire in 24 hours.<br>
|
||||||
|
If you didn't create a Mana Core account, you can safely ignore this email.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 24px 40px; background-color: #f9fafb; border-top: 1px solid #e5e7eb; border-radius: 0 0 8px 8px;">
|
||||||
|
<p style="margin: 0; font-size: 12px; color: #9ca3af; text-align: center;">
|
||||||
|
© ${new Date().getFullYear()} Mana Core. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
9
services/mana-core-auth/src/email/email.module.ts
Normal file
9
services/mana-core-auth/src/email/email.module.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { EmailService } from './email.service';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [EmailService],
|
||||||
|
exports: [EmailService],
|
||||||
|
})
|
||||||
|
export class EmailModule {}
|
||||||
320
services/mana-core-auth/src/email/email.service.ts
Normal file
320
services/mana-core-auth/src/email/email.service.ts
Normal file
|
|
@ -0,0 +1,320 @@
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import * as brevo from '@getbrevo/brevo';
|
||||||
|
|
||||||
|
export interface SendEmailOptions {
|
||||||
|
to: string;
|
||||||
|
subject: string;
|
||||||
|
htmlContent: string;
|
||||||
|
textContent?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PasswordResetEmailData {
|
||||||
|
email: string;
|
||||||
|
name?: string;
|
||||||
|
resetUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InvitationEmailData {
|
||||||
|
email: string;
|
||||||
|
organizationName: string;
|
||||||
|
inviterName?: string;
|
||||||
|
invitationUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VerificationEmailData {
|
||||||
|
email: string;
|
||||||
|
name?: string;
|
||||||
|
verificationUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EmailService {
|
||||||
|
private readonly logger = new Logger(EmailService.name);
|
||||||
|
private readonly apiInstance: brevo.TransactionalEmailsApi;
|
||||||
|
private readonly fromEmail: string;
|
||||||
|
private readonly fromName: string;
|
||||||
|
private readonly isConfigured: boolean;
|
||||||
|
|
||||||
|
constructor(private configService: ConfigService) {
|
||||||
|
const apiKey = this.configService.get<string>('email.brevoApiKey');
|
||||||
|
this.fromEmail = this.configService.get<string>('email.fromEmail') || 'noreply@manacore.app';
|
||||||
|
this.fromName = this.configService.get<string>('email.fromName') || 'Mana Core';
|
||||||
|
|
||||||
|
this.apiInstance = new brevo.TransactionalEmailsApi();
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
this.apiInstance.setApiKey(brevo.TransactionalEmailsApiApiKeys.apiKey, apiKey);
|
||||||
|
this.isConfigured = true;
|
||||||
|
this.logger.log('Email service configured with Brevo');
|
||||||
|
} else {
|
||||||
|
this.isConfigured = false;
|
||||||
|
this.logger.warn('Email service not configured - BREVO_API_KEY is missing');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a transactional email via Brevo
|
||||||
|
*/
|
||||||
|
async sendEmail(options: SendEmailOptions): Promise<boolean> {
|
||||||
|
if (!this.isConfigured) {
|
||||||
|
this.logger.warn(`[DEV MODE] Would send email to ${options.to}: ${options.subject}`);
|
||||||
|
this.logger.debug(`Email content: ${options.htmlContent}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sendSmtpEmail = new brevo.SendSmtpEmail();
|
||||||
|
sendSmtpEmail.sender = { email: this.fromEmail, name: this.fromName };
|
||||||
|
sendSmtpEmail.to = [{ email: options.to }];
|
||||||
|
sendSmtpEmail.subject = options.subject;
|
||||||
|
sendSmtpEmail.htmlContent = options.htmlContent;
|
||||||
|
|
||||||
|
if (options.textContent) {
|
||||||
|
sendSmtpEmail.textContent = options.textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.apiInstance.sendTransacEmail(sendSmtpEmail);
|
||||||
|
this.logger.log(`Email sent successfully to ${options.to}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to send email to ${options.to}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send password reset email
|
||||||
|
*/
|
||||||
|
async sendPasswordResetEmail(data: PasswordResetEmailData): Promise<boolean> {
|
||||||
|
const subject = 'Reset your Mana Core password';
|
||||||
|
const htmlContent = this.getPasswordResetTemplate(data);
|
||||||
|
const textContent = `
|
||||||
|
Reset your password
|
||||||
|
|
||||||
|
Hi${data.name ? ` ${data.name}` : ''},
|
||||||
|
|
||||||
|
You requested to reset your password. Click the link below to set a new password:
|
||||||
|
|
||||||
|
${data.resetUrl}
|
||||||
|
|
||||||
|
This link will expire in 1 hour.
|
||||||
|
|
||||||
|
If you didn't request this, you can safely ignore this email.
|
||||||
|
|
||||||
|
- The Mana Core Team
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
return this.sendEmail({
|
||||||
|
to: data.email,
|
||||||
|
subject,
|
||||||
|
htmlContent,
|
||||||
|
textContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send organization invitation email
|
||||||
|
*/
|
||||||
|
async sendInvitationEmail(data: InvitationEmailData): Promise<boolean> {
|
||||||
|
const subject = `You've been invited to join ${data.organizationName} on Mana Core`;
|
||||||
|
const htmlContent = this.getInvitationTemplate(data);
|
||||||
|
const textContent = `
|
||||||
|
You've been invited to ${data.organizationName}
|
||||||
|
|
||||||
|
Hi,
|
||||||
|
|
||||||
|
${data.inviterName ? `${data.inviterName} has` : 'You have been'} invited you to join ${data.organizationName} on Mana Core.
|
||||||
|
|
||||||
|
Click the link below to accept the invitation:
|
||||||
|
|
||||||
|
${data.invitationUrl}
|
||||||
|
|
||||||
|
This invitation will expire in 7 days.
|
||||||
|
|
||||||
|
- The Mana Core Team
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
return this.sendEmail({
|
||||||
|
to: data.email,
|
||||||
|
subject,
|
||||||
|
htmlContent,
|
||||||
|
textContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send email verification email
|
||||||
|
*/
|
||||||
|
async sendVerificationEmail(data: VerificationEmailData): Promise<boolean> {
|
||||||
|
const subject = 'Verify your Mana Core email address';
|
||||||
|
const htmlContent = this.getVerificationTemplate(data);
|
||||||
|
const textContent = `
|
||||||
|
Verify your email address
|
||||||
|
|
||||||
|
Hi${data.name ? ` ${data.name}` : ''},
|
||||||
|
|
||||||
|
Please verify your email address by clicking the link below:
|
||||||
|
|
||||||
|
${data.verificationUrl}
|
||||||
|
|
||||||
|
This link will expire in 24 hours.
|
||||||
|
|
||||||
|
If you didn't create a Mana Core account, you can safely ignore this email.
|
||||||
|
|
||||||
|
- The Mana Core Team
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
return this.sendEmail({
|
||||||
|
to: data.email,
|
||||||
|
subject,
|
||||||
|
htmlContent,
|
||||||
|
textContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Password reset email template
|
||||||
|
*/
|
||||||
|
private getPasswordResetTemplate(data: PasswordResetEmailData): string {
|
||||||
|
return `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Reset your password</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
|
||||||
|
<table role="presentation" style="width: 100%; border-collapse: collapse;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px 20px;">
|
||||||
|
<table role="presentation" style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px; text-align: center;">
|
||||||
|
<h1 style="margin: 0 0 24px; font-size: 24px; font-weight: 600; color: #1a1a1a;">Reset your password</h1>
|
||||||
|
<p style="margin: 0 0 24px; font-size: 16px; line-height: 1.5; color: #4a4a4a;">
|
||||||
|
Hi${data.name ? ` ${data.name}` : ''},<br><br>
|
||||||
|
You requested to reset your password. Click the button below to set a new password:
|
||||||
|
</p>
|
||||||
|
<a href="${data.resetUrl}" style="display: inline-block; padding: 14px 32px; background-color: #6366f1; color: #ffffff; text-decoration: none; border-radius: 6px; font-weight: 500; font-size: 16px;">
|
||||||
|
Reset Password
|
||||||
|
</a>
|
||||||
|
<p style="margin: 24px 0 0; font-size: 14px; color: #6b7280;">
|
||||||
|
This link will expire in 1 hour.<br>
|
||||||
|
If you didn't request this, you can safely ignore this email.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 24px 40px; background-color: #f9fafb; border-top: 1px solid #e5e7eb; border-radius: 0 0 8px 8px;">
|
||||||
|
<p style="margin: 0; font-size: 12px; color: #9ca3af; text-align: center;">
|
||||||
|
© ${new Date().getFullYear()} Mana Core. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Organization invitation email template
|
||||||
|
*/
|
||||||
|
private getInvitationTemplate(data: InvitationEmailData): string {
|
||||||
|
return `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Organization Invitation</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
|
||||||
|
<table role="presentation" style="width: 100%; border-collapse: collapse;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px 20px;">
|
||||||
|
<table role="presentation" style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px; text-align: center;">
|
||||||
|
<h1 style="margin: 0 0 24px; font-size: 24px; font-weight: 600; color: #1a1a1a;">You've been invited!</h1>
|
||||||
|
<p style="margin: 0 0 24px; font-size: 16px; line-height: 1.5; color: #4a4a4a;">
|
||||||
|
${data.inviterName ? `${data.inviterName} has` : 'You have been'} invited you to join <strong>${data.organizationName}</strong> on Mana Core.
|
||||||
|
</p>
|
||||||
|
<a href="${data.invitationUrl}" style="display: inline-block; padding: 14px 32px; background-color: #6366f1; color: #ffffff; text-decoration: none; border-radius: 6px; font-weight: 500; font-size: 16px;">
|
||||||
|
Accept Invitation
|
||||||
|
</a>
|
||||||
|
<p style="margin: 24px 0 0; font-size: 14px; color: #6b7280;">
|
||||||
|
This invitation will expire in 7 days.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 24px 40px; background-color: #f9fafb; border-top: 1px solid #e5e7eb; border-radius: 0 0 8px 8px;">
|
||||||
|
<p style="margin: 0; font-size: 12px; color: #9ca3af; text-align: center;">
|
||||||
|
© ${new Date().getFullYear()} Mana Core. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Email verification template
|
||||||
|
*/
|
||||||
|
private getVerificationTemplate(data: VerificationEmailData): string {
|
||||||
|
return `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Verify your email</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
|
||||||
|
<table role="presentation" style="width: 100%; border-collapse: collapse;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px 20px;">
|
||||||
|
<table role="presentation" style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px; text-align: center;">
|
||||||
|
<h1 style="margin: 0 0 24px; font-size: 24px; font-weight: 600; color: #1a1a1a;">Verify your email</h1>
|
||||||
|
<p style="margin: 0 0 24px; font-size: 16px; line-height: 1.5; color: #4a4a4a;">
|
||||||
|
Hi${data.name ? ` ${data.name}` : ''},<br><br>
|
||||||
|
Thanks for signing up! Please verify your email address by clicking the button below:
|
||||||
|
</p>
|
||||||
|
<a href="${data.verificationUrl}" style="display: inline-block; padding: 14px 32px; background-color: #6366f1; color: #ffffff; text-decoration: none; border-radius: 6px; font-weight: 500; font-size: 16px;">
|
||||||
|
Verify Email
|
||||||
|
</a>
|
||||||
|
<p style="margin: 24px 0 0; font-size: 14px; color: #6b7280;">
|
||||||
|
This link will expire in 24 hours.<br>
|
||||||
|
If you didn't create a Mana Core account, you can safely ignore this email.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 24px 40px; background-color: #f9fafb; border-top: 1px solid #e5e7eb; border-radius: 0 0 8px 8px;">
|
||||||
|
<p style="margin: 0; font-size: 12px; color: #9ca3af; text-align: center;">
|
||||||
|
© ${new Date().getFullYear()} Mana Core. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
2
services/mana-core-auth/src/email/index.ts
Normal file
2
services/mana-core-auth/src/email/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './email.service';
|
||||||
|
export * from './email.module';
|
||||||
Loading…
Add table
Add a link
Reference in a new issue