Merge pull request #42 from Hexastack/fix/smtp-config

fix: smtp config
This commit is contained in:
Mohamed Marrouchi 2024-09-19 06:35:25 +01:00 committed by GitHub
commit ced9f0538c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 157 additions and 213 deletions

View File

@ -9,7 +9,10 @@ define add_service
COMPOSE_FILES += -f ./docker/docker-compose.$(1).prod.yml
endif
else
COMPOSE_FILES += -f ./docker/docker-compose.$(1).yml -f ./docker/docker-compose.$(1).dev.yml
COMPOSE_FILES += -f ./docker/docker-compose.$(1).yml
ifneq ($(wildcard ./docker/docker-compose.$(1).dev.yml),)
COMPOSE_FILES += -f ./docker/docker-compose.$(1).dev.yml
endif
endif
endef
@ -23,6 +26,10 @@ ifneq ($(NLU),)
$(eval $(call add_service,nlu))
endif
ifneq ($(SMTP4DEV),)
$(eval $(call add_service,smtp4dev))
endif
# Ensure .env file exists and matches .env.example
check-env:
@if [ ! -f "./docker/.env" ]; then \
@ -48,4 +55,4 @@ destroy: check-env
docker compose $(COMPOSE_FILES) down -v
migrate-up:
docker-compose $(COMPOSE_FILES) up --no-deps -d database-init
docker-compose $(COMPOSE_FILES) up --no-deps -d database-init

View File

