From 03a8cbcec0f3eaccc63a4d5aedf8658988bfb713 Mon Sep 17 00:00:00 2001 From: Antoine Jaussoin Date: Sat, 9 Apr 2022 12:58:24 +0100 Subject: [PATCH] Adding support for SMTP-based emails (#383) * Introducing smtp sender * Remove old sendgrid config * alpha * Email doc * copy templates * Readme and versions * lock versions * remove stuff * Customise quick start * Moving some options to advanced settings * Improve docker compose editor --- .env.example | 11 +- .github/workflows/alpha.yml | 2 +- README.md | 2 + backend/package.json | 13 +- backend/src/admin/router.ts | 7 +- backend/src/auth/register/register-user.ts | 7 +- backend/src/common/payloads.ts | 2 +- backend/src/config.ts | 12 +- backend/src/email/emailSender.ts | 128 +++---- backend/src/email/register.template.html | 5 + .../src/email/reset-password.template.html | 5 + backend/src/email/self-hosted.template.html | 321 ++++++++++++++++++ backend/src/email/sendgrid-sender.ts | 28 ++ backend/src/email/smtp-sender.ts | 27 ++ backend/src/email/template-loader.ts | 46 +++ backend/src/email/types.ts | 5 + backend/src/email/utils.ts | 5 + backend/src/types.ts | 9 +- backend/yarn.lock | 147 +++++++- docs/docs/self-hosting/optionals.md | 9 +- docs/docs/self-hosting/passwords.md | 2 +- .../quick-start/Accordion.module.css | 16 + .../self-hosting/quick-start/Accordion.tsx | 21 ++ .../self-hosting/quick-start/ComposeView.tsx | 72 +++- docs/docs/self-hosting/quick-start/Editor.tsx | 209 ++++++++++-- docs/docs/self-hosting/quick-start/Field.tsx | 2 +- docs/docs/self-hosting/quick-start/Toggle.tsx | 35 ++ docs/docs/self-hosting/sendgrid.mdx | 71 ++-- docs/package.json | 2 +- frontend/package.json | 10 +- .../src/auth/modal/account/LostPassword.tsx | 2 +- frontend/src/common/payloads.ts | 2 +- frontend/src/global/state.ts | 2 +- frontend/yarn.lock | 8 +- integration/package.json | 2 +- package.json | 2 +- self-hosting/docker-compose.full.yml | 9 +- 37 files changed, 1057 insertions(+), 201 deletions(-) create mode 100644 backend/src/email/register.template.html create mode 100644 backend/src/email/reset-password.template.html create mode 100644 backend/src/email/self-hosted.template.html create mode 100644 backend/src/email/sendgrid-sender.ts create mode 100644 backend/src/email/smtp-sender.ts create mode 100644 backend/src/email/template-loader.ts create mode 100644 backend/src/email/types.ts create mode 100644 backend/src/email/utils.ts create mode 100644 docs/docs/self-hosting/quick-start/Accordion.module.css create mode 100644 docs/docs/self-hosting/quick-start/Accordion.tsx create mode 100644 docs/docs/self-hosting/quick-start/Toggle.tsx diff --git a/.env.example b/.env.example index 132a5d134..c83a3d699 100644 --- a/.env.example +++ b/.env.example @@ -32,10 +32,13 @@ OKTA_SECRET= BASE_URL=http://localhost:3000 SECURE_COOKIES=false SENDGRID_API_KEY= -SENDGRID_SENDER= -SENDGRID_VERIFICATION_EMAIL_TID= -SENDGRID_RESET_PASSWORD_TID= -SENDGRID_SELF_HOST_EMAIL_TID= +SENDGRID_SENDER=you@email.com +MAIL_SMTP_HOST= +MAIL_PORT=465 +MAIL_SECURE=true +MAIL_USER= +MAIL_PASSWORD= +MAIL_SENDER=you@email.com STRIPE_SECRET= STRIPE_KEY= STRIPE_WEBHOOK_SECRET= diff --git a/.github/workflows/alpha.yml b/.github/workflows/alpha.yml index d1bb2b942..6d31efb97 100644 --- a/.github/workflows/alpha.yml +++ b/.github/workflows/alpha.yml @@ -2,7 +2,7 @@ name: 'Alpha Build' on: push: - branches: [v4140/integration] + branches: [v4140/x] jobs: build: diff --git a/README.md b/README.md index 1ddae014d..5b7b8da6a 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,8 @@ This will run a demo version, which you can turn into a fully licenced version b - Upgrade to React 18 - Replace icons by emoji for columns headers (fully customisable) +- Adding SMTP support for self-hosting, in addition to SendGrid +- Simplification of SendGrid setup, by removing the need of creating email templates. They are now hardcoded. ### Version 4.13.0 diff --git a/backend/package.json b/backend/package.json index 942b8c6c7..0f0805408 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,10 +1,10 @@ { "name": "@retrospected/backend", - "version": "4.13.0", + "version": "4.14.0", "license": "GNU GPLv3", "private": true, "scripts": { - "build": "rimraf dist && tsc --build", + "build": "rimraf dist && tsc --build && yarn copy-templates", "start": "nodemon --exec 'yarn fix & ts-node' --files ./src/index.ts", "create-migration": "ts-node ./node_modules/typeorm/cli.js --config src/db/orm-config.ts migration:generate -n ", "create-empty-migration": "ts-node ./node_modules/typeorm/cli.js --config src/db/orm-config.ts migration:create -n ", @@ -15,7 +15,8 @@ "ci-test": "CI=true yarn test", "fix": "eslint 'src/**/*.ts' --fix", "backend-production": "yarn migrate-production && cd ./dist/src && node index.js", - "migrate-production": "node ./node_modules/typeorm/cli.js --config dist/src/db/orm-config.js migration:run" + "migrate-production": "node ./node_modules/typeorm/cli.js --config dist/src/db/orm-config.js migration:run", + "copy-templates": "copyfiles -u 0 src/**/*.html dist/" }, "dependencies": { "@sendgrid/mail": "7.6.2", @@ -32,6 +33,7 @@ "@types/lodash": "4.14.181", "@types/node": "17.0.23", "@types/node-fetch": "2.5.12", + "@types/nodemailer": "6.4.4", "@types/passport": "1.0.7", "@types/passport-github2": "1.2.5", "@types/passport-google-oauth20": "2.0.11", @@ -46,6 +48,7 @@ "bcryptjs": "2.4.3", "chalk": "4.1.2", "connect-redis": "6.1.3", + "copyfiles": "2.4.1", "crypto-js": "4.1.1", "csurf": "1.11.0", "date-fns": "2.28.0", @@ -58,11 +61,13 @@ "express-rate-limit": "6.3.0", "express-session": "1.17.2", "freemail": "1.7.0", + "handlebars": "4.7.7", "jest": "27.5.1", "lexorank": "1.0.4", "lodash": "4.17.21", "moment": "2.29.1", "node-fetch": "2.6.7", + "nodemailer": "6.7.3", "nodemon": "2.0.15", "passport": "0.5.0", "passport-github2": "0.1.12", @@ -84,7 +89,7 @@ "ts-jest": "27.1.4", "ts-node": "10.7.0", "typeorm": "0.2.45", - "typeorm-naming-strategies": "^3.0.0", + "typeorm-naming-strategies": "3.0.0", "typescript": "4.6.3", "uuid": "8.3.2" }, diff --git a/backend/src/admin/router.ts b/backend/src/admin/router.ts index 5ad6a45c2..2754b9f18 100644 --- a/backend/src/admin/router.ts +++ b/backend/src/admin/router.ts @@ -9,6 +9,7 @@ import { isLicenced } from '../security/is-licenced'; import { AdminChangePasswordPayload, BackendCapabilities } from '../common'; import { getIdentityFromRequest, hashPassword } from '../utils'; import csurf from 'csurf'; +import { canSendEmails } from '../email/utils'; const router = express.Router(); const csrfProtection = csurf(); @@ -19,11 +20,7 @@ router.get('/self-hosting', async (_, res) => { adminEmail: config.SELF_HOSTED_ADMIN, selfHosted: config.SELF_HOSTED, licenced: !!licence, - sendGridAvailable: - !!config.SENDGRID_API_KEY && - !!config.SENDGRID_RESET_PASSWORD_TID && - !!config.SENDGRID_VERIFICATION_EMAIL_TID && - !!config.SENDGRID_SENDER, + emailAvailable: canSendEmails(), disableAnonymous: config.DISABLE_ANONYMOUS_LOGIN, disablePasswords: config.DISABLE_PASSWORD_LOGIN, disablePasswordRegistration: config.DISABLE_PASSWORD_REGISTRATION, diff --git a/backend/src/auth/register/register-user.ts b/backend/src/auth/register/register-user.ts index ddfba0d91..77a5c3a7e 100644 --- a/backend/src/auth/register/register-user.ts +++ b/backend/src/auth/register/register-user.ts @@ -3,7 +3,7 @@ import { v4 } from 'uuid'; import { hashPassword } from '../../utils'; import { UserIdentityEntity } from '../../db/entities'; import { getIdentityByUsername, registerUser } from '../../db/actions/users'; -import config from '../../config'; +import { canSendEmails } from '../../email/utils'; export default async function registerPasswordUser( details: RegisterPayload @@ -24,10 +24,7 @@ export default async function registerPasswordUser( type: 'password', username: details.username, password: hashedPassword, - emailVerification: - config.SENDGRID_API_KEY && config.SENDGRID_VERIFICATION_EMAIL_TID - ? v4() - : undefined, + emailVerification: canSendEmails() ? v4() : undefined, language: details.language, }); diff --git a/backend/src/common/payloads.ts b/backend/src/common/payloads.ts index 053206fa8..a0041f009 100644 --- a/backend/src/common/payloads.ts +++ b/backend/src/common/payloads.ts @@ -53,7 +53,7 @@ export interface SelfHostedCheckPayload { export interface BackendCapabilities { selfHosted: boolean; - sendGridAvailable: boolean; + emailAvailable: boolean; adminEmail: string; licenced: boolean; oAuth: OAuthAvailabilities; diff --git a/backend/src/config.ts b/backend/src/config.ts index 0ff839027..0bf5a5941 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -85,12 +85,6 @@ const config: BackendConfig = { OKTA_SECRET: defaults('OKTA_SECRET', ''), SENDGRID_API_KEY: defaults('SENDGRID_API_KEY', ''), SENDGRID_SENDER: defaults('SENDGRID_SENDER', ''), - SENDGRID_VERIFICATION_EMAIL_TID: defaults( - 'SENDGRID_VERIFICATION_EMAIL_TID', - '' - ), - SENDGRID_RESET_PASSWORD_TID: defaults('SENDGRID_RESET_PASSWORD_TID', ''), - SENDGRID_SELF_HOST_EMAIL_TID: defaults('SENDGRID_SELF_HOST_EMAIL_TID', ''), STRIPE_SECRET: defaults('STRIPE_SECRET', ''), STRIPE_WEBHOOK_SECRET: defaults('STRIPE_WEBHOOK_SECRET', ''), STRIPE_TEAM_PRODUCT: defaults('STRIPE_TEAM_PRODUCT', ''), @@ -106,6 +100,12 @@ const config: BackendConfig = { RATE_LIMIT_WS_POINTS: defaultsNumber('RATE_LIMIT_WS_POINTS', 600), RATE_LIMIT_WS_DURATION: defaultsNumber('RATE_LIMIT_WS_DURATION', 60), WS_MAX_BUFFER_SIZE: defaultsNumber('WS_MAX_BUFFER_SIZE', 10_000), + MAIL_SMTP_HOST: defaults('MAIL_SMTP_HOST', ''), + MAIL_PORT: defaultsNumber('MAIL_PORT', 465), + MAIL_SECURE: defaultsBool('MAIL_SECURE', true), + MAIL_SENDER: defaults('MAIL_SENDER', ''), + MAIL_USER: defaults('MAIL_USER', ''), + MAIL_PASSWORD: defaults('MAIL_PASSWORD', ''), }; export default config; diff --git a/backend/src/email/emailSender.ts b/backend/src/email/emailSender.ts index 8592fe4b2..7d65bd301 100644 --- a/backend/src/email/emailSender.ts +++ b/backend/src/email/emailSender.ts @@ -1,66 +1,75 @@ -import sendGrid, { MailDataRequired } from '@sendgrid/mail'; +import chalk from 'chalk'; import config from '../config'; import randomWords from './random-words'; +import { sendGridSender } from './sendgrid-sender'; +import { smtpSender } from './smtp-sender'; +import { + getPasswordResetTemplate, + getRegisterTemplate, + getSelfHostedWelcomeEmailTemplate, +} from './template-loader'; +import { EmailSender } from './types'; -if (config.SENDGRID_API_KEY) { - sendGrid.setApiKey(config.SENDGRID_API_KEY); +function getSender(): EmailSender | null { + if (config.SENDGRID_API_KEY) { + console.log( + chalk`📨 {red SendGrid} is going to be used for {yellow emails}` + ); + return sendGridSender; + } else if (config.MAIL_SMTP_HOST) { + console.log(chalk`📨 {red SMTP} is going to be used for {yellow emails}`); + return smtpSender; + } + + console.log( + chalk`📨 {red NO EMAIL PROVIDER CONFIGURED}. It is recommended to setup a SendGrid or SMTP email provider for account management.` + ); + return null; } +const send = getSender(); + export async function sendVerificationEmail( email: string, name: string, code: string ) { - if (!config.SENDGRID_API_KEY) { - throw Error('Sendgrid is not activated.'); + if (!send) { + return; } - const msg: MailDataRequired = { - to: email, - from: config.SENDGRID_SENDER, - templateId: config.SENDGRID_VERIFICATION_EMAIL_TID, - dynamicTemplateData: { - name, - code, - domain: config.BASE_URL, - email: encodeURIComponent(email), - }, - }; - try { - await sendGrid.send(msg); - } catch (e) { - console.error('Send grid error: ', e); + const template = await getRegisterTemplate( + encodeURIComponent(email), + name, + code, + config.BASE_URL + ); + const result = send( + email, + 'Welcome to Retrospected - Verify your email', + template + ); + if (!result) { + console.error('Sending email did not work'); } } -type SendGridError = { - response: { - body: unknown; - }; -}; - export async function sendResetPassword( email: string, name: string, code: string ) { - if (!config.SENDGRID_API_KEY) { - throw Error('Sendgrid is not activated.'); + if (!send) { + return; } - const msg: MailDataRequired = { - to: email, - from: config.SENDGRID_SENDER, - templateId: config.SENDGRID_RESET_PASSWORD_TID, - dynamicTemplateData: { - name, - code, - domain: config.BASE_URL, - email: encodeURIComponent(email), - }, - }; - try { - await sendGrid.send(msg); - } catch (e: unknown) { - console.error('Send grid error: ', e, (e as SendGridError).response.body); + const template = await getPasswordResetTemplate( + encodeURIComponent(email), + name, + code, + config.BASE_URL + ); + const result = send(email, 'Retrospected - Reset your password', template); + if (!result) { + console.error('Sending email did not work'); } } @@ -73,30 +82,23 @@ export async function sendSelfHostWelcome( name: string, key: string ) { - if (!config.SENDGRID_API_KEY) { - throw Error('Sendgrid is not activated.'); + if (!send) { + return; } - const dbPassword = generatePassword(); const pgAdminPassword = generatePassword(); const sessionSecret = generatePassword(); - const msg: MailDataRequired = { - to: email, - from: config.SENDGRID_SENDER, - templateId: config.SENDGRID_SELF_HOST_EMAIL_TID, - dynamicTemplateData: { - name, - key, - dbPassword, - pgAdminPassword, - sessionSecret, - email, - }, - }; - try { - await sendGrid.send(msg); - } catch (e: unknown) { - console.error('Send grid error: ', e, (e as SendGridError).response.body); + const template = await getSelfHostedWelcomeEmailTemplate( + name, + key, + dbPassword, + pgAdminPassword, + sessionSecret, + email + ); + const result = send(email, 'Welcome to Retrospected Self-Hosted!', template); + if (!result) { + console.error('Sending email did not work'); } } diff --git a/backend/src/email/register.template.html b/backend/src/email/register.template.html new file mode 100644 index 000000000..e40c47dcb --- /dev/null +++ b/backend/src/email/register.template.html @@ -0,0 +1,5 @@ +
Hello {{name}}!
+

+
Please click on the email below to validate your email and start using Retrospected right away.
+

+
{{domain}}/validate?email={{email}}&code={{code}}
\ No newline at end of file diff --git a/backend/src/email/reset-password.template.html b/backend/src/email/reset-password.template.html new file mode 100644 index 000000000..822d20981 --- /dev/null +++ b/backend/src/email/reset-password.template.html @@ -0,0 +1,5 @@ +
Hi {{name}},
+

+
To reset your password, follow the link:
+

+
{{domain}}/reset?email={{email}}&code={{code}}
\ No newline at end of file diff --git a/backend/src/email/self-hosted.template.html b/backend/src/email/self-hosted.template.html new file mode 100644 index 000000000..c62eed7de --- /dev/null +++ b/backend/src/email/self-hosted.template.html @@ -0,0 +1,321 @@ + + + + + +

Hello {{name}}!

+ +

+ Thank you so much for purchasing a Self-Hosted licence of Retrospected. +

+ +

+ You can now use Retrospected on your premises, by using the key below: +

+ +
{{key}}
+ +
+

Quick Start

+

+ You can be up and running in 5 minutes by using your personalised + docker-compose file below.
+ Alternatively, if you would like to customise it or get more details + about the installation, please visit our + documentation website.
+ You will also find useful information about + backup, + database management, etc. +

+
+ +

+ +
+ +

Quick Start

+ +

+ You'll be up and running in no time. Your + docker-compose.yml file is ready to go. +

+ +
+

1 - Prerequisites

+ +

+ Install + Docker and + Docker Compose. +

+
+ +
+

2 - Install your docker-compose file

+ +
    +
  1. + Create an empty docker-compose.yml file +
  2. +
  3. + Copy the content at the end of this email, and paste it in that file + you created. +
  4. +
  5. + + Optional: Change the various passwords, ports and other + settings. +
  6. +
  7. + In the same directory, do + docker-compose up -d +
  8. +
  9. You're done!
  10. +
+
+

Warning

+

+ If you wish to change the default passwords, it is very important that + you change them + BEFORE running docker-compose for the first time. Once run, all + passwords are set and modifying them later on will not work (it will + actually fail to run). +

+
+
+ +
+

3 - Enjoy!

+ +

+ Retrospected is now running, on the port defined in your + docker-compose.yml file (1800 by default). +

+

+ You can also access + PGAdmin, which will give you access to your database. PGAdmin is available on + port 1801 (by default), and you can use the following credentials if you + didn't modify the defaults: +

+ +

+ If you want more configuration options, have a look at our + docker-compose.yml + editor here. +

+
+ +

Let us know if you have any issue, we'll be happy to help!

+

The Retrospected Team

+ + + +

Your docker-compose.yml file:

+ + +
version: '3'
+services:
+  postgres:
+    image: postgres:11.6
+    hostname: postgres
+    environment:
+      # Only change the Database password below BEFORE running for the first time. Once the database
+      # is initialised, you can't change the password anymore.
+      # This password has to be the same as the DB_PASSWORD in the "backend" section below.
+      POSTGRES_PASSWORD: {{dbPassword}}
+
+      # -- Dot not modify --
+      POSTGRES_USER: postgres
+      POSTGRES_DB: retroboard
+    volumes:
+      - database:/var/lib/postgresql/data
+    restart: unless-stopped
+    logging:
+      driver: 'json-file'
+      options:
+        max-size: '50m'
+
+  backend:
+    image: antoinejaussoin/retro-board-backend:latest
+    depends_on:
+      - redis
+    environment:
+      LICENCE_KEY: {{key}} # Your personal licence key
+      SELF_HOSTED_ADMIN: '{{email}}' # This is the user who is going to be admin on the self-hosted Retrospected instance.
+      DB_PASSWORD: {{dbPassword}} # Must be the same as POSTGRES_PASSWORD above
+      SESSION_SECRET: {{sessionSecret}} # This can be anything. You don't have to change this.
+
+    restart: unless-stopped
+    logging:
+      driver: 'json-file'
+      options:
+        max-size: '50m'
+
+  pgadmin:
+    image: dpage/pgadmin4:4.15 # use biarms/pgadmin4 on ARM
+    depends_on:
+      - postgres
+    ports:
+      - '1801:80' # Change 1801 to whatever port you want to access pgAdmin from
+    environment:
+      PGADMIN_DEFAULT_EMAIL: '{{email}}' # This will give you access to PGAdmin to manage your database
+      PGADMIN_DEFAULT_PASSWORD: {{pgAdminPassword}} # Your default password. Change this if you want, but BEFORE running for the first time.
+    volumes:
+      - pgadmin:/var/lib/pgadmin
+    restart: unless-stopped
+    logging:
+      driver: 'json-file'
+      options:
+        max-size: '50m'
+
+  frontend:
+    image: antoinejaussoin/retro-board-frontend:latest
+    depends_on:
+      - backend
+    ports:
+      - '1800:80' # Change 1800 to whatever port you want to access Retrospected from
+    restart: unless-stopped
+    logging:
+      driver: 'json-file'
+      options:
+        max-size: '50m'
+
+  redis:
+    image: redis:latest
+    depends_on:
+      - postgres
+    restart: unless-stopped
+    logging:
+      driver: 'json-file'
+      options:
+        max-size: '50m'
+
+volumes:
+  database:
+  pgadmin:
+ + + + + + diff --git a/backend/src/email/sendgrid-sender.ts b/backend/src/email/sendgrid-sender.ts new file mode 100644 index 000000000..56486b90d --- /dev/null +++ b/backend/src/email/sendgrid-sender.ts @@ -0,0 +1,28 @@ +import sendGrid, { MailDataRequired } from '@sendgrid/mail'; +import config from '../config'; +import { EmailSender } from './types'; + +if (config.SENDGRID_API_KEY) { + sendGrid.setApiKey(config.SENDGRID_API_KEY); +} + +export const sendGridSender: EmailSender = async function ( + to: string, + subject: string, + body: string +): Promise { + const msg: MailDataRequired = { + to, + from: config.SENDGRID_SENDER, + html: body, + subject, + }; + try { + await sendGrid.send(msg); + } catch (e) { + console.error('Error while using sendgrid: ', e); + return false; + } + + return true; +}; diff --git a/backend/src/email/smtp-sender.ts b/backend/src/email/smtp-sender.ts new file mode 100644 index 000000000..c1960f9fa --- /dev/null +++ b/backend/src/email/smtp-sender.ts @@ -0,0 +1,27 @@ +import nodemailer from 'nodemailer'; +import config from '../config'; +import { EmailSender } from './types'; + +const transporter = nodemailer.createTransport({ + host: config.MAIL_SMTP_HOST, + port: config.MAIL_PORT, + secure: config.MAIL_SECURE, + auth: { + user: config.MAIL_USER, + pass: config.MAIL_PASSWORD, + }, +}); + +export const smtpSender: EmailSender = async function ( + to: string, + subject: string, + body: string +): Promise { + const response = await transporter.sendMail({ + from: config.MAIL_SENDER, + to, + subject, + html: body, + }); + return !!response.accepted; +}; diff --git a/backend/src/email/template-loader.ts b/backend/src/email/template-loader.ts new file mode 100644 index 000000000..73f83dba4 --- /dev/null +++ b/backend/src/email/template-loader.ts @@ -0,0 +1,46 @@ +import fs from 'fs/promises'; +import path from 'path'; +import h from 'handlebars'; + +async function getEmailBody(data: T, fileName: string): Promise { + const file = path.resolve(__dirname, fileName); + const content = await fs.readFile(file, 'utf8'); + const template = h.compile(content); + const result = template(data); + return result; +} + +export async function getRegisterTemplate( + email: string, + name: string, + code: string, + domain: string +): Promise { + return getEmailBody({ email, name, code, domain }, 'register.template.html'); +} + +export async function getPasswordResetTemplate( + email: string, + name: string, + code: string, + domain: string +): Promise { + return getEmailBody( + { email, name, code, domain }, + 'reset-password.template.html' + ); +} + +export async function getSelfHostedWelcomeEmailTemplate( + name: string, + key: string, + dbPassword: string, + pgAdminPassword: string, + sessionSecret: string, + email: string +): Promise { + return getEmailBody( + { email, name, key, dbPassword, pgAdminPassword, sessionSecret }, + 'self-hosted.template.html' + ); +} diff --git a/backend/src/email/types.ts b/backend/src/email/types.ts new file mode 100644 index 000000000..03ff6b677 --- /dev/null +++ b/backend/src/email/types.ts @@ -0,0 +1,5 @@ +export type EmailSender = ( + to: string, + subject: string, + body: string +) => Promise; diff --git a/backend/src/email/utils.ts b/backend/src/email/utils.ts new file mode 100644 index 000000000..0ce3fa470 --- /dev/null +++ b/backend/src/email/utils.ts @@ -0,0 +1,5 @@ +import config from '../config'; + +export function canSendEmails() { + return !!config.MAIL_SMTP_HOST || !!config.SENDGRID_API_KEY; +} diff --git a/backend/src/types.ts b/backend/src/types.ts index c6e98eeed..96ad39701 100644 --- a/backend/src/types.ts +++ b/backend/src/types.ts @@ -35,9 +35,6 @@ export interface BackendConfig { OKTA_SECRET: string; SENDGRID_API_KEY: string; SENDGRID_SENDER: string; - SENDGRID_VERIFICATION_EMAIL_TID: string; - SENDGRID_RESET_PASSWORD_TID: string; - SENDGRID_SELF_HOST_EMAIL_TID: string; STRIPE_SECRET: string; STRIPE_WEBHOOK_SECRET: string; STRIPE_TEAM_PRODUCT: string; @@ -53,6 +50,12 @@ export interface BackendConfig { RATE_LIMIT_WS_POINTS: number; RATE_LIMIT_WS_DURATION: number; WS_MAX_BUFFER_SIZE: number; + MAIL_SMTP_HOST: string; + MAIL_PORT: number; + MAIL_SECURE: boolean; + MAIL_USER: string; + MAIL_PASSWORD: string; + MAIL_SENDER: string; } export type LicenceMetadata = { diff --git a/backend/yarn.lock b/backend/yarn.lock index c3f28d2ff..a81c80ff7 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -910,6 +910,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.23.tgz#3b41a6e643589ac6442bdbd7a4a3ded62f33f7da" integrity sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw== +"@types/nodemailer@6.4.4": + version "6.4.4" + resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.4.tgz#c265f7e7a51df587597b3a49a023acaf0c741f4b" + integrity sha512-Ksw4t7iliXeYGvIQcSIgWQ5BLuC/mljIEbjf615svhZL10PE9t+ei8O9gDaD3FPCasUJn9KTLwz2JFJyiiyuqw== + dependencies: + "@types/node" "*" + "@types/oauth@*": version "0.9.1" resolved "https://registry.yarnpkg.com/@types/oauth/-/oauth-0.9.1.tgz#e17221e7f7936b0459ae7d006255dff61adca305" @@ -1717,6 +1724,24 @@ cookie@0.4.2, cookie@^0.4.1, cookie@~0.4.1: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== +copyfiles@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/copyfiles/-/copyfiles-2.4.1.tgz#d2dcff60aaad1015f09d0b66e7f0f1c5cd3c5da5" + integrity sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg== + dependencies: + glob "^7.0.5" + minimatch "^3.0.3" + mkdirp "^1.0.4" + noms "0.0.0" + through2 "^2.0.1" + untildify "^4.0.0" + yargs "^16.1.0" + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + cors@~2.8.5: version "2.8.5" resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" @@ -2476,7 +2501,7 @@ glob-parent@^6.0.1: dependencies: is-glob "^4.0.3" -glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: +glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -2541,6 +2566,18 @@ graceful-fs@^4.1.2, graceful-fs@^4.2.9: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== +handlebars@4.7.7: + version "4.7.7" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" + integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.0" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -2690,7 +2727,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -2806,6 +2843,16 @@ is-yarn-global@^0.3.0: resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -3516,7 +3563,7 @@ mimic-response@^1.0.0, mimic-response@^1.0.1: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== -minimatch@^3.0.4: +minimatch@^3.0.3, minimatch@^3.0.4: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -3577,6 +3624,11 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +neo-async@^2.6.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + node-fetch@2.6.7, node-fetch@^2.6.1: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" @@ -3594,6 +3646,11 @@ node-releases@^2.0.2: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.2.tgz#7139fe71e2f4f11b47d4d2986aaf8c48699e0c01" integrity sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg== +nodemailer@6.7.3: + version "6.7.3" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.3.tgz#b73f9a81b9c8fa8acb4ea14b608f5e725ea8e018" + integrity sha512-KUdDsspqx89sD4UUyUKzdlUOper3hRkDVkrKh/89G+d9WKsU5ox51NWS4tB1XR5dPUdR4SP0E3molyEfOvSa3g== + nodemon@2.0.15: version "2.0.15" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.15.tgz#504516ce3b43d9dc9a955ccd9ec57550a31a8d4e" @@ -3610,6 +3667,14 @@ nodemon@2.0.15: undefsafe "^2.0.5" update-notifier "^5.1.0" +noms@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/noms/-/noms-0.0.0.tgz#da8ebd9f3af9d6760919b27d9cdc8092a7332859" + integrity sha1-2o69nzr51nYJGbJ9nNyAkqczKFk= + dependencies: + inherits "^2.0.1" + readable-stream "~1.0.31" + nopt@~1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" @@ -4070,6 +4135,11 @@ pretty-format@^27.0.0, pretty-format@^27.5.1: ansi-styles "^5.0.0" react-is "^17.0.1" +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + prompts@^2.0.1: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -4178,6 +4248,29 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +readable-stream@~1.0.31: + version "1.0.34" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@~2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -4308,7 +4401,7 @@ safe-buffer@5.2.1, safe-buffer@^5.0.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@~5.1.1: +safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== @@ -4548,6 +4641,18 @@ string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2 is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -4663,6 +4768,14 @@ throat@^6.0.1: resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375" integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w== +through2@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + tldjs@^1.5.2: version "1.8.0" resolved "https://registry.yarnpkg.com/tldjs/-/tldjs-1.8.0.tgz#b9c16bd6de357b55ffcbe7d5be41f6b3ca76e3fa" @@ -4829,7 +4942,7 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typeorm-naming-strategies@^3.0.0: +typeorm-naming-strategies@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/typeorm-naming-strategies/-/typeorm-naming-strategies-3.0.0.tgz#d709110a24bea464ce983a0b3e6e728e59ed88c1" integrity sha512-N8unfoNa+TYzmH2G5hA2Uh2ZHDACRRshHv8GsB7l/oakcoA9cUTtdeNp24pjpb9HY96sBub2pb8wxXX1afT5RA== @@ -4862,6 +4975,11 @@ typescript@4.6.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.3.tgz#eefeafa6afdd31d725584c67a0eaba80f6fc6c6c" integrity sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw== +uglify-js@^3.1.4: + version "3.15.3" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.15.3.tgz#9aa82ca22419ba4c0137642ba0df800cb06e0471" + integrity sha512-6iCVm2omGJbsu3JWac+p6kUiOpg3wFO2f8lIXjfEb8RrmLjzog1wTPMmwKB7swfzzqxj9YM+sGUM++u1qN4qJg== + uid-safe@2.1.5, uid-safe@~2.1.5: version "2.1.5" resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" @@ -4901,6 +5019,11 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= +untildify@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" + integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== + update-notifier@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-5.1.0.tgz#4ab0d7c7f36a231dd7316cf7729313f0214d9ad9" @@ -4935,6 +5058,11 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + utils-merge@1.0.1, utils-merge@1.x.x: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" @@ -5053,6 +5181,11 @@ word-wrap@^1.2.3, word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -5120,7 +5253,7 @@ xmldom@0.1.x: resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff" integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ== -xtend@^4.0.0: +xtend@^4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== @@ -5152,7 +5285,7 @@ yargs-parser@^21.0.0: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.1.tgz#0267f286c877a4f0f728fceb6f8a3e4cb95c6e35" integrity sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg== -yargs@^16.0.0, yargs@^16.2.0: +yargs@^16.0.0, yargs@^16.1.0, yargs@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== diff --git a/docs/docs/self-hosting/optionals.md b/docs/docs/self-hosting/optionals.md index 015838bb8..9012e1b94 100644 --- a/docs/docs/self-hosting/optionals.md +++ b/docs/docs/self-hosting/optionals.md @@ -67,9 +67,12 @@ services: SQL_LOG: 'false' # Whether to log SQL queries in the console SENDGRID_API_KEY: # Used for Sendgrid email reminders SENDGRID_SENDER: # Email to be used as the sender for emails - SENDGRID_VERIFICATION_EMAIL_TID: # Verification email template ID - SENDGRID_RESET_PASSWORD_TID: # Reset password email template ID - SENDGRID_SELF_HOST_EMAIL_TID: # Self host welcome email template ID + MAIL_SMTP_HOST: # SMTP server host for sending emails via SMTP (instead of SendGrid) + MAIL_PORT: 465 # SMTP port (usually 465 for secure SMTP) + MAIL_SECURE: true # If SMTP is using encryption, usually via port 465, set this to true + MAIL_USER: # SMTP username (or email) + MAIL_PASSWORD: # SMTP user password + MAIL_SENDER: # SMTP sender email (usually matches MAIL_USER) STRIPE_SECRET: # Stripe payment account secret STRIPE_WEBHOOK_SECRET: # Stripe webhook secret STRIPE_TEAM_PRODUCT: # Stripe product information diff --git a/docs/docs/self-hosting/passwords.md b/docs/docs/self-hosting/passwords.md index aa71b7162..19a52542b 100644 --- a/docs/docs/self-hosting/passwords.md +++ b/docs/docs/self-hosting/passwords.md @@ -1,5 +1,5 @@ --- -sidebar_position: 5 +sidebar_position: 7 --- # 🔑 Passwords diff --git a/docs/docs/self-hosting/quick-start/Accordion.module.css b/docs/docs/self-hosting/quick-start/Accordion.module.css new file mode 100644 index 000000000..ddd608d62 --- /dev/null +++ b/docs/docs/self-hosting/quick-start/Accordion.module.css @@ -0,0 +1,16 @@ +.container { + border: 1px solid #E0E0E0; + border-radius: 5px; + padding: 10px; + background-color: #FAFAFA; + margin-bottom: 20px; +} + +.title { + font-weight: bold; + cursor: pointer; +} + +.content { +margin-top: 20px; +} \ No newline at end of file diff --git a/docs/docs/self-hosting/quick-start/Accordion.tsx b/docs/docs/self-hosting/quick-start/Accordion.tsx new file mode 100644 index 000000000..a197fb81d --- /dev/null +++ b/docs/docs/self-hosting/quick-start/Accordion.tsx @@ -0,0 +1,21 @@ +import React, { PropsWithChildren, useState } from 'react'; +import styles from './Accordion.module.css'; + +type AccordionProps = { + title: string; +}; + +export function Accordion({ + title, + children, +}: PropsWithChildren) { + const [open, setOpen] = useState(false); + return ( +
+
setOpen((p) => !p)}> + {title} {!open ? <>(click to open) : null} +
+ {open ?
{children}
: null} +
+ ); +} diff --git a/docs/docs/self-hosting/quick-start/ComposeView.tsx b/docs/docs/self-hosting/quick-start/ComposeView.tsx index ebdb970e8..3931be8dd 100644 --- a/docs/docs/self-hosting/quick-start/ComposeView.tsx +++ b/docs/docs/self-hosting/quick-start/ComposeView.tsx @@ -4,7 +4,7 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { xonokai } from 'react-syntax-highlighter/dist/esm/styles/prism'; import styles from './ComposeView.module.css'; -type ComposeViewProps = { +type ComposeViewSettings = { dbPassword: string; pgPassword: string; sessionSecret: string; @@ -13,18 +13,70 @@ type ComposeViewProps = { port: string; pgPort: string; arm: boolean; + disableAnon: boolean; + disablePassword: boolean; + disableRegistration: boolean; + useSendgrid: boolean; + useSmtp: boolean; + sendgridKey: string; + sendgridSender: string; + smtpHost: string; + smtpPort: string; + smtpSecure: boolean; + smtpUser: string; + smtpPassword: string; + smtpSender: string; +}; + +type ComposeViewProps = { + settings: ComposeViewSettings; }; +function p(condition: boolean, key: string, value: string, number = false) { + return condition + ? ` ${key}: ${number ? value : "'" + value + "'"}` + : null; +} + export default function ComposeView({ - dbPassword, - pgPassword, - email, - licence, - sessionSecret, - port, - pgPort, - arm, + settings: { + dbPassword, + pgPassword, + email, + licence, + sessionSecret, + port, + pgPort, + arm, + disableAnon, + disablePassword, + disableRegistration, + useSendgrid, + useSmtp, + sendgridKey, + sendgridSender, + smtpHost, + smtpPort, + smtpSecure, + smtpUser, + smtpPassword, + smtpSender, + }, }: ComposeViewProps) { + const optionals = [ + p(disableAnon, 'DISABLE_ANONYMOUS_LOGIN', 'true'), + p(disablePassword, 'DISABLE_PASSWORD_LOGIN', 'true'), + p(disableRegistration, 'DISABLE_PASSWORD_REGISTRATION', 'true'), + p(useSendgrid, 'SENDGRID_API_KEY', sendgridKey), + p(useSendgrid, 'SENDGRID_SENDER', sendgridSender), + p(useSmtp, 'MAIL_SMTP_HOST', smtpHost), + p(useSmtp, 'MAIL_PORT', smtpPort, true), + p(useSmtp, 'MAIL_SECURE', smtpSecure ? 'true' : 'false'), + p(useSmtp, 'MAIL_USER', smtpUser), + p(useSmtp, 'MAIL_PASSWORD', smtpPassword), + p(useSmtp, 'MAIL_SENDER', smtpSender), + ].filter(Boolean); + const text = `version: '3' services: frontend: @@ -48,6 +100,8 @@ services: SELF_HOSTED_ADMIN: '${email}' DB_PASSWORD: '${dbPassword}' SESSION_SECRET: '${sessionSecret}' +${optionals.join('\n')} + restart: unless-stopped logging: driver: 'json-file' diff --git a/docs/docs/self-hosting/quick-start/Editor.tsx b/docs/docs/self-hosting/quick-start/Editor.tsx index 3eb10abc3..579b99539 100644 --- a/docs/docs/self-hosting/quick-start/Editor.tsx +++ b/docs/docs/self-hosting/quick-start/Editor.tsx @@ -1,13 +1,14 @@ import React, { useEffect, useState } from 'react'; import ComposeView from './ComposeView'; -import { InputField, Field } from './Field'; +import { InputField } from './Field'; import styles from './Editor.module.css'; import randomWords from 'random-words'; import queryString from 'query-string'; import RunDetails from './RunDetails'; import useIsBrowser from '@docusaurus/useIsBrowser'; -import Toggle from 'react-toggle'; import usePersistedState from './usePersistedState'; +import { FieldToggle } from './Toggle'; +import { Accordion } from './Accordion'; function getRandomPassword() { return randomWords(4).join('-'); @@ -32,6 +33,35 @@ export default function Editor() { const [port, setPort] = usePersistedState('port', '80'); const [pgPort, setPgPort] = usePersistedState('pg-port', '81'); const [isArm, setIsArm] = usePersistedState('is-arm', false); + const [disableAnon, setDisableAnon] = usePersistedState( + 'disable-anon', + false + ); + const [disablePassword, setDisablePasswordAccounts] = usePersistedState( + 'disable-password-accounts', + false + ); + const [disableRegistration, setDisablePasswordRegistration] = + usePersistedState('disable-password-reg', false); + const [useSendgrid, setUseSendgrid] = usePersistedState( + 'use-sendgrid', + false + ); + const [useSmtp, setUseSmtp] = usePersistedState('use-smtp', false); + const [sendgridKey, setSendgridKey] = usePersistedState('sendgrid-key', ''); + const [sendgridSender, setSendgridSender] = usePersistedState( + 'sendgrid-sender', + 'your@email.com' + ); + const [smtpHost, setSmtpHost] = usePersistedState('smtp-host', ''); + const [smtpPort, setSmtpPort] = usePersistedState('smtp-port', '465'); + const [smtpSecure, setSmtpSecure] = usePersistedState('smtp-secure', true); + const [smtpUser, setSmtpUser] = usePersistedState('smtp-user', ''); + const [smtpPassword, setSmtpPassword] = usePersistedState( + 'smtp-password', + '' + ); + const [smtpSender, setSmtpSender] = usePersistedState('smtp-sender', ''); useEffect(() => { if (isBrowser) { @@ -82,45 +112,162 @@ export default function Editor() { value={dbPassword} onChange={setDbPassword} /> - - -
- setIsArm(e.target.checked)} - /> - -
-
+ +
+ + + + + +
+
+ +
+ + {useSendgrid ? ( + <> + + + + ) : null} +
+
+ + {useSmtp && !useSendgrid ? ( + <> + + + + + + + + ) : null} +
+

Your customised docker-compose file:

2 - Run Docker

    diff --git a/docs/docs/self-hosting/quick-start/Field.tsx b/docs/docs/self-hosting/quick-start/Field.tsx index d702c4d4d..af28b8bc9 100644 --- a/docs/docs/self-hosting/quick-start/Field.tsx +++ b/docs/docs/self-hosting/quick-start/Field.tsx @@ -1,7 +1,7 @@ import React from 'react'; import styles from './Field.module.css'; -type FieldProps = { +export type FieldProps = { label: string; description?: string; }; diff --git a/docs/docs/self-hosting/quick-start/Toggle.tsx b/docs/docs/self-hosting/quick-start/Toggle.tsx new file mode 100644 index 000000000..d8a218005 --- /dev/null +++ b/docs/docs/self-hosting/quick-start/Toggle.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import Toggle from 'react-toggle'; +import { Field, FieldProps } from './Field'; +import styles from './Editor.module.css'; + +type ToggleProps = FieldProps & { + id: string; + toggleLabel: string; + value: boolean; + onChange: (value: boolean) => void; +}; + +export function FieldToggle({ + label, + toggleLabel, + description, + value, + id, + onChange, +}: ToggleProps) { + return ( + +
    + onChange(e.target.checked)} + /> + +
    +
    + ); +} diff --git a/docs/docs/self-hosting/sendgrid.mdx b/docs/docs/self-hosting/sendgrid.mdx index 5630f8a94..e600fe47b 100644 --- a/docs/docs/self-hosting/sendgrid.mdx +++ b/docs/docs/self-hosting/sendgrid.mdx @@ -1,16 +1,30 @@ -# 📧 Sendgrid +--- +sidebar_position: 6 +--- -By default, and when using the basic configuration in the [quick-start](quick-start) page, -your users will be able to register using an email without having to verify that email. +# 📧 Emails +A Retrospected instance sometimes need to send email to its users, when they register or want to change their password. + +You have three mechanism for sending emails (or not) you can choose from: + +- [Sendgrid](https://sendgrid.com) +- SMTP +- Nothing: no emails are sent + +## Nothing (not recommended) + +If you don't specify any mechanism (neither Sendgrid nor SMTP), verification emails won't be sent and users will be able to register any email. It also means they **won't be able to change their password if they forget it**. You have two ways of solving this problem: - Use the [Admin Panel](admin), if you are the admin, to manually change someone else's password -- Setup Sendgrid so that emails are verified, and recovery emails are sent if a user forgets their password +- Setup Sendgrid or SMTP so that emails are verified, and recovery emails are sent if a user forgets their password The latter is what we are going to setup in this guide. +## SendGrid (recommended) + :::info Is it free? Sendgrid is completely free up to a certain amount of emails per day. For a self-hosted instance, you are very unlikely to have to pay for it. @@ -25,48 +39,27 @@ unlikely to have to pay for it. - Click create, and then copy the key that you've created somewhere safe (we'll need it later) -### Create the templates - -We will need to create two templates, one for the verification email (the one sent after registration), -and the other one for the password reset email. - -- Go to `Email API` and `Dynamic Templates` -- Click on `Create a Dynamic Template`, and name it `Verification Email` -- On the created template, click on `Add Version` -- Select the blank template, and then the `Design Editor` -- Add a Text field, and add the following content: -``` -Hello {{name}}! - -Please click on the email below to validate your email and start using Retrospected right away. - -{{domain}}/validate?email={{email}}&code={{code}} -``` -- Then save -- Create another dynamic template, this time named `Reset Password Email`, create a new version, blank template and Design Editor -- Add a Text field and add the following content: -``` -Hi {{name}}, - -To reset your password, follow the link: - -{{domain}}/reset?email={{email}}&code={{code}} -``` -- Click save. You should now have two templates, with one version each, like this: - -- Within each template, you have a `Template ID`: copy the two of them (one for each template), we'll need them later. - - ### Set the environement variables We should now have all the information we need. In the `backend` section of your `docker-compose.yml` file, add the following variables: - `SENDGRID_API_KEY`: this is the API key you got in the first section of this guide - `SENDGRID_SENDER`: enter the email you used to create your Sendgrid account -- `SENDGRID_VERIFICATION_EMAIL_TID`: this is the **Template ID** you created in the second section of this guide, for the **verification email**. -- `SENDGRID_RESET_PASSWORD_TID`: this is the **Template ID** you created in the second section of this guide, for the **password reset email**. - `BASE_URL`: this is the URL to your self-hosted Retrospected (for example: `http://retro.mycompany.com`) ### Done! -Now that it is setup, your users should now need to verify their email and will be able to reset their passwords themselves. \ No newline at end of file +Now that it is setup, your users should now need to verify their email and will be able to reset their passwords themselves. + +## SMTP + +### Setting up SMTP + +In the `backend` section of your `docker-compose.yml` file, add the following variables: + - `MAIL_SMTP_HOST`: SMTP server host (example: `smtp.myemail.com`) + - `MAIL_PORT`: SMTP port (usually `465` for secure SMTP) + - `MAIL_SECURE`: If SMTP is using encryption, usually via port 465, set this to `true` + - `MAIL_USER`: SMTP username (or email) + - `MAIL_PASSWORD`: SMTP user password + - `MAIL_SENDER`: SMTP sender email (usually matches `MAIL_USER`) + diff --git a/docs/package.json b/docs/package.json index e04f68f7b..c78dfde16 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,6 +1,6 @@ { "name": "docs", - "version": "4.13.0", + "version": "4.14.0", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/frontend/package.json b/frontend/package.json index 0b1ba51c5..403a8abf0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "@retrospected/frontend", - "version": "4.13.0", + "version": "4.14.0", "license": "GNU GPLv3", "private": true, "dependencies": { @@ -18,7 +18,7 @@ "@testing-library/react": "13.0.0", "@testing-library/react-hooks": "7.0.2", "@types/crypto-js": "4.1.1", - "@types/emoji-mart": "^3.0.9", + "@types/emoji-mart": "3.0.9", "@types/jest": "27.4.1", "@types/lodash": "4.14.181", "@types/md5": "2.3.2", @@ -29,15 +29,15 @@ "@types/react-copy-to-clipboard": "5.0.2", "@types/react-dom": "17.0.14", "@types/react-helmet": "6.1.5", - "@types/react-router-dom": "^5.3.3", + "@types/react-router-dom": "5.3.3", "@types/shortid": "0.0.29", "@types/uuid": "8.3.4", "bowser": "2.11.0", - "buffer": "^6.0.3", + "buffer": "6.0.3", "core-js": "3.21.1", "crypto-js": "4.1.1", "date-fns": "2.28.0", - "emoji-mart": "^3.0.1", + "emoji-mart": "3.0.1", "flag-icons": "6.1.1", "http-proxy-middleware": "2.0.4", "isemail": "3.2.0", diff --git a/frontend/src/auth/modal/account/LostPassword.tsx b/frontend/src/auth/modal/account/LostPassword.tsx index 4cd5e63df..961c8e903 100644 --- a/frontend/src/auth/modal/account/LostPassword.tsx +++ b/frontend/src/auth/modal/account/LostPassword.tsx @@ -23,7 +23,7 @@ const LostPassword = () => { reset(); }, [email]); - if (!backend.sendGridAvailable && backend.selfHosted) { + if (!backend.emailAvailable && backend.selfHosted) { return ( You are using a Self-Hosted version of Retrospected, without email diff --git a/frontend/src/common/payloads.ts b/frontend/src/common/payloads.ts index 053206fa8..a0041f009 100644 --- a/frontend/src/common/payloads.ts +++ b/frontend/src/common/payloads.ts @@ -53,7 +53,7 @@ export interface SelfHostedCheckPayload { export interface BackendCapabilities { selfHosted: boolean; - sendGridAvailable: boolean; + emailAvailable: boolean; adminEmail: string; licenced: boolean; oAuth: OAuthAvailabilities; diff --git a/frontend/src/global/state.ts b/frontend/src/global/state.ts index e12fea756..372f559ff 100644 --- a/frontend/src/global/state.ts +++ b/frontend/src/global/state.ts @@ -18,6 +18,6 @@ export const backendCapabilitiesState = atom({ slack: false, okta: false, }, - sendGridAvailable: false, + emailAvailable: false, }, }); diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 02bc73f12..badccdb85 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2036,7 +2036,7 @@ resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.1.1.tgz#602859584cecc91894eb23a4892f38cfa927890d" integrity sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA== -"@types/emoji-mart@^3.0.9": +"@types/emoji-mart@3.0.9": version "3.0.9" resolved "https://registry.yarnpkg.com/@types/emoji-mart/-/emoji-mart-3.0.9.tgz#2f7ef5d9ec194f28029c46c81a5fc1e5b0efa73c" integrity sha512-qdBo/2Y8MXaJ/2spKjDZocuq79GpnOhkwMHnK2GnVFa8WYFgfA+ei6sil3aeWQPCreOKIx9ogPpR5+7MaOqYAA== @@ -2299,7 +2299,7 @@ hoist-non-react-statics "^3.3.0" redux "^4.0.0" -"@types/react-router-dom@^5.3.3": +"@types/react-router-dom@5.3.3": version "5.3.3" resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw== @@ -3282,7 +3282,7 @@ buffer-indexof@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== -buffer@^6.0.3: +buffer@6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== @@ -4294,7 +4294,7 @@ emittery@^0.8.1: resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860" integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg== -emoji-mart@^3.0.1: +emoji-mart@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-3.0.1.tgz#9ce86706e02aea0506345f98464814a662ca54c6" integrity sha512-sxpmMKxqLvcscu6mFn9ITHeZNkGzIvD0BSNFE/LJESPbCA8s1jM6bCDPjWbV31xHq7JXaxgpHxLB54RCbBZSlg== diff --git a/integration/package.json b/integration/package.json index 7028acb10..f57acf5e8 100644 --- a/integration/package.json +++ b/integration/package.json @@ -1,6 +1,6 @@ { "name": "retro-board-integration", - "version": "4.13.0", + "version": "4.14.0", "description": "Integrations tests", "main": "index.js", "directories": { diff --git a/package.json b/package.json index 447895ab2..0a96a2b72 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "retrospected", - "version": "4.13.0", + "version": "4.14.0", "description": "An agile retrospective board - Powering www.retrospected.com", "private": true, "scripts": { diff --git a/self-hosting/docker-compose.full.yml b/self-hosting/docker-compose.full.yml index 3fc140d82..c9b4475a2 100644 --- a/self-hosting/docker-compose.full.yml +++ b/self-hosting/docker-compose.full.yml @@ -95,9 +95,12 @@ services: SQL_LOG: 'false' # Whether to log SQL queries in the console SENDGRID_API_KEY: # Used for Sendgrid email reminders SENDGRID_SENDER: # Email to be used as the sender for emails - SENDGRID_VERIFICATION_EMAIL_TID: # Verification email template ID - SENDGRID_RESET_PASSWORD_TID: # Reset password email template ID - SENDGRID_SELF_HOST_EMAIL_TID: # Self host welcome email template ID + MAIL_SMTP_HOST: # SMTP server host for sending emails via SMTP (instead of SendGrid) + MAIL_PORT: 465 # SMTP port (usually 465 for secure SMTP) + MAIL_SECURE: true # If SMTP is using encryption, usually via port 465, set this to true + MAIL_USER: # SMTP username (or email) + MAIL_PASSWORD: # SMTP user password + MAIL_SENDER: # SMTP sender email (usually matches MAIL_USER) STRIPE_SECRET: # Stripe payment account secret STRIPE_WEBHOOK_SECRET: # Stripe webhook secret STRIPE_TEAM_PRODUCT: # Stripe product information