diff --git a/.drone.yml b/.drone.yml index 9a2a300..fb4006f 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,3 +1,23 @@ +--- +kind: pipeline +name: tests:node_latest +clone: + disable: true +steps: + - name: checkout pr + image: alpine/git + commands: + - git clone $DRONE_REMOTE_URL . + - git checkout $DRONE_SOURCE_BRANCH + - name: run tests + image: node:latest + commands: + - yarn + - yarn test:ci +trigger: + event: + - pull_request + --- kind: pipeline type: docker diff --git a/CHANGELOG.md b/CHANGELOG.md index ad93058..431570f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,4 +2,4 @@ All notable changes to this project will be documented in this file. Dates are displayed in UTC. -#### 0.0.1 +#### 0.1.0 diff --git a/README.md b/README.md index 3e83512..6c6ca2c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,74 @@ -# mailer +# @lfk/mailer +[![Build Status](https://ci.odit.services/api/badges/lfk/mailer/status.svg?ref=refs/heads/main)](https://ci.odit.services/lfk/mailer) -Handles mail generation and sending (pw reset, welcome mail, etc) \ No newline at end of file +Handles mail generation and sending (pw reset, welcome mail, etc) + +## Dev Setup 🛠 +> Local dev setup + +1. Rename the .env.example file to .env (you can adjust app port and other settings, if needed) or generate a example env with `yarn && yarn test:generate_env`. +2. Install Dependencies + ```bash + yarn + ``` +3. Start the server + ```bash + yarn dev + ``` + +## Templates +> The mailer uses html and plaintext templates to generate various mails. +> The templates are stored in src/templates by default. + +We provide a set of default templates that we use for the ["Lauf für Kaya!" charity run](https://lauf-fuer-kaya.de). +We use handlebars for templateing utilizing i18next for translation - the i18n string format in the templates is : `{{__ "string"}}` +You can provide your own templates by replacing the ones we provided before compiling the project or by simply mounting your custom templates into the docker container. + +The server currently needs the following templates to work: +* pw-reset.html +* pw-reset.txt +* test.html +* test.txt +* welcome_runner.html +* welcome_runner.txt + +| Name | Type | Default | Description +| - | - | - | - +| APP_PORT | Number | 4010 | The port the backend server listens on. Is optional. +| NODE_ENV | String | dev | The apps env - influences debug info. +| API_KEY | String(min length: 64) | Random generated string | The api key you want to use for auth (query-param `key`), has to be at least 64 chars long. +| API_URL | String(url) | "http://localhost:8080" | The URL ponting to the base (root) of the lfk runner system. +| MAIL_SERVER | String(FQDN) | None | The mailserver (smtp) used to send mails via nodemailer. +| MAIL_PORT | Number | 25 | The mailserver's port (smtp). +| MAIL_USER | String | None | The username used to authenticate against the mailserver. +| MAIL_PASSWORD | String | None | The password used to authenticate against the mailserver. +| MAIL_FROM | String | None | The mail address that mails get sent from. +| PRIVACY_URL | String | "/privacy" | The url path that get's attached to the app url to link to the privacy page. +| IMPRINT_URL | String | "/imprint" | The url path that get's attached to the app url to link to the imprint page. +| COPYRIGHT_OWNER | String | "LfK!" | Text that gets inserted as the "copyright by" owner in the mails. +| EVENT_NAME | String | "Testing 4 Kaya" | The event's name - used to generate the mail text. +| CONTACT_MAIL | String(email) | MAIL_FROM | Contact mail address listed at the bottom of some mail templates. + +## Recommended Editor + +[Visual Studio Code](https://code.visualstudio.com/) + +### Recommended Extensions + +* will be automatically recommended via ./vscode/extensions.json +* we also provide a config for i18n-ally in the .vscode folder + +## Staging +### Branches & Tags +* vX.Y.Z: Release tags created from the main branch + * The version numbers follow the semver standard + * A new release tag automaticly triggers the release ci pipeline +* main: Protected "release" branch + * The latest tag of the docker image get's build from this + * New releases get created as tags from this +* dev: Current dev branch for merging the different feature branches and bugfixes + * The dev tag of the docker image get's build from this + * Only push minor changes to this branch! + * To merge a feature branch into this please create a pull request +* feature/xyz: Feature branches - nameing scheme: `feature/issueid-title` +* bugfix/xyz: Branches for bugfixes - nameing scheme:`bugfix/issueid-title` \ No newline at end of file diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..91a2d2c --- /dev/null +++ b/jest.config.js @@ -0,0 +1,4 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', +}; \ No newline at end of file diff --git a/licenses.md b/licenses.md index 0afda7a..30fc94b 100644 --- a/licenses.md +++ b/licenses.md @@ -428,6 +428,35 @@ OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. SOFTWARE +# @types/jest +**Author**: undefined +**Repo**: https://github.com/DefinitelyTyped/DefinitelyTyped.git +**License**: MIT +**Description**: TypeScript definitions for Jest +## License Text + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + # @types/node **Author**: undefined **Repo**: https://github.com/DefinitelyTyped/DefinitelyTyped.git @@ -486,6 +515,33 @@ OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. SOFTWARE +# axios +**Author**: Matt Zabriskie +**Repo**: https://github.com/axios/axios.git +**License**: MIT +**Description**: Promise based HTTP client for the browser and node.js +## License Text +Copyright (c) 2014-present Matt Zabriskie + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + # cp-cli **Author**: undefined **Repo**: git+https://github.com/screendriver/cp-cli.git @@ -515,6 +571,35 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# jest +**Author**: undefined +**Repo**: https://github.com/facebook/jest +**License**: MIT +**Description**: Delightful JavaScript Testing. +## License Text +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + # nodemon **Author**: [object Object] **Repo**: https://github.com/remy/nodemon.git @@ -604,6 +689,35 @@ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ## License Text +# ts-jest +**Author**: Kulshekhar Kabra (https://github.com/kulshekhar) +**Repo**: git+https://github.com/kulshekhar/ts-jest.git +**License**: MIT +**Description**: A preprocessor with source maps support to help use TypeScript with Jest +## License Text +MIT License + +Copyright (c) 2016-2018 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + # ts-node **Author**: [object Object] **Repo**: git://github.com/TypeStrong/ts-node.git diff --git a/package.json b/package.json index a872e2b..fa6592e 100644 --- a/package.json +++ b/package.json @@ -1,84 +1,92 @@ -{ - "name": "@odit/lfk-mailer", - "version": "0.0.1", - "description": "The document mailer for the LfK! runner system. This generates and sends mails (password reset, welcome, ...)", - "main": "src/app.ts", - "scripts": { - "dev": "nodemon src/app.ts", - "build": "rimraf ./dist && tsc && cp-cli ./src/templates ./dist/templates && cp-cli ./src/locales ./dist/locales", - "licenses:export": "license-exporter --markdown", - "release": "release-it --only-version", - "translations:sort": "node ./scripts/sort_translations.js", - "test:generate_env": "ts-node ./scripts/create_testenv.ts" - }, - "repository": { - "type": "git", - "url": "git@git.odit.services:lfk/mailer.git" - }, - "keywords": [ - "odit", - "lfk", - "mail", - "node" - ], - "author": { - "name": "ODIT.Services", - "email": "info@odit.services", - "url": "https://odit.services" - }, - "contributors": [ - { - "name": "Philipp Dormann", - "email": "philipp@philippdormann.de", - "url": "https://philippdormann.de" - }, - { - "name": "Nicolai Ort", - "email": "info@nicolai-ort.com", - "url": "https://nicolai-ort.com" - } - ], - "license": "CC-BY-NC-SA-4.0", - "dependencies": { - "@odit/class-validator-jsonschema": "^2.1.1", - "class-transformer": "0.3.1", - "class-validator": "^0.13.1", - "consola": "^2.15.3", - "cors": "^2.8.5", - "dotenv": "^8.2.0", - "express": "^4.17.1", - "handlebars": "^4.7.6", - "i18next": "^19.8.7", - "i18next-fs-backend": "^1.0.8", - "nodemailer": "^6.5.0", - "reflect-metadata": "^0.1.13", - "routing-controllers": "0.9.0-alpha.6", - "routing-controllers-openapi": "2.2.0" - }, - "devDependencies": { - "@odit/license-exporter": "^0.0.10", - "@types/express": "^4.17.11", - "@types/node": "^14.14.22", - "@types/nodemailer": "^6.4.0", - "cp-cli": "^2.0.0", - "nodemon": "^2.0.7", - "release-it": "^14.2.2", - "rimraf": "^3.0.2", - "start-server-and-test": "^1.12.0", - "ts-node": "^9.1.1", - "typescript": "^4.1.3" - }, - "release-it": { - "git": { - "commit": true, - "requireCleanWorkingDir": false, - "commitMessage": "🚀Bumped version to v${version}", - "requireBranch": "dev", - "push": false, - "tag": false - }, - "npm": { - "publish": false - } - } -} \ No newline at end of file +{ + "name": "@odit/lfk-mailer", + "version": "0.1.0", + "description": "The document mailer for the LfK! runner system. This generates and sends mails (password reset, welcome, ...)", + "main": "src/app.ts", + "scripts": { + "dev": "nodemon src/app.ts", + "build": "rimraf ./dist && tsc && cp-cli ./src/templates ./dist/templates && cp-cli ./src/locales ./dist/locales", + "licenses:export": "license-exporter --markdown", + "release": "release-it --only-version", + "translations:sort": "node ./scripts/sort_translations.js", + "test": "jest", + "test:watch": "jest --watchAll", + "test:generate_env": "ts-node ./scripts/create_testenv.ts", + "test:ci": "npm run test:generate_env && npm run test:ci:run", + "test:ci:run": "start-server-and-test dev http://localhost:4010/docs/openapi.json test" + }, + "repository": { + "type": "git", + "url": "git@git.odit.services:lfk/mailer.git" + }, + "keywords": [ + "odit", + "lfk", + "mail", + "node" + ], + "author": { + "name": "ODIT.Services", + "email": "info@odit.services", + "url": "https://odit.services" + }, + "contributors": [ + { + "name": "Philipp Dormann", + "email": "philipp@philippdormann.de", + "url": "https://philippdormann.de" + }, + { + "name": "Nicolai Ort", + "email": "info@nicolai-ort.com", + "url": "https://nicolai-ort.com" + } + ], + "license": "CC-BY-NC-SA-4.0", + "dependencies": { + "@odit/class-validator-jsonschema": "^2.1.1", + "class-transformer": "0.3.1", + "class-validator": "^0.13.1", + "consola": "^2.15.3", + "cors": "^2.8.5", + "dotenv": "^8.2.0", + "express": "^4.17.1", + "handlebars": "^4.7.6", + "i18next": "^19.8.7", + "i18next-fs-backend": "^1.0.8", + "nodemailer": "^6.5.0", + "reflect-metadata": "^0.1.13", + "routing-controllers": "0.9.0-alpha.6", + "routing-controllers-openapi": "2.2.0" + }, + "devDependencies": { + "@odit/license-exporter": "^0.0.10", + "@types/express": "^4.17.11", + "@types/jest": "^26.0.20", + "@types/node": "^14.14.22", + "@types/nodemailer": "^6.4.0", + "axios": "^0.21.1", + "cp-cli": "^2.0.0", + "jest": "^26.6.3", + "nodemon": "^2.0.7", + "release-it": "^14.2.2", + "rimraf": "^3.0.2", + "start-server-and-test": "^1.12.0", + "ts-jest": "^26.5.2", + "ts-node": "^9.1.1", + "typescript": "^4.1.3" + }, + "release-it": { + "git": { + "commit": true, + "requireCleanWorkingDir": false, + "commitMessage": "🚀Bumped version to v${version}", + "requireBranch": "dev", + "push": false, + "tag": false + }, + "npm": { + "publish": false + } + } +} diff --git a/src/Mailer.ts b/src/Mailer.ts index 93597d6..d2752f8 100644 --- a/src/Mailer.ts +++ b/src/Mailer.ts @@ -16,7 +16,7 @@ import { MailServerConfigError } from './errors/MailErrors'; */ export class Mailer { private transport: Mail; - private static interpolations = { copyright_owner: config.copyright_owner } + private static interpolations = { copyright_owner: config.copyright_owner, event_name: config.event_name, contact_mail: config.contact_mail } /** * Main constructor. @@ -69,11 +69,18 @@ export class Mailer { public async sendResetMail(to_address: string, token: string, locale: string = "en") { await i18next.changeLanguage(locale); - const reset_link = `${config.app_url}/reset/${(Buffer.from(token)).toString("base64")}` + const replacements = { + recipient_mail: to_address, + copyright_owner: config.copyright_owner, + link_imprint: `${config.app_url}/imprint`, + link_privacy: `${config.app_url}/privacy`, + reset_link: `${config.app_url}/reset/${(Buffer.from(token)).toString("base64")}` + } + const template_html = Handlebars.compile(fs.readFileSync(__dirname + '/templates/pw-reset.html', { encoding: 'utf8' })); const template_txt = Handlebars.compile(fs.readFileSync(__dirname + '/templates/pw-reset.txt', { encoding: 'utf8' })); - const body_html = template_html({ recipient_mail: to_address, copyright_owner: config.copyright_owner, link_imprint: `${config.app_url}/imprint`, link_privacy: `${config.app_url}/privacy`, reset_link }); - const body_txt = template_txt({ recipient_mail: to_address, copyright_owner: config.copyright_owner, link_imprint: `${config.app_url}/imprint`, link_privacy: `${config.app_url}/privacy`, reset_link }); + const body_html = template_html(replacements); + const body_txt = template_txt(replacements); const mail: MailOptions = { to: to_address, @@ -91,13 +98,18 @@ export class Mailer { public async sendTestMail(locale: string = "en") { await i18next.changeLanguage(locale); const to_address: string = config.mail_from; + const replacements = { + recipient_mail: to_address, + copyright_owner: config.copyright_owner, + link_imprint: `${config.app_url}/imprint`, + link_privacy: `${config.app_url}/privacy` + } const template_html = Handlebars.compile(fs.readFileSync(__dirname + '/templates/test.html', { encoding: 'utf8' })); const template_txt = Handlebars.compile(fs.readFileSync(__dirname + '/templates/test.txt', { encoding: 'utf8' })); - const body_html = template_html({ recipient_mail: to_address, copyright_owner: config.copyright_owner, link_imprint: `${config.app_url}/imprint`, link_privacy: `${config.app_url}/privacy` }); - const body_txt = template_txt({ recipient_mail: to_address, copyright_owner: config.copyright_owner, link_imprint: `${config.app_url}/imprint`, link_privacy: `${config.app_url}/privacy` }); + const body_html = template_html(replacements); + const body_txt = template_txt(replacements); - fs.writeFileSync("./test.tmp", body_txt); const mail: MailOptions = { to: to_address, subject: i18next.t("test-mail", Mailer.interpolations).toString(), @@ -107,6 +119,39 @@ export class Mailer { await this.sendMail(mail); } + /** + * Function for sending a reset mail from the reset mail template. + * @param to_address The address the mail will be sent to. Should always get pulled from a user object. + * @param token The requested password reset token - will be combined with the app_url to generate a password reset link. + */ + public async sendWelcomeMail(to_address: string, token: string, locale: string = "en") { + await i18next.changeLanguage(locale); + + const replacements = { + recipient_mail: to_address, + copyright_owner: config.copyright_owner, + link_imprint: `${config.app_url}/imprint`, + link_privacy: `${config.app_url}/privacy`, + selfservice_link: `${config.app_url}/selfservice/profile/${token}`, + forgot_link: `${config.app_url}/selfservice`, + contact_mail: config.contact_mail, + event_name: config.event_name + } + + const template_html = Handlebars.compile(fs.readFileSync(__dirname + '/templates/welcome_runner.html', { encoding: 'utf8' })); + const template_txt = Handlebars.compile(fs.readFileSync(__dirname + '/templates/welcome_runner.txt', { encoding: 'utf8' })); + const body_html = template_html(replacements); + const body_txt = template_txt(replacements); + + const mail: MailOptions = { + to: to_address, + subject: i18next.t("event_name-registration", Mailer.interpolations).toString(), + text: body_txt, + html: body_html + }; + await this.sendMail(mail); + } + /** * Wrapper function for sending a mail via this object's transporter. * @param mail MailOptions object containing the diff --git a/src/config.ts b/src/config.ts index 06d882d..2464181 100644 --- a/src/config.ts +++ b/src/config.ts @@ -15,7 +15,9 @@ export const config = { mail_from: process.env.MAIL_FROM, privacy_url: process.env.PRIVACY_URL || "/privacy", imprint_url: process.env.IMPRINT_URL || "/imprint", - copyright_owner: process.env.COPYRIGHT_OWNER || "LfK!" + copyright_owner: process.env.COPYRIGHT_OWNER || "LfK!", + event_name: process.env.EVENT_NAME || "Testing 4 Kaya!", + contact_mail: process.env.CONTACT_MAIL || process.env.MAIL_FROM, } let errors = 0 if (typeof config.internal_port !== "number") { diff --git a/src/controllers/MailController.ts b/src/controllers/MailController.ts index 337318f..99be6c2 100644 --- a/src/controllers/MailController.ts +++ b/src/controllers/MailController.ts @@ -4,6 +4,7 @@ import { Mailer } from '../Mailer'; import { locales } from '../models/LocaleEnum'; import { ResetMail } from '../models/ResetMail'; import { SuccessResponse } from '../models/SuccessResponse'; +import { WelcomeMail } from '../models/WelcomeMail'; /** * The mail controller handels all endpoints concerning Mail sending. @@ -45,4 +46,20 @@ export class MailController { } return new SuccessResponse(locale); } + + @Post('/registration') + @OpenAPI({ description: "Sends registration welcome mails", parameters: [{ in: "query", name: "locale", schema: { type: "string", enum: ["de", "en"] } }] }) + async sendRegistrationWelcome(@Body({ validate: true }) mailOptions: WelcomeMail, @QueryParam("locale") locale: locales) { + if (!this.initialized) { + await this.mailer.init(); + this.initialized = true; + } + try { + this.mailer.sendWelcomeMail(mailOptions.address, mailOptions.selfserviceToken, locale?.toString()) + } catch (error) { + console.log(error) + throw error; + } + return new SuccessResponse(locale); + } } diff --git a/src/locales/de.json b/src/locales/de.json index 5e980de..82de50b 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -1,7 +1,10 @@ { "a-password-reset-for-your-account-got-requested": "Ein Passwort Reset wurde für dein Konto beantragt.", "all-rights-reserved": "Alle Rechte vorbehalten", + "event_name-registration": "{{event_name}} Registrierung", + "if-you-didnt-register-yourself-you-should-contact-us-to-get-your-data-removed-from-our-systems": "Solltest du dich nicht selbst registriert haben schick uns bitte eine Mail und wir entfernen deine Daten aus unserem System: ", "if-you-didnt-request-the-reset-please-ignore-this-mail": "Solltest du den Reset nicht beantragt haben kannst du diese Mail einfach ignorieren.", + "if-you-ever-loose-the-link-you-can-request-a-new-one-by-visiting-our-website": "Solltest du den Link verlieren kannst du auf unserer Website einen neuen beantragen:", "imprint": "Impressum", "lfk-mail-test": "{{copyright_owner}} - Mail test", "lfk-password-reset": "{{copyright_owner}} - Passwort zurücksetzen", @@ -9,8 +12,15 @@ "privacy": "Datenschutz", "reset-password": "Passwort zurücksetzen", "test-mail": "Test mail", - "this-is-a-test-mail-triggered-by-an-admin-in-the-lfk-backend": "Das ist eine Testmail, die von einem Admin im LfK! backend erzeugt wurde.", + "thanks-for-registering-and-welcome-to-the-event_name": "Vielen Dank für die Registrierung und willkommen beim {{event_name}}!", + "the-only-thing-you-have-to-do-now-is-to-bring-your-registration-code-with-you": "Du must nichts weiter machen, außer deinen Registrierungscode zum Lauf mitzubringen.", + "this-is-a-test-mail-triggered-by-an-admin-in-the-lfk-backend": "Das ist eine Testmail, die von einem Admin im LfK! Backend erzeugt wurde.", "this-mail-was-sent-to-recipient_mail-because-someone-request-a-mail-test-for-this-mail-address": "Du bekommst diese Mail, weil jemand eine Testmail für deine Mail-Adresse angefragt hat.", "this-mail-was-sent-to-you-because-someone-request-a-password-reset-for-a-account-linked-to-the-mail-address": "Du bekommst diese E-Mail, weil jemand einen Passwort-Reset für deinen Account beantragt hat.", + "this-mail-was-sent-to-you-because-someone-used-your-mail-address-to-register-themselfes-for-the-event_name": "Du bekommst diese Mail, weil jemand deine E-Mail-Adresse verwendet hat, um sich beim {{event_name}} zu registrieren.", + "view-my-data": "Meine Daten", + "we-successfully-processed-your-registration": "Wir haben deine Registrierung erfolgreich verarbeitet.", + "welcome": "Willkommen", + "you-can-view-your-registration-code-lap-times-and-much-more-by-visiting-our-selfservice": "Du kannst deinen Registrierungscode, deine Rundenzeiten unv vieles mehr im Selfservice einsehen:", "your-password-wont-be-changed-until-you-click-the-reset-link-below-and-set-a-new-one": "Dein Passwort wird erst zurückgesetzt, wenn du den Reset-Link öffnest und ein neues Passwort setzt." } \ No newline at end of file diff --git a/src/locales/en.json b/src/locales/en.json index 7f56e85..627914c 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1,7 +1,10 @@ { "a-password-reset-for-your-account-got-requested": "A password reset for your account got requested.", "all-rights-reserved": "All rights reserved.", + "event_name-registration": "{{event_name}} Registration", + "if-you-didnt-register-yourself-you-should-contact-us-to-get-your-data-removed-from-our-systems": "If you didn't register yourself you should contact us to get your data removed from our systems:", "if-you-didnt-request-the-reset-please-ignore-this-mail": "If you didn't request the reset please ignore this mail.", + "if-you-ever-loose-the-link-you-can-request-a-new-one-by-visiting-our-website": "If you ever loose the link you can request a new one by visiting our website:", "imprint": "Imprint", "lfk-mail-test": "{{copyright_owner}} - Mail test", "lfk-password-reset": "{{copyright_owner}} - Password reset", @@ -9,8 +12,15 @@ "privacy": "Privacy", "reset-password": "Reset password", "test-mail": "Test mail", + "thanks-for-registering-and-welcome-to-the-event_name": "Thanks for registering and welcome to the {{event_name}}!", + "the-only-thing-you-have-to-do-now-is-to-bring-your-registration-code-with-you": "The only thing you have to do now is to bring your registration code with you.", "this-is-a-test-mail-triggered-by-an-admin-in-the-lfk-backend": "This is a test mail triggered by an admin in the LfK! backend.", "this-mail-was-sent-to-recipient_mail-because-someone-request-a-mail-test-for-this-mail-address": "This mail was sent to you because someone request a mail test for this mail address.", "this-mail-was-sent-to-you-because-someone-request-a-password-reset-for-a-account-linked-to-the-mail-address": "This mail was sent to you because someone request a password reset for a account linked to the mail address.", + "this-mail-was-sent-to-you-because-someone-used-your-mail-address-to-register-themselfes-for-the-event_name": "This mail was sent to you, because someone used your mail address to register themselfes for the {{event_name}}", + "view-my-data": "View my data", + "we-successfully-processed-your-registration": "We successfully processed your registration.", + "welcome": "Welcome", + "you-can-view-your-registration-code-lap-times-and-much-more-by-visiting-our-selfservice": "You can view your registration code, lap times and much more by visiting our selfservice:", "your-password-wont-be-changed-until-you-click-the-reset-link-below-and-set-a-new-one": "Your password won't be changed until you click the reset link below and set a new one." } \ No newline at end of file diff --git a/src/models/WelcomeMail.ts b/src/models/WelcomeMail.ts new file mode 100644 index 0000000..78cd928 --- /dev/null +++ b/src/models/WelcomeMail.ts @@ -0,0 +1,16 @@ +import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; + +/** + * Simple welcome mail request class for validation and easier handling. + */ +export class WelcomeMail { + + @IsString() + @IsEmail() + @IsNotEmpty() + address: string; + + @IsString() + @IsNotEmpty() + selfserviceToken: string; +} \ No newline at end of file diff --git a/src/templates/welcome_runner.html b/src/templates/welcome_runner.html new file mode 100644 index 0000000..cf35fa8 --- /dev/null +++ b/src/templates/welcome_runner.html @@ -0,0 +1,397 @@ + + + + + {{__ "event_name-registration"}} + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ {{__ "event_name-registration"}} +
+ + + + +
+ + \ No newline at end of file diff --git a/src/templates/welcome_runner.txt b/src/templates/welcome_runner.txt new file mode 100644 index 0000000..7cc050f --- /dev/null +++ b/src/templates/welcome_runner.txt @@ -0,0 +1,17 @@ +{{__ "event_name-registration"}} + +{{__ "thanks-for-registering-and-welcome-to-the-event_name"}} +{{__ "we-successfully-processed-your-registration"}} + +{{__ "the-only-thing-you-have-to-do-now-is-to-bring-your-registration-code-with-you"}} +{{__ "you-can-view-your-registration-code-lap-times-and-much-more-by-visiting-our-selfservice"}} + +{{selfservice_link}} + +{{__ "if-you-ever-loose-the-link-you-can-request-a-new-one-by-visiting-our-website"}} {{forgot_link}} + + +Copyright © {{copyright_owner}}. {{__ "all-rights-reserved"}}. +{{__ "imprint"}}: {{link_imprint}} | {{__ "privacy"}}: {{link_privacy}} +{{__ "this-mail-was-sent-to-you-because-someone-used-your-mail-address-to-register-themselfes-for-the-event_name"}} +{{__ "if-you-didnt-register-yourself-you-should-contact-us-to-get-your-data-removed-from-our-systems"}} {{contact_mail}} \ No newline at end of file diff --git a/src/tests/api_docs.spec.ts b/src/tests/api_docs.spec.ts new file mode 100644 index 0000000..9e9444a --- /dev/null +++ b/src/tests/api_docs.spec.ts @@ -0,0 +1,34 @@ +import axios from 'axios'; +import { config } from '../config'; +const base = "http://localhost:" + config.internal_port + +describe('GET /docs/openapi.json', () => { + it('OpenAPI Spec is availdable 200', async () => { + const res = await axios.get(base + '/docs/openapi.json'); + expect(res.status).toEqual(200); + }); +}); +describe('GET /docs/swagger.json', () => { + it('OpenAPI Spec is availdable 200', async () => { + const res = await axios.get(base + '/docs/swagger.json'); + expect(res.status).toEqual(200); + }); +}); +describe('GET /docs/swaggerui', () => { + it('swaggerui is availdable 200', async () => { + const res = await axios.get(base + '/docs/swaggerui'); + expect(res.status).toEqual(200); + }); +}); +describe('GET /docs/redoc', () => { + it('redoc is availdable 200', async () => { + const res = await axios.get(base + '/docs/redoc'); + expect(res.status).toEqual(200); + }); +}); +describe('GET /docs/rapidoc', () => { + it('rapidoc is availdable 200', async () => { + const res = await axios.get(base + '/docs/rapidoc'); + expect(res.status).toEqual(200); + }); +}); diff --git a/src/tests/pw_reset_mail.spec.ts b/src/tests/pw_reset_mail.spec.ts new file mode 100644 index 0000000..d43f3a3 --- /dev/null +++ b/src/tests/pw_reset_mail.spec.ts @@ -0,0 +1,72 @@ +import axios from 'axios'; +import { config } from '../config'; +const base = "http://localhost:" + config.internal_port + +const axios_config = { + validateStatus: undefined +}; + +describe('POST /reset without auth', () => { + it('Post without auth should return 401', async () => { + const res = await axios.post(base + '/reset', null, axios_config); + expect(res.status).toEqual(401); + }); +}); + +describe('POST /reset with auth but wrong body', () => { + it('Post with auth but no body should return 400', async () => { + const res = await axios.post(base + '/reset?key=' + config.api_key, null, axios_config); + expect(res.status).toEqual(400); + }); + it('Post with auth but no mail should return 400', async () => { + const res = await axios.post(base + '/reset?key=' + config.api_key, { resetKey: "test" }, axios_config); + expect(res.status).toEqual(400); + }); + it('Post with auth but no reset key should return 400', async () => { + const res = await axios.post(base + '/reset?key=' + config.api_key, { address: "test@dev.lauf-fuer-kaya.de" }, axios_config); + expect(res.status).toEqual(400); + }); + it('Post with auth but invalid mail should return 400', async () => { + const res = await axios.post(base + '/reset?key=' + config.api_key, { resetKey: "test", address: "testdev.l.de" }, axios_config); + expect(res.status).toEqual(400); + }); +}); + +describe('POST /reset with auth and vaild body', () => { + it('Post with auth, body and no locale should return 200', async () => { + const res = await axios.post(base + '/reset?key=' + config.api_key, { + resetKey: "test", + address: "test@dev.lauf-fuer-kaya.de" + }, axios_config); + expect(res.status).toEqual(200); + expect(res.data).toEqual({ + success: true, + message: "Sent!", + locale: "en" + }) + }); + it('Post with auth, body and locale=en should return 200', async () => { + const res = await axios.post(base + '/reset?locale=en&key=' + config.api_key, { + resetKey: "test", + address: "test@dev.lauf-fuer-kaya.de" + }, axios_config); + expect(res.status).toEqual(200); + expect(res.data).toEqual({ + success: true, + message: "Sent!", + locale: "en" + }) + }); + it('Post with auth, body and locale=de should return 200', async () => { + const res = await axios.post(base + '/reset?locale=de&key=' + config.api_key, { + resetKey: "test", + address: "test@dev.lauf-fuer-kaya.de" + }, axios_config); + expect(res.status).toEqual(200); + expect(res.data).toEqual({ + success: true, + message: "Sent!", + locale: "de" + }) + }); +}); \ No newline at end of file diff --git a/src/tests/selfservice_welcome_mail.spec.ts b/src/tests/selfservice_welcome_mail.spec.ts new file mode 100644 index 0000000..709eec0 --- /dev/null +++ b/src/tests/selfservice_welcome_mail.spec.ts @@ -0,0 +1,72 @@ +import axios from 'axios'; +import { config } from '../config'; +const base = "http://localhost:" + config.internal_port + +const axios_config = { + validateStatus: undefined +}; + +describe('POST /registration without auth', () => { + it('Post without auth should return 401', async () => { + const res = await axios.post(base + '/registration', null, axios_config); + expect(res.status).toEqual(401); + }); +}); + +describe('POST /registration with auth but wrong body', () => { + it('Post with auth but no body should return 400', async () => { + const res = await axios.post(base + '/registration?key=' + config.api_key, null, axios_config); + expect(res.status).toEqual(400); + }); + it('Post with auth but no mail should return 400', async () => { + const res = await axios.post(base + '/registration?key=' + config.api_key, { selfserviceToken: "test" }, axios_config); + expect(res.status).toEqual(400); + }); + it('Post with auth but no reset key should return 400', async () => { + const res = await axios.post(base + '/registration?key=' + config.api_key, { address: "test@dev.lauf-fuer-kaya.de" }, axios_config); + expect(res.status).toEqual(400); + }); + it('Post with auth but invalid mail should return 400', async () => { + const res = await axios.post(base + '/registration?key=' + config.api_key, { selfserviceToken: "test", address: "testdev.l.de" }, axios_config); + expect(res.status).toEqual(400); + }); +}); + +describe('POST /reset with auth and vaild body', () => { + it('Post with auth, body and no locale should return 200', async () => { + const res = await axios.post(base + '/registration?key=' + config.api_key, { + selfserviceToken: "test", + address: "test@dev.lauf-fuer-kaya.de" + }, axios_config); + expect(res.status).toEqual(200); + expect(res.data).toEqual({ + success: true, + message: "Sent!", + locale: "en" + }) + }); + it('Post with auth, body and locale=en should return 200', async () => { + const res = await axios.post(base + '/registration?locale=en&key=' + config.api_key, { + selfserviceToken: "test", + address: "test@dev.lauf-fuer-kaya.de" + }, axios_config); + expect(res.status).toEqual(200); + expect(res.data).toEqual({ + success: true, + message: "Sent!", + locale: "en" + }) + }); + it('Post with auth, body and locale=de should return 200', async () => { + const res = await axios.post(base + '/registration?locale=de&key=' + config.api_key, { + selfserviceToken: "test", + address: "test@dev.lauf-fuer-kaya.de" + }, axios_config); + expect(res.status).toEqual(200); + expect(res.data).toEqual({ + success: true, + message: "Sent!", + locale: "de" + }) + }); +}); \ No newline at end of file diff --git a/src/tests/test_mail.spec.ts b/src/tests/test_mail.spec.ts new file mode 100644 index 0000000..ff3dac6 --- /dev/null +++ b/src/tests/test_mail.spec.ts @@ -0,0 +1,44 @@ +import axios from 'axios'; +import { config } from '../config'; +const base = "http://localhost:" + config.internal_port + +const axios_config = { + validateStatus: undefined +}; + +describe('POST /test without auth', () => { + it('Post without auth should return 401', async () => { + const res = await axios.post(base + '/test', null, axios_config); + expect(res.status).toEqual(401); + }); +}); + +describe('POST /test with auth', () => { + it('Post with auth and no locale should return 200', async () => { + const res = await axios.post(base + '/test?key=' + config.api_key, null, axios_config); + expect(res.status).toEqual(200); + expect(res.data).toEqual({ + success: true, + message: "Sent!", + locale: "en" + }) + }); + it('Post with auth and locale=en should return 200', async () => { + const res = await axios.post(base + '/test?locale=en&key=' + config.api_key, null, axios_config); + expect(res.status).toEqual(200); + expect(res.data).toEqual({ + success: true, + message: "Sent!", + locale: "en" + }) + }); + it('Post with auth and locale=de should return 200', async () => { + const res = await axios.post(base + '/test?locale=de&key=' + config.api_key, null, axios_config); + expect(res.status).toEqual(200); + expect(res.data).toEqual({ + success: true, + message: "Sent!", + locale: "de" + }) + }); +}); \ No newline at end of file