@ -57,24 +57,28 @@ const i18nOptions: I18nOptions = {
@Module({
imports: [
MailerModule.forRoot({
transport: new SMTPTransport({
...config.emails.smtp,
logger: true,
}),
template: {
adapter: new MjmlAdapter('ejs', { inlineCssEnabled: false }),
dir: './src/templates',
options: {
context: {
appName: config.parameters.appName,
appUrl: config.parameters.appUrl,
// TODO: add i18n support
},
},
},
defaults: { from: config.parameters.email.main },
}),
...(config.emails.isEnabled
? [
MailerModule.forRoot({
transport: new SMTPTransport({
...config.emails.smtp,
logger: true,
debug: false,
}),
template: {
adapter: new MjmlAdapter('ejs', { inlineCssEnabled: false }),
dir: './src/templates',
options: {
context: {
appName: config.parameters.appName,
appUrl: config.parameters.appUrl,
},
},
},
defaults: { from: config.emails.from },
}),
]
: []),
MongooseModule.forRoot(config.mongo.uri, {
dbName: config.mongo.dbName,
connectionFactory: (connection) => {

View File

@ -8,19 +8,14 @@
*/
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { ExtendedI18nService } from './extended-i18n.service';
@Injectable()
export class AppService {
constructor(
private readonly i18n: ExtendedI18nService,
private readonly eventEmitter: EventEmitter2,
) {}
constructor(private readonly i18n: ExtendedI18nService) {}
getHello(): string {
this.eventEmitter.emit('hook:i18n:refresh', []);
return this.i18n.t('Welcome');
return this.i18n.t('welcome', { lang: 'en' });
}
}

View File

@ -1,18 +1,21 @@
{
"invitation_subject": "[Hexabot] Sign-Up Invitation",
"account_confirmation_subject": "[Hexabot] Account Confirmation",
"password_reset_subject": "[Hexabot] Password Reset",
"welcome": "Welcome",
"hi": "Hi",
"invitation_to_join": "Invitation to join",
"invitation_for_account_creation": "You have been invited to create a",
"account": "account",
"create_account:": "Click on the button below to create your account:",
"create_account": "Click on the button below to create your account:",
"registration_failed": "Registration failed! Either email address or invitation token is invalid.",
"create_account_button": "Click on the button below to create your account",
"join": "Join",
"account_successfully_created_confirm_password": "You have successfully created an account but you will need to confirm your email address.",
"confirm_account:": "Click on the button below in order to confirm your account:",
"confirm_account": "Click on the button below in order to confirm your account:",
"password_reset_request": "A request to reset the password for your account has been made.",
"reset": "Reset",
"click_to_reset_password:": "Click on the button below in order to set a new password:",
"click_to_reset_password": "Click on the button below in order to set a new password:",
"confirm": "Confirm",
"best_regards": "Best Regards,"
}

View File

@ -1,4 +1,7 @@
{
"invitation_subject": "[Hexabot] Invitation à s'inscrire",
"account_confirmation_subject": "[Hexabot] Confirmation de compte",
"password_reset_subject": "[Hexabot] Réinitialisation du mot de passe",
"welcome": "Bienvenue",
"hi": "Bonjour",
"invitation_to_join": "Invitation",

View File

@ -13,7 +13,7 @@ import { Config } from './types';
export const config: Config = {
i18n: {
translationFilename: process.env.I18N_TRANSLATION_FILENAME || '',
translationFilename: process.env.I18N_TRANSLATION_FILENAME || 'messages',
},
appPath: process.cwd(),
apiPath: process.env.API_ORIGIN,
@ -77,7 +77,7 @@ export const config: Config = {
: [undefined], // ['http://example.com', 'https://example.com'],
},
session: {
secret: process.env.SESSION_SECRET || '4fac3596aeb0d048e7b6b38235c29248',
secret: process.env.SESSION_SECRET || 'changeme',
name: process.env.SESSION_NAME || 'hex.sid',
adapter: 'connect-mongo',
url: 'mongodb://localhost:27017/hexabot',
@ -90,23 +90,18 @@ export const config: Config = {
},
},
emails: {
isEnabled: process.env.EMAIL_SMTP_ENABLED === 'true' || false,
smtp: {
port: parseInt(process.env.EMAIL_SMTP_PORT) || 25,
host: process.env.EMAIL_SMTP_HOST || 'smtp.mailgun.org',
host: process.env.EMAIL_SMTP_HOST || 'localhost',
ignoreTLS: false,
secure: process.env.EMAIL_SMTP_SECURE === 'true' || false,
auth: {
user:
process.env.EMAIL_SMTP_USER ||
'postmaster@sandbox9471202ff10448c7ac917618fe94d8e1.mailgun.org',
pass: process.env.EMAIL_SMTP_PASS || 'e58526b30ad640394b5c77a211a19c5b',
user: process.env.EMAIL_SMTP_USER || '',
pass: process.env.EMAIL_SMTP_PASS || '',
},
},
},
datastores: {
default: {
adapter: 'sails-mongo',
url: 'mongodb://localhost:27017/hexabot',
},
from: process.env.EMAIL_SMTP_FROM || 'noreply@example.com',
},
parameters: {
uploadDir:
@ -117,17 +112,9 @@ export const config: Config = {
maxUploadSize: process.env.UPLOAD_MAX_SIZE_IN_BYTES
? Number(process.env.UPLOAD_MAX_SIZE_IN_BYTES)
: 2000000,
transport: 'smtp',
email: {
main: 'postmaster@sandbox9471202ff10448c7ac917618fe94d8e1.mailgun.org',
},
appName: 'Hexabot.ai',
apiUrl: 'http://localhost:4000',
appUrl: 'http://localhost:8081',
geocoder: {
provider: 'opencage',
apiKey: 'c2a490d593b14612aefa6ec2e6b77c47',
},
},
pagination: {
limit: 10,
@ -135,7 +122,7 @@ export const config: Config = {
chatbot: {
lang: {
default: 'en',
available: ['en', 'fr', 'ar', 'tn'],
available: ['en', 'fr'],
},
messages: {
track_delivery: false,

View File

@ -7,6 +7,7 @@
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
*/
import SMTPConnection from 'nodemailer/lib/smtp-connection';
import type { ServerOptions, Socket } from 'socket.io';
type TJwtOptions = {
@ -69,38 +70,18 @@ export type Config = {
};
};
emails: {
smtp: {
port: number;
host: string;
secure: boolean;
auth: {
user: string;
pass: string;
};
};
};
datastores: {
default: {
adapter: string;
url: string;
};
isEnabled: boolean;
smtp: Partial<SMTPConnection.Options>;
from: string;
};
parameters: {
uploadDir: string;
avatarDir: string;
storageMode: 'disk' | 'memory';
maxUploadSize: number;
transport: string;
email: {
main: string;
};
appName: string;
apiUrl: string;
appUrl: string;
geocoder: {
provider: string;
apiKey: string;
};
};
pagination: {
limit: number;

View File

@ -58,9 +58,11 @@ export class ExtendedI18nService<
initDynamicTranslations(translations: Translation[]) {
this.dynamicTranslations = translations.reduce((acc, curr) => {
const { str, translations } = curr;
Object.entries(translations).forEach(([lang, t]) => {
acc[lang][str] = t;
});
Object.entries(translations)
.filter(([lang]) => lang in acc)
.forEach(([lang, t]) => {
acc[lang][str] = t;
});
return acc;
}, this.dynamicTranslations);

View File

@ -111,10 +111,4 @@ export type Settings = {
fallback_message: string[];
fallback_block: string;
};
email_settings: {
mailer: string;
auth_user: string;
auth_pass: string;
from: string;
};
} & Record<string, any>;

View File

@ -99,56 +99,6 @@ export const settingModels: SettingCreateDto[] = [
},
weight: 6,
},
{
group: 'email_settings',
label: 'from',
value: 'no-reply@domain.com',
type: SettingType.text,
weight: 1,
},
{
group: 'email_settings',
label: 'mailer',
value: 'sendmail',
options: ['sendmail', 'smtp'],
type: SettingType.select,
weight: 2,
},
{
group: 'email_settings',
label: 'host',
value: 'localhost',
type: SettingType.text,
weight: 3,
},
{
group: 'email_settings',
label: 'port',
value: '25',
type: SettingType.text,
weight: 4,
},
{
group: 'email_settings',
label: 'secure',
value: true,
type: SettingType.checkbox,
weight: 5,
},
{
group: 'email_settings',
label: 'auth_user',
value: '',
type: SettingType.text,
weight: 6,
},
{
group: 'email_settings',
label: 'auth_pass',
value: '',
type: SettingType.text,
weight: 7,
},
{
group: 'contact',
label: 'contact_email_recipient',

View File

@ -24,10 +24,10 @@
</mj-section>
<mj-section>
<mj-column>
<mj-text font-size="14px" color="#000" font-family="helvetica"
><%= t('best_regards') %>,</mj-text
<mj-text font-size="16px" color="#000" font-family="helvetica"
><%= t('best_regards') %></mj-text
>
<mj-text font-size="14px" color="#000" font-family="helvetica"
<mj-text font-size="16px" color="#000" font-family="helvetica"
><%= this.appName %></mj-text
>
</mj-column>

View File

@ -26,11 +26,11 @@
</mj-section>
<mj-section>
<mj-column>
<mj-text font-size="14px" color="#000" font-family="helvetica"
<mj-text font-size="16px" color="#000" font-family="helvetica"
><%= t('best_regards') %></mj-text
>
<mj-text font-size="14px" color="#000" font-family="helvetica"
<mj-text font-size="16px" color="#000" font-family="helvetica"
><%= this.appName %></mj-text
>
</mj-column>

View File

@ -23,10 +23,10 @@
</mj-section>
<mj-section>
<mj-column>
<mj-text font-size="14px" color="#000" font-family="helvetica"
><%= t('best_regards') %>,</mj-text
<mj-text font-size="16px" color="#000" font-family="helvetica"
><%= t('best_regards') %></mj-text
>
<mj-text font-size="14px" color="#000" font-family="helvetica"
<mj-text font-size="16px" color="#000" font-family="helvetica"
><%= this.appName %></mj-text
>
</mj-column>

View File

@ -11,6 +11,7 @@ import {
Inject,
Injectable,
InternalServerErrorException,
Optional,
} from '@nestjs/common';
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
import { MailerService } from '@nestjs-modules/mailer';
@ -32,7 +33,7 @@ export class InvitationService extends BaseService<Invitation> {
@Inject(InvitationRepository)
readonly repository: InvitationRepository,
@Inject(JwtService) private readonly jwtService: JwtService,
private readonly mailerService: MailerService,
@Optional() private readonly mailerService: MailerService | undefined,
private logger: LoggerService,
protected readonly i18n: ExtendedI18nService,
) {
@ -54,24 +55,28 @@ export class InvitationService extends BaseService<Invitation> {
*/
async create(dto: InvitationCreateDto): Promise<Invitation> {
const jwt = await this.sign(dto);
try {
await this.mailerService.sendMail({
to: dto.email,
template: 'invitation.mjml',
context: {
token: jwt,
// TODO: Which language should we use?
t: (key: string) => this.i18n.t(key),
},
});
} catch (e) {
this.logger.error(
'Could not send email',
e.message,
e.stack,
'InvitationService',
);
throw new InternalServerErrorException('Could not send email');
if (this.mailerService) {
try {
await this.mailerService.sendMail({
to: dto.email,
template: 'invitation.mjml',
context: {
token: jwt,
// TODO: Which language should we use?
t: (key: string) =>
this.i18n.t(key, { lang: config.chatbot.lang.default }),
},
subject: this.i18n.t('invitation_subject'),
});
} catch (e) {
this.logger.error(
'Could not send email',
e.message,
e.stack,
'InvitationService',
);
throw new InternalServerErrorException('Could not send email');
}
}
const newInvitation = await super.create({ ...dto, token: jwt });
return { ...newInvitation, token: jwt };

View File

@ -13,6 +13,7 @@ import {
Injectable,
InternalServerErrorException,
NotFoundException,
Optional,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
@ -30,7 +31,7 @@ import { UserRequestResetDto, UserResetPasswordDto } from '../dto/user.dto';
export class PasswordResetService {
constructor(
@Inject(JwtService) private readonly jwtService: JwtService,
private readonly mailerService: MailerService,
@Optional() private readonly mailerService: MailerService | undefined,
private logger: LoggerService,
private readonly userService: UserService,
public readonly i18n: ExtendedI18nService,
@ -55,24 +56,29 @@ export class PasswordResetService {
throw new NotFoundException('User not found');
}
const jwt = await this.sign(dto);
try {
await this.mailerService.sendMail({
to: dto.email,
template: 'password_reset.mjml',
context: {
token: jwt,
first_name: user.first_name,
t: (key: string) => this.i18n.t(key),
},
});
} catch (e) {
this.logger.error(
'Could not send email',
e.message,
e.stack,
'InvitationService',
);
throw new InternalServerErrorException('Could not send email');
if (this.mailerService) {
try {
await this.mailerService.sendMail({
to: dto.email,
template: 'password_reset.mjml',
context: {
token: jwt,
first_name: user.first_name,
t: (key: string) =>
this.i18n.t(key, { lang: config.chatbot.lang.default }),
},
subject: this.i18n.t('password_reset_subject'),
});
} catch (e) {
this.logger.error(
'Could not send email',
e.message,
e.stack,
'InvitationService',
);
throw new InternalServerErrorException('Could not send email');
}
}
// TODO: hash the token before saving it

View File

@ -11,6 +11,7 @@ import {
Inject,
Injectable,
InternalServerErrorException,
Optional,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
@ -33,7 +34,7 @@ export class ValidateAccountService {
constructor(
@Inject(JwtService) private readonly jwtService: JwtService,
private readonly userService: UserService,
private readonly mailerService: MailerService,
@Optional() private readonly mailerService: MailerService | undefined,
private readonly i18n: ExtendedI18nService,
) {}
@ -71,16 +72,19 @@ export class ValidateAccountService {
) {
const confirmationToken = await this.sign({ email: dto.email });
await this.mailerService.sendMail({
to: dto.email,
template: 'account_confirmation.mjml',
context: {
token: confirmationToken,
first_name: dto.first_name,
t: (key: string) => this.i18n.t(key),
},
subject: 'Account confirmation Email',
});
if (this.mailerService) {
await this.mailerService.sendMail({
to: dto.email,
template: 'account_confirmation.mjml',
context: {
token: confirmationToken,
first_name: dto.first_name,
t: (key: string) =>
this.i18n.t(key, { lang: config.chatbot.lang.default }),
},
subject: this.i18n.t('account_confirmation_subject'),
});
}
}
/**

View File

@ -30,13 +30,15 @@ MONGO_PASSWORD=dev_only
MONGO_URI=mongodb://${MONGO_USER}:${MONGO_PASSWORD}@mongo:27017/
MONGO_DB=hexabot
# SMTP Config for local dev env
# SMTP Config (for local dev env, use smtp4dev by doing `make start SMTP4DEV=1`)
APP_SMTP_4_DEV_PORT=9002
EMAIL_SMTP_ENABLED=false
EMAIL_SMTP_HOST=smtp4dev
EMAIL_SMTP_PORT=25
EMAIL_SMTP_SECURE=false
EMAIL_SMTP_USER=dev_only
EMAIL_SMTP_PASS=dev_only
EMAIL_SMTP_FROM=noreply@example.com
# NLU Server
AUTH_TOKEN=token123

View File

@ -17,21 +17,6 @@ services:
#- ../api/node_modules:/app/node_modules
command: ["npm", "run", "start:debug"]
smtp4dev:
image: rnwood/smtp4dev:v3
restart: always
ports:
- ${APP_SMTP_4_DEV_PORT}:80
- "25:25"
- "143:143"
volumes:
- smtp4dev-data:/smtp4dev
environment:
- ServerOptions__HostName=smtp4dev
- ServerOptions__LockSettings=true
networks:
- db-network
mongo-express:
container_name: mongoUi
image: mongo-express:1-20
@ -52,6 +37,3 @@ services:
- ../widget/src:/app/src
ports:
- ${APP_WIDGET_PORT}:5173
volumes:
smtp4dev-data:

View File

@ -0,0 +1,20 @@
version: "3.8"
services:
smtp4dev:
image: rnwood/smtp4dev:v3
restart: always
ports:
- ${APP_SMTP_4_DEV_PORT}:80
- "25:25"
- "143:143"
volumes:
- smtp4dev-data:/smtp4dev
environment:
- ServerOptions__HostName=smtp4dev
- ServerOptions__LockSettings=true
networks:
- app-network
volumes:
smtp4dev-data:

View File

@ -40,7 +40,7 @@
"edit_account_email": "A valid e-mail address. All e-mails from the system will be sent to this address. The e-mail address is not made public and will only be used if you wish to receive a new password or wish to receive certain news or notifications by e-mail.",
"new_password": "To change the current password, enter the new password in both fields.",
"account_update_success": "Account has been updated successfully",
"account_disabled": "Your account has been disabled!",
"account_disabled": "Your account has been either disabled or is pending confirmation.",
"success_invitation_sent": "Invitation to join has been successfully sent.",
"item_delete_confirm": "Are you sure you want to delete this item?",
"item_delete_success": "Item has been deleted successfully",
@ -213,7 +213,6 @@
"offline": "Web Channel",
"twitter": "Twitter",
"dimelo": "Dimelo",
"email_settings": "Email",
"contact": "Contact Infos",
"chatbot_settings": "Chatbot",
"nlp_settings": "NLP Provider",

View File

@ -40,7 +40,7 @@
"edit_account_email": "Une adresse e-mail valide Tous les e-mails du système seront envoyés à cette adresse. L'adresse e-mail n'est pas rendue publique et ne sera utilisée que si vous souhaitez recevoir un nouveau mot de passe ou si vous souhaitez recevoir certaines nouvelles ou notifications par e-mail.",
"new_password": "Pour changer le mot de passe actuel, entrez le nouveau mot de passe dans les deux champs.",
"account_update_success": "Le compte a été mis à jour avec succès",
"account_disabled": "Votre compte a été désactivé!",
"account_disabled": "Votre compte a été désactivé ou est en attente de confirmation.",
"success_invitation_sent": "L'invitation a été envoyée avec succès.",
"item_delete_confirm": "Êtes-vous sûr de bien vouloir supprimer cet élément?",
"item_delete_success": "L'élément a été supprimé avec succès",