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