From b5468f81ee8900b8e448cc50c4ade37472605972 Mon Sep 17 00:00:00 2001 From: Emnaghz Date: Wed, 18 Sep 2024 12:25:56 +0100 Subject: [PATCH 01/14] feat: add merge workflow --- .github/workflows/merge.yml | 111 ++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 .github/workflows/merge.yml diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml new file mode 100644 index 00000000..bb32564c --- /dev/null +++ b/.github/workflows/merge.yml @@ -0,0 +1,111 @@ +name: Build and Push Docker Image On Merge +on: + push: + branches: ['main'] + + workflow_dispatch: + +jobs: + paths-filter: + runs-on: ubuntu-latest + outputs: + frontend: ${{ steps.filter.outputs.frontend }} + api: ${{ steps.filter.outputs.api }} + widget: ${{ steps.filter.outputs.widget }} + steps: + - uses: actions/checkout@v4 + - name: Filter paths + id: filter + uses: dorny/paths-filter@v3 + with: + filters: | + frontend: + - 'frontend/**' + api: + - 'api/**' + widget: + - 'widget/**' + + frontend-build-and-push: + runs-on: ubuntu-latest + if: ${{ needs.paths-filter.outputs.frontend == 'true' }} + steps: + - name: Check out repository code ... + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push docker image + id: docker_build + uses: docker/build-push-action@v6 + with: + context: ./frontend + platforms: linux/amd64,linux/arm64 + push: true + tags: hexastack/hexabot-frontend:latest + + api-build-and-push: + runs-on: ubuntu-latest + if: ${{ needs.paths-filter.outputs.api == 'true' }} + steps: + - name: Check out repository code ... + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push docker image + id: docker_build + uses: docker/build-push-action@v6 + with: + context: ./api + platforms: linux/amd64,linux/arm64 + push: true + tags: hexastack/hexabot-api:latest + + widget-build-and-push: + runs-on: ubuntu-latest + if: ${{ needs.paths-filter.outputs.widget == 'true' }} + steps: + - name: Check out repository code ... + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push docker image + id: docker_build + uses: docker/build-push-action@v6 + with: + context: ./widget + platforms: linux/amd64,linux/arm64 + push: true + tags: hexastack/hexabot-widget:latest From 7495dfef2aaad29e0fec25a28715a0fd09215a02 Mon Sep 17 00:00:00 2001 From: medtaher Date: Wed, 18 Sep 2024 14:00:38 +0100 Subject: [PATCH 02/14] fix: remove extra rerendering when position changes --- frontend/src/components/visual-editor/v2/Diagrams.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/visual-editor/v2/Diagrams.tsx b/frontend/src/components/visual-editor/v2/Diagrams.tsx index 0e9aac32..b12e210f 100644 --- a/frontend/src/components/visual-editor/v2/Diagrams.tsx +++ b/frontend/src/components/visual-editor/v2/Diagrams.tsx @@ -48,7 +48,6 @@ import { IBlock } from "@/types/block.types"; import { ICategory } from "@/types/category.types"; import { BlockPorts } from "@/types/visual-editor.types"; - import BlockDialog from "../BlockDialog"; import { ZOOM_LEVEL } from "../constants"; import { useVisualEditor } from "../hooks/useVisualEditor"; @@ -273,7 +272,13 @@ const Diagrams = () => { zoomUpdated: debouncedZoomEvent, offsetUpdated: debouncedOffsetEvent, }); - }, [JSON.stringify(blocks)]); + }, [ + JSON.stringify( + blocks.map((b) => { + return { ...b, position: undefined, updatedAt: undefined }; + }), + ), + ]); const handleDeleteButton = () => { const selectedEntities = engine?.getModel().getSelectedEntities(); From cc36b16d316b3915e1136af239accc00eb5863eb Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi Date: Wed, 18 Sep 2024 17:33:12 +0100 Subject: [PATCH 03/14] fix: smtp config --- Makefile | 11 +++- api/src/app.module.ts | 40 ++++++++------- api/src/app.service.ts | 9 +--- api/src/config/i18n/en/messages.json | 9 ++-- api/src/config/i18n/fr/messages.json | 3 ++ api/src/config/index.ts | 31 ++++-------- api/src/config/types.ts | 27 ++-------- api/src/extended-i18n.service.ts | 8 +-- api/src/setting/schemas/types.ts | 6 --- api/src/setting/seeds/setting.seed-model.ts | 50 ------------------- api/src/templates/account_confirmation.mjml | 6 +-- api/src/templates/invitation.mjml | 4 +- api/src/templates/password_reset.mjml | 6 +-- api/src/user/services/invitation.service.ts | 43 +++++++++------- .../user/services/passwordReset.service.ts | 44 +++++++++------- .../user/services/validate-account.service.ts | 26 ++++++---- docker/.env.example | 4 +- docker/docker-compose.dev.yml | 18 ------- docker/docker-compose.smtp4dev.yml | 20 ++++++++ frontend/src/i18n/en/translation.json | 3 +- frontend/src/i18n/fr/translation.json | 2 +- 21 files changed, 157 insertions(+), 213 deletions(-) create mode 100644 docker/docker-compose.smtp4dev.yml diff --git a/Makefile b/Makefile index 9e7ba3be..1167bb80 100644 --- a/Makefile +++ b/Makefile @@ -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 \ No newline at end of file + docker-compose $(COMPOSE_FILES) up --no-deps -d database-init diff --git a/api/src/app.module.ts b/api/src/app.module.ts index 5ee186b7..53f6ef07 100644 --- a/api/src/app.module.ts +++ b/api/src/app.module.ts @@ -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) => { diff --git a/api/src/app.service.ts b/api/src/app.service.ts index 09a07e5e..a78e0d3d 100644 --- a/api/src/app.service.ts +++ b/api/src/app.service.ts @@ -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' }); } } diff --git a/api/src/config/i18n/en/messages.json b/api/src/config/i18n/en/messages.json index efb9d1cd..c9ecb497 100644 --- a/api/src/config/i18n/en/messages.json +++ b/api/src/config/i18n/en/messages.json @@ -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," } diff --git a/api/src/config/i18n/fr/messages.json b/api/src/config/i18n/fr/messages.json index 5280a1fa..5a09570f 100644 --- a/api/src/config/i18n/fr/messages.json +++ b/api/src/config/i18n/fr/messages.json @@ -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", diff --git a/api/src/config/index.ts b/api/src/config/index.ts index 98d541c5..3b51ff4e 100644 --- a/api/src/config/index.ts +++ b/api/src/config/index.ts @@ -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, diff --git a/api/src/config/types.ts b/api/src/config/types.ts index 80150cec..971a1be4 100644 --- a/api/src/config/types.ts +++ b/api/src/config/types.ts @@ -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; + 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; diff --git a/api/src/extended-i18n.service.ts b/api/src/extended-i18n.service.ts index 64b7a1c7..5bb4e3d7 100644 --- a/api/src/extended-i18n.service.ts +++ b/api/src/extended-i18n.service.ts @@ -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); diff --git a/api/src/setting/schemas/types.ts b/api/src/setting/schemas/types.ts index f81c7352..f13f849e 100644 --- a/api/src/setting/schemas/types.ts +++ b/api/src/setting/schemas/types.ts @@ -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; diff --git a/api/src/setting/seeds/setting.seed-model.ts b/api/src/setting/seeds/setting.seed-model.ts index 3103685a..855aafa6 100644 --- a/api/src/setting/seeds/setting.seed-model.ts +++ b/api/src/setting/seeds/setting.seed-model.ts @@ -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', diff --git a/api/src/templates/account_confirmation.mjml b/api/src/templates/account_confirmation.mjml index dbb9f89c..cf10ff87 100644 --- a/api/src/templates/account_confirmation.mjml +++ b/api/src/templates/account_confirmation.mjml @@ -24,10 +24,10 @@ - <%= t('best_regards') %>,<%= t('best_regards') %> - <%= this.appName %> diff --git a/api/src/templates/invitation.mjml b/api/src/templates/invitation.mjml index 197d78d4..e608bc0d 100644 --- a/api/src/templates/invitation.mjml +++ b/api/src/templates/invitation.mjml @@ -26,11 +26,11 @@ - <%= t('best_regards') %> - <%= this.appName %> diff --git a/api/src/templates/password_reset.mjml b/api/src/templates/password_reset.mjml index 0ea9794d..34975029 100644 --- a/api/src/templates/password_reset.mjml +++ b/api/src/templates/password_reset.mjml @@ -23,10 +23,10 @@ - <%= t('best_regards') %>,<%= t('best_regards') %> - <%= this.appName %> diff --git a/api/src/user/services/invitation.service.ts b/api/src/user/services/invitation.service.ts index db0c8a71..84bc02cc 100644 --- a/api/src/user/services/invitation.service.ts +++ b/api/src/user/services/invitation.service.ts @@ -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 { @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 { */ async create(dto: InvitationCreateDto): Promise { 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 }; diff --git a/api/src/user/services/passwordReset.service.ts b/api/src/user/services/passwordReset.service.ts index 0143c1c1..60143f82 100644 --- a/api/src/user/services/passwordReset.service.ts +++ b/api/src/user/services/passwordReset.service.ts @@ -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 diff --git a/api/src/user/services/validate-account.service.ts b/api/src/user/services/validate-account.service.ts index 3dcdf80a..4c2d06c6 100644 --- a/api/src/user/services/validate-account.service.ts +++ b/api/src/user/services/validate-account.service.ts @@ -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'), + }); + } } /** diff --git a/docker/.env.example b/docker/.env.example index 2104dcc3..8e4f837e 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -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 diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 724b6568..3ba63cc2 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -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: diff --git a/docker/docker-compose.smtp4dev.yml b/docker/docker-compose.smtp4dev.yml new file mode 100644 index 00000000..4db737a4 --- /dev/null +++ b/docker/docker-compose.smtp4dev.yml @@ -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: diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index bbe36a8e..250e5d45 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -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", diff --git a/frontend/src/i18n/fr/translation.json b/frontend/src/i18n/fr/translation.json index 2cc08f8f..e9fb58e8 100644 --- a/frontend/src/i18n/fr/translation.json +++ b/frontend/src/i18n/fr/translation.json @@ -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", From eb3f53d787140aaed1a4dbfec88bc41d0c33e9a3 Mon Sep 17 00:00:00 2001 From: Emnaghz Date: Thu, 19 Sep 2024 11:25:09 +0100 Subject: [PATCH 04/14] feat: optimize workflow --- .github/workflows/merge.yml | 83 +++++++++++-------------------------- 1 file changed, 25 insertions(+), 58 deletions(-) diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml index bb32564c..56281097 100644 --- a/.github/workflows/merge.yml +++ b/.github/workflows/merge.yml @@ -1,8 +1,8 @@ name: Build and Push Docker Image On Merge + on: push: branches: ['main'] - workflow_dispatch: jobs: @@ -21,91 +21,58 @@ jobs: filters: | frontend: - 'frontend/**' - api: + api: - 'api/**' widget: - 'widget/**' - frontend-build-and-push: + build-and-push: runs-on: ubuntu-latest - if: ${{ needs.paths-filter.outputs.frontend == 'true' }} + needs: + - paths-filter steps: - - name: Check out repository code ... + - name: Check out repository code uses: actions/checkout@v4 - + - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - + - name: Login to Docker Hub - uses: docker/login-action@v3 + id: docker_login + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push docker image - id: docker_build + - name: Build and push frontend Docker image + if: ${{ needs.paths-filter.outputs.frontend == 'true' }} uses: docker/build-push-action@v6 with: - context: ./frontend + context: ./ + file: ./frontend/Dockerfile platforms: linux/amd64,linux/arm64 push: true - tags: hexastack/hexabot-frontend:latest - - api-build-and-push: - runs-on: ubuntu-latest - if: ${{ needs.paths-filter.outputs.api == 'true' }} - steps: - - name: Check out repository code ... - uses: actions/checkout@v4 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + tags: ${{ secrets.DOCKERHUB_USERNAME }}/hexabot-frontend:latest - - name: Build and push docker image - id: docker_build + - name: Build and push API Docker image + if: ${{ needs.paths-filter.outputs.api == 'true' }} uses: docker/build-push-action@v6 with: - context: ./api + context: ./ + file: ./api/Dockerfile platforms: linux/amd64,linux/arm64 push: true - tags: hexastack/hexabot-api:latest - - widget-build-and-push: - runs-on: ubuntu-latest - if: ${{ needs.paths-filter.outputs.widget == 'true' }} - steps: - - name: Check out repository code ... - uses: actions/checkout@v4 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + tags: ${{ secrets.DOCKERHUB_USERNAME }}/hexabot-api:latest - - name: Build and push docker image - id: docker_build + - name: Build and push widget Docker image + if: ${{ needs.paths-filter.outputs.widget == 'true' }} uses: docker/build-push-action@v6 with: - context: ./widget + context: ./ + file: ./widget/Dockerfile platforms: linux/amd64,linux/arm64 push: true - tags: hexastack/hexabot-widget:latest + tags: ${{ secrets.DOCKERHUB_USERNAME }}/hexabot-widget:latest From aa05fe1704b67ffbe1e690b4dbc7854174864a4d Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi Date: Tue, 17 Sep 2024 17:12:20 +0100 Subject: [PATCH 05/14] fix: load config on runtime --- docker/docker-compose.dev.yml | 5 +++ docker/docker-compose.yml | 8 +---- frontend/Dockerfile | 12 ------- frontend/next.config.mjs | 8 +++++ frontend/src/hooks/useConfig.tsx | 50 ++++++++++++++++++++++++++++ frontend/src/pages/_app.tsx | 57 +++++++++++++++++--------------- frontend/src/pages/api/config.ts | 16 +++++++++ 7 files changed, 110 insertions(+), 46 deletions(-) create mode 100644 frontend/src/hooks/useConfig.tsx create mode 100644 frontend/src/pages/api/config.ts diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 3ba63cc2..b4171110 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -16,6 +16,11 @@ services: - ../api/migrations:/app/migrations #- ../api/node_modules:/app/node_modules command: ["npm", "run", "start:debug"] + + hexabot-frontend: + build: + context: ../ + dockerfile: ./frontend/Dockerfile mongo-express: container_name: mongoUi diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 80102c66..5549acf3 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -30,7 +30,6 @@ services: condition: service_healthy database-init: condition: service_completed_successfully - healthcheck: test: "wget --spider http://localhost:3000" interval: 10s @@ -40,12 +39,7 @@ services: hexabot-frontend: container_name: frontend - build: - context: ../ - dockerfile: ./frontend/Dockerfile - args: - - NEXT_PUBLIC_API_ORIGIN=${NEXT_PUBLIC_API_ORIGIN} - - NEXT_PUBLIC_SSO_ENABLED=${NEXT_PUBLIC_SSO_ENABLED} + image: hexabot-ui:latest env_file: .env ports: - ${APP_FRONTEND_PORT}:8080 diff --git a/frontend/Dockerfile b/frontend/Dockerfile index cea1c11b..d8d812c6 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -21,14 +21,6 @@ RUN \ # Rebuild the source code only when needed FROM base AS builder -ARG NEXT_PUBLIC_API_ORIGIN -ENV NEXT_PUBLIC_API_ORIGIN=${NEXT_PUBLIC_API_ORIGIN} -ARG NEXT_PUBLIC_SSO_ENABLED -ENV NEXT_PUBLIC_SSO_ENABLED=${NEXT_PUBLIC_SSO_ENABLED} - -ENV REACT_APP_WIDGET_API_URL=${NEXT_PUBLIC_API_ORIGIN} -ENV REACT_APP_WIDGET_CHANNEL=test -ENV REACT_APP_WIDGET_TOKEN=test WORKDIR /app @@ -57,10 +49,6 @@ ENV NODE_ENV production # Uncomment the following line in case you want to disable telemetry during runtime. ENV NEXT_TELEMETRY_DISABLED 1 -# Set the environment variable API_ORIGIN -ENV NEXT_PUBLIC_API_ORIGIN ${NEXT_PUBLIC_API_ORIGIN:-"http://localhost:3000"} -ENV NEXT_PUBLIC_SSO_ENABLED ${NEXT_PUBLIC_SSO_ENABLED:-"false"} - RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index 774cf83f..f1c7e508 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -4,6 +4,14 @@ import withTM from "next-transpile-modules"; const apiUrl = process.env.NEXT_PUBLIC_API_ORIGIN || "http://localhost:4000/"; const url = new URL(apiUrl); const nextConfig = withTM(["hexabot-widget"])({ + async rewrites() { + return [ + { + source: "/config", + destination: "/api/config", + }, + ]; + }, webpack(config, _options) { return config; }, diff --git a/frontend/src/hooks/useConfig.tsx b/frontend/src/hooks/useConfig.tsx new file mode 100644 index 00000000..f8a4fd94 --- /dev/null +++ b/frontend/src/hooks/useConfig.tsx @@ -0,0 +1,50 @@ +import { createContext, useContext, useEffect, useState } from "react"; + +const ConfigContext = createContext(null); + +export interface IConfig { + NEXT_PUBLIC_API_ORIGIN: string; + NEXT_PUBLIC_SSO_ENABLED: boolean; + REACT_APP_WIDGET_API_URL: string; + REACT_APP_WIDGET_CHANNEL: string; + REACT_APP_WIDGET_TOKEN: string; +} + +export const ConfigProvider = ({ children }) => { + const [config, setConfig] = useState(null); + + useEffect(() => { + const fetchConfig = async () => { + try { + const res = await fetch("/config"); + const data = await res.json(); + + setConfig(data); + } catch (error) { + // eslint-disable-next-line no-console + console.error("Failed to fetch configuration:", error); + } + }; + + fetchConfig(); + }, []); + + if (!config) { + // You can return a loader here if you want + return null; + } + + return ( + {children} + ); +}; + +export const useConfig = () => { + const context = useContext(ConfigContext); + + if (!context) { + throw new Error("useConfig must be used within a ConfigProvider"); + } + + return context; +}; diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index ad0bbab1..b93e392d 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -20,6 +20,7 @@ import { ReactQueryDevtools } from "react-query/devtools"; import { SnackbarCloseButton } from "@/app-components/displays/Toast/CloseButton"; import { ApiClientProvider } from "@/hooks/useApiClient"; import { AuthProvider } from "@/hooks/useAuth"; +import { ConfigProvider } from "@/hooks/useConfig"; import { PermissionProvider } from "@/hooks/useHasPermission"; import { SettingsProvider } from "@/hooks/useSetting"; import { ToastProvider } from "@/hooks/useToast"; @@ -69,33 +70,35 @@ const App = ({ Component, pageProps }: TAppPropsWithLayout) => { />
- - ( - - )} - > - - - - - - - - - {getLayout()} - - - - - - - - - - + + + ( + + )} + > + + + + + + + + + {getLayout()} + + + + + + + + + + +
); diff --git a/frontend/src/pages/api/config.ts b/frontend/src/pages/api/config.ts new file mode 100644 index 00000000..2af6d1ab --- /dev/null +++ b/frontend/src/pages/api/config.ts @@ -0,0 +1,16 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +type ResponseData = { + apiUrl: string; + ssoEnabled: boolean; +}; + +export default function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + res.status(200).json({ + apiUrl: process.env.NEXT_PUBLIC_API_ORIGIN || "http://localhost:3000", + ssoEnabled: process.env.NEXT_PUBLIC_SSO_ENABLED === "true" || false, + }); +} From cdf846131211e7f295f3d4bf1e1858e6ad47cc36 Mon Sep 17 00:00:00 2001 From: Yassine Sallemi Date: Fri, 20 Sep 2024 16:21:30 +0100 Subject: [PATCH 06/14] fix: config fetched on runtime --- frontend/next.config.mjs | 20 ---------------- .../tables/columns/renderPicture.tsx | 4 ++++ .../src/components/inbox/components/Chat.tsx | 10 ++++++-- .../inbox/components/ChatActions.tsx | 4 +++- .../inbox/components/ConversationsList.tsx | 3 +++ .../components/inbox/helpers/mapMessages.tsx | 12 ++++------ .../components/nlp/components/NlpSample.tsx | 6 ++--- frontend/src/components/users/index.tsx | 11 ++++----- frontend/src/hooks/useApiClient.tsx | 7 +++--- frontend/src/hooks/useConfig.tsx | 9 +++---- frontend/src/layout/Header.tsx | 9 ++++--- frontend/src/layout/VerticalMenu.tsx | 11 +++++---- frontend/src/pages/api/config.ts | 2 +- frontend/src/pages/visual-editor.tsx | 9 +++---- frontend/src/websocket/SocketIoClient.ts | 9 ++----- frontend/src/websocket/socket-hooks.tsx | 24 ++++++++++++------- 16 files changed, 70 insertions(+), 80 deletions(-) diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index f1c7e508..6d0080df 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -1,8 +1,6 @@ /** @type {import('next').NextConfig} */ import withTM from "next-transpile-modules"; -const apiUrl = process.env.NEXT_PUBLIC_API_ORIGIN || "http://localhost:4000/"; -const url = new URL(apiUrl); const nextConfig = withTM(["hexabot-widget"])({ async rewrites() { return [ @@ -16,29 +14,11 @@ const nextConfig = withTM(["hexabot-widget"])({ return config; }, publicRuntimeConfig: { - apiUrl, - ssoEnabled: process.env.NEXT_PUBLIC_SSO_ENABLED === "true", lang: { default: "en", }, }, output: "standalone", - images: { - remotePatterns: [ - { - protocol: "https", - hostname: url.hostname, - port: url.port, - pathname: "/attachment/**", - }, - { - protocol: "http", - hostname: url.hostname, - port: url.port, - pathname: "/attachment/**", - }, - ], - }, }); export default nextConfig; diff --git a/frontend/src/app-components/tables/columns/renderPicture.tsx b/frontend/src/app-components/tables/columns/renderPicture.tsx index 33af1740..3684a00a 100644 --- a/frontend/src/app-components/tables/columns/renderPicture.tsx +++ b/frontend/src/app-components/tables/columns/renderPicture.tsx @@ -11,12 +11,15 @@ import { Grid } from "@mui/material"; import { GridRenderCellParams } from "@mui/x-data-grid"; import { getAvatarSrc } from "@/components/inbox/helpers/mapMessages"; +import { useConfig } from "@/hooks/useConfig"; import { EntityType } from "@/services/types"; export const buildRenderPicture = ( entityType: EntityType.USER | EntityType.SUBSCRIBER, ) => function RenderPicture(params: GridRenderCellParams) { + const { apiUrl } = useConfig(); + return ( @@ -118,6 +123,7 @@ export function Chat() { i18n.language, )}`} src={getAvatarSrc( + apiUrl, message.sender ? EntityType.SUBSCRIBER : EntityType.USER, diff --git a/frontend/src/components/inbox/components/ChatActions.tsx b/frontend/src/components/inbox/components/ChatActions.tsx index b91f5cc9..605ab2e4 100644 --- a/frontend/src/components/inbox/components/ChatActions.tsx +++ b/frontend/src/components/inbox/components/ChatActions.tsx @@ -18,12 +18,14 @@ import { Input } from "@/app-components/inputs/Input"; import { useFind } from "@/hooks/crud/useFind"; import { useUpdate } from "@/hooks/crud/useUpdate"; import { useAuth } from "@/hooks/useAuth"; +import { useConfig } from "@/hooks/useConfig"; import { EntityType } from "@/services/types"; import { getAvatarSrc } from "../helpers/mapMessages"; import { useChat } from "../hooks/ChatContext"; export const ChatActions = () => { + const { apiUrl } = useConfig(); const { t } = useTranslation(); const { subscriber: activeChat } = useChat(); const [takeoverBy, setTakeoverBy] = useState( @@ -57,7 +59,7 @@ export const ChatActions = () => { diff --git a/frontend/src/components/inbox/components/ConversationsList.tsx b/frontend/src/components/inbox/components/ConversationsList.tsx index b032f514..666cadd4 100644 --- a/frontend/src/components/inbox/components/ConversationsList.tsx +++ b/frontend/src/components/inbox/components/ConversationsList.tsx @@ -16,6 +16,7 @@ import InboxIcon from "@mui/icons-material/MoveToInbox"; import { Chip, debounce, Grid } from "@mui/material"; import { useTranslation } from "react-i18next"; +import { useConfig } from "@/hooks/useConfig"; import { Title } from "@/layout/content/Title"; import { EntityType } from "@/services/types"; @@ -29,6 +30,7 @@ export const SubscribersList = (props: { searchPayload: any; assignedTo: AssignedTo; }) => { + const { apiUrl } = useConfig(); const { t, i18n } = useTranslation(); const chat = useChat(); const { fetchNextPage, isFetching, subscribers, hasNextPage } = @@ -58,6 +60,7 @@ export const SubscribersList = (props: { > } diff --git a/frontend/src/components/users/index.tsx b/frontend/src/components/users/index.tsx index a77391bf..6cde7a77 100644 --- a/frontend/src/components/users/index.tsx +++ b/frontend/src/components/users/index.tsx @@ -11,7 +11,6 @@ import { faUsers } from "@fortawesome/free-solid-svg-icons"; import PersonAddAlt1Icon from "@mui/icons-material/PersonAddAlt1"; import { Button, Grid, Paper } from "@mui/material"; import { GridColDef } from "@mui/x-data-grid"; -import getConfig from "next/config"; import { useTranslation } from "react-i18next"; import { ChipEntity } from "@/app-components/displays/ChipEntity"; @@ -25,6 +24,7 @@ import { buildRenderPicture } from "@/app-components/tables/columns/renderPictur import { DataGrid } from "@/app-components/tables/DataGrid"; import { useFind } from "@/hooks/crud/useFind"; import { useUpdate } from "@/hooks/crud/useUpdate"; +import { useConfig } from "@/hooks/useConfig"; import { getDisplayDialogs, useDialog } from "@/hooks/useDialog"; import { useHasPermission } from "@/hooks/useHasPermission"; import { useSearch } from "@/hooks/useSearch"; @@ -39,9 +39,8 @@ import { getDateTimeFormatter } from "@/utils/date"; import { EditUserDialog } from "./EditUserDialog"; import { InvitationDialog } from "./InvitationDialog"; -const { publicRuntimeConfig } = getConfig(); - export const Users = () => { + const { ssoEnabled } = useConfig(); const { t } = useTranslation(); const { toast } = useToast(); const { mutateAsync: updateUser } = useUpdate(EntityType.USER, { @@ -157,7 +156,7 @@ export const Users = () => { }, }); }} - disabled={publicRuntimeConfig.ssoEnabled} + disabled={ssoEnabled} > {t(params.row.state ? "label.enabled" : "label.disabled")} @@ -188,7 +187,7 @@ export const Users = () => { valueGetter: (params) => t("datetime.updated_at", getDateTimeFormatter(params)), }, - ...(!publicRuntimeConfig.ssoEnabled ? [actionColumns] : []), + ...(!ssoEnabled ? [actionColumns] : []), ]; return ( @@ -207,7 +206,7 @@ export const Users = () => { - {!publicRuntimeConfig.ssoEnabled && + {!ssoEnabled && hasPermission(EntityType.USER, PermissionAction.CREATE) ? (