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"}} +
+ + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+

{{event_name}}

+
+

{{__ "welcome"}}

+

+ {{__ "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"}} +

+
+ + + + + +
+ {{__ "view-my-data"}} +
+ +
+

Link: {{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"}}     + {{__ "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/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/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