mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 19:46:42 +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
|
|
@ -21,6 +21,7 @@
|
|||
"db:studio": "drizzle-kit studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"@getbrevo/brevo": "^3.0.1",
|
||||
"@google/generative-ai": "^0.24.1",
|
||||
"@nestjs/common": "^10.4.15",
|
||||
"@nestjs/config": "^3.3.0",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { APP_FILTER } from '@nestjs/core';
|
|||
import configuration from './config/configuration';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { CreditsModule } from './credits/credits.module';
|
||||
import { EmailModule } from './email/email.module';
|
||||
import { FeedbackModule } from './feedback/feedback.module';
|
||||
import { ReferralsModule } from './referrals/referrals.module';
|
||||
import { SettingsModule } from './settings/settings.module';
|
||||
|
|
@ -27,6 +28,7 @@ import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
|||
AiModule,
|
||||
AuthModule,
|
||||
CreditsModule,
|
||||
EmailModule,
|
||||
FeedbackModule,
|
||||
HealthModule,
|
||||
ReferralsModule,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { getDb } from '../db/connection';
|
|||
import { organizations, members, invitations } from '../db/schema/organizations.schema';
|
||||
import { users, sessions, accounts, verificationTokens, jwks } from '../db/schema/auth.schema';
|
||||
import type { JWTPayloadContext } from './types/better-auth.types';
|
||||
import { sendPasswordResetEmail, sendOrganizationInvitationEmail } from '../email/email-sender';
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
sendResetPassword: async ({ user, url, token }) => {
|
||||
// TODO: Implement email sending service (e.g., Resend, SendGrid)
|
||||
// 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>`
|
||||
// });
|
||||
sendResetPassword: async ({ user, url }) => {
|
||||
await sendPasswordResetEmail(user.email, url, user.name);
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -143,14 +133,16 @@ export function createBetterAuth(databaseUrl: string) {
|
|||
|
||||
// Email invitation handler
|
||||
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
|
||||
console.log('TODO: Send invitation email', {
|
||||
to: email,
|
||||
organization: organization.name,
|
||||
invitationId: data.id,
|
||||
});
|
||||
await sendOrganizationInvitationEmail(
|
||||
email,
|
||||
organization.name,
|
||||
invitationUrl,
|
||||
inviter?.user?.name
|
||||
);
|
||||
},
|
||||
|
||||
// Custom roles and permissions
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@ export default () => ({
|
|||
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: {
|
||||
origin: process.env.CORS_ORIGINS?.split(',') || [
|
||||
'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