From 65f764f6d93db15e34c23e98b5041254f44043e8 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 5 May 2025 20:44:14 +0200 Subject: [PATCH] feat(backend): signup email localized (#8459) * localized registration email * localized email verification email * localized reset password email --- .../sendChatMessageMail.spec.ts.snap | 8 + .../sendEmailVerification.spec.ts.snap | 261 ++++++++ .../sendNotificationMail.spec.ts.snap | 72 +++ .../sendRegistrationMail.spec.ts.snap | 559 ++++++++++++++++++ .../sendResetPasswordMail.spec.ts.snap | 260 ++++++++ .../__snapshots__/sendWrongEmail.spec.ts.snap | 255 ++++++++ backend/src/emails/locales/de.json | 36 +- backend/src/emails/locales/en.json | 35 +- backend/src/emails/sendEmail.ts | 135 +++++ .../src/emails/sendEmailVerification.spec.ts | 35 ++ .../src/emails/sendRegistrationMail.spec.ts | 63 ++ .../src/emails/sendResetPasswordMail.spec.ts | 35 ++ backend/src/emails/sendWrongEmail.spec.ts | 31 + .../templates/emailVerification/html.pug | 10 + .../templates/emailVerification/subject.pug | 1 + .../emails/templates/includes/greeting.pug | 11 +- .../src/emails/templates/includes/webflow.css | 4 + .../src/emails/templates/includes/welcome.pug | 1 + backend/src/emails/templates/layout.pug | 8 +- .../emails/templates/registration/html.pug | 15 + .../emails/templates/registration/subject.pug | 1 + .../emails/templates/resetPassword/html.pug | 9 + .../templates/resetPassword/subject.pug | 1 + .../src/emails/templates/wrongEmail/html.pug | 10 + .../emails/templates/wrongEmail/subject.pug | 1 + backend/src/graphql/resolvers/emails.ts | 1 + .../graphql/resolvers/passwordReset.spec.ts | 8 +- .../graphql/resolvers/registration.spec.ts | 6 +- .../src/graphql/types/type/EmailAddress.gql | 6 +- backend/src/graphql/types/type/User.gql | 2 +- .../src/middleware/login/loginMiddleware.ts | 32 +- .../middleware/permissionsMiddleware.spec.ts | 8 +- .../components/PasswordReset/Request.spec.js | 14 +- webapp/components/PasswordReset/Request.vue | 6 +- .../Registration/RegistrationSlideEmail.vue | 6 +- webapp/components/Registration/Signup.spec.js | 5 +- webapp/components/Registration/Signup.vue | 6 +- 37 files changed, 1907 insertions(+), 50 deletions(-) create mode 100644 backend/src/emails/__snapshots__/sendEmailVerification.spec.ts.snap create mode 100644 backend/src/emails/__snapshots__/sendRegistrationMail.spec.ts.snap create mode 100644 backend/src/emails/__snapshots__/sendResetPasswordMail.spec.ts.snap create mode 100644 backend/src/emails/__snapshots__/sendWrongEmail.spec.ts.snap create mode 100644 backend/src/emails/sendEmailVerification.spec.ts create mode 100644 backend/src/emails/sendRegistrationMail.spec.ts create mode 100644 backend/src/emails/sendResetPasswordMail.spec.ts create mode 100644 backend/src/emails/sendWrongEmail.spec.ts create mode 100644 backend/src/emails/templates/emailVerification/html.pug create mode 100644 backend/src/emails/templates/emailVerification/subject.pug create mode 100644 backend/src/emails/templates/includes/welcome.pug create mode 100644 backend/src/emails/templates/registration/html.pug create mode 100644 backend/src/emails/templates/registration/subject.pug create mode 100644 backend/src/emails/templates/resetPassword/html.pug create mode 100644 backend/src/emails/templates/resetPassword/subject.pug create mode 100644 backend/src/emails/templates/wrongEmail/html.pug create mode 100644 backend/src/emails/templates/wrongEmail/subject.pug diff --git a/backend/src/emails/__snapshots__/sendChatMessageMail.spec.ts.snap b/backend/src/emails/__snapshots__/sendChatMessageMail.spec.ts.snap index fd7b90395..57b256a12 100644 --- a/backend/src/emails/__snapshots__/sendChatMessageMail.spec.ts.snap +++ b/backend/src/emails/__snapshots__/sendChatMessageMail.spec.ts.snap @@ -62,6 +62,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -185,6 +189,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; diff --git a/backend/src/emails/__snapshots__/sendEmailVerification.spec.ts.snap b/backend/src/emails/__snapshots__/sendEmailVerification.spec.ts.snap new file mode 100644 index 000000000..34c945d65 --- /dev/null +++ b/backend/src/emails/__snapshots__/sendEmailVerification.spec.ts.snap @@ -0,0 +1,261 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`sendEmailVerification English renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social", + "html": " + + + + + + + + +
+
+
+
+
+

Hello User,

+
+
+

So, you want to change your e-mail? No problem! Just click the button below to verify your new address:

Verify e-mail address +

If you don't want to change your e-mail address feel free to ignore this message.

+

If the above button doesn't work, you can also copy the following code into your browser window: 123456

+
+

See you soon on ocelot.social!

+

– The ocelot.social Team

+
+
+ +
+ +", + "subject": "New E-Mail Address ocelot.social", + "text": "HELLO USER, + +So, you want to change your e-mail? No problem! Just click the button below to +verify your new address: + +Verify e-mail address +[http://webapp:3000/settings/my-email-address/verify?email=user%40example.org&nonce=123456] + +If you don't want to change your e-mail address feel free to ignore this +message. + +If the above button doesn't work, you can also copy the following code into your +browser window: 123456 + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendEmailVerification German renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social", + "html": " + + + + + + + + +
+
+
+
+
+

Hallo User,

+
+
+

Du möchtest also deine E-Mail ändern? Kein Problem! Mit Klick auf diesen Button kannst Du Deine neue E-Mail Adresse bestätigen:

E-Mail Adresse bestätigen +

Falls Du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese Nachricht einfach ignorieren.

+

Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: 123456

+
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team

+
+
+ +
+ +", + "subject": "Neue E-Mail Addresse ocelot.social", + "text": "HALLO USER, + +Du möchtest also deine E-Mail ändern? Kein Problem! Mit Klick auf diesen Button +kannst Du Deine neue E-Mail Adresse bestätigen: + +E-Mail Adresse bestätigen +[http://webapp:3000/settings/my-email-address/verify?email=user%40example.org&nonce=123456] + +Falls Du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese +Nachricht einfach ignorieren. + +Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in +Dein Browserfenster kopieren: 123456 + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; diff --git a/backend/src/emails/__snapshots__/sendNotificationMail.spec.ts.snap b/backend/src/emails/__snapshots__/sendNotificationMail.spec.ts.snap index 698ae9082..0fec27b7c 100644 --- a/backend/src/emails/__snapshots__/sendNotificationMail.spec.ts.snap +++ b/backend/src/emails/__snapshots__/sendNotificationMail.spec.ts.snap @@ -62,6 +62,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -184,6 +188,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -308,6 +316,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -432,6 +444,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -556,6 +572,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -679,6 +699,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -801,6 +825,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -920,6 +948,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -1043,6 +1075,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -1166,6 +1202,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -1288,6 +1328,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -1412,6 +1456,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -1536,6 +1584,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -1660,6 +1712,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -1783,6 +1839,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -1905,6 +1965,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -2024,6 +2088,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; @@ -2147,6 +2215,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; diff --git a/backend/src/emails/__snapshots__/sendRegistrationMail.spec.ts.snap b/backend/src/emails/__snapshots__/sendRegistrationMail.spec.ts.snap new file mode 100644 index 000000000..3b8d1c077 --- /dev/null +++ b/backend/src/emails/__snapshots__/sendRegistrationMail.spec.ts.snap @@ -0,0 +1,559 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`sendRegistrationMail with invite code English renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social", + "html": " + + + + + + + + +
+
+
+
+
+

Welcome to ocelot.social!

+
+
+

Thank you for joining our cause – it's awesome to have you on board. There's just one tiny step missing before we can start shaping the world together … Please confirm your e-mail address by clicking the button below:

Confirm your e-mail address +

If the above button doesn't work, you can also copy the following code into your browser window: 123456

+

However, this only works if you have registered through our website.

+

If you didn't sign up for ocelot.social we recommend you to check it out! It's a social network from people for people who want to connect and change the world together. +

+

PS: If you ignore this e-mail we will not create an account for you. ;)

+
+

See you soon on ocelot.social!

+

– The ocelot.social Team

+
+
+ +
+ +", + "subject": "Welcome to ocelot.social", + "text": "WELCOME TO OCELOT.SOCIAL! + +Thank you for joining our cause – it's awesome to have you on board. There's +just one tiny step missing before we can start shaping the world together … +Please confirm your e-mail address by clicking the button below: + +Confirm your e-mail address +[http://webapp:3000/registration?email=user%40example.org&nonce=123456&inviteCode=welcome&method=invite-code] + +If the above button doesn't work, you can also copy the following code into your +browser window: 123456 + +However, this only works if you have registered through our website. + +If you didn't sign up for ocelot.social we recommend you to check it out! It's a +social network from people for people who want to connect and change the world +together. + +PS: If you ignore this e-mail we will not create an account for you. ;) + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendRegistrationMail with invite code German renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social", + "html": " + + + + + + + + +
+
+
+
+
+

Willkommen bei ocelot.social!

+
+
+

Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können … Bitte bestätige Deine E-Mail Adresse:

Bestätige Deine E-Mail Adresse +

Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: 123456

+

Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert hast.

+

Falls Du Dich nicht selbst bei ocelot.social angemeldet hast, schau doch mal vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen. +

+

PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese E-Mail einfach ignorieren. ;)

+
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team

+
+
+ +
+ +", + "subject": "Willkommen bei ocelot.social", + "text": "WILLKOMMEN BEI OCELOT.SOCIAL! + +Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt +fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können +… Bitte bestätige Deine E-Mail Adresse: + +Bestätige Deine E-Mail Adresse +[http://webapp:3000/registration?email=user%40example.org&nonce=123456&inviteCode=welcome&method=invite-code] + +Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in +Dein Browserfenster kopieren: 123456 + +Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert +hast. + +Falls Du Dich nicht selbst bei ocelot.social angemeldet hast, schau doch mal +vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen. + +PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese E-Mail einfach +ignorieren. ;) + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendRegistrationMail without invite code English renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social", + "html": " + + + + + + + + +
+
+
+
+
+

Welcome to ocelot.social!

+
+
+

Thank you for joining our cause – it's awesome to have you on board. There's just one tiny step missing before we can start shaping the world together … Please confirm your e-mail address by clicking the button below:

Confirm your e-mail address +

If the above button doesn't work, you can also copy the following code into your browser window: 123456

+

However, this only works if you have registered through our website.

+

If you didn't sign up for ocelot.social we recommend you to check it out! It's a social network from people for people who want to connect and change the world together. +

+

PS: If you ignore this e-mail we will not create an account for you. ;)

+
+

See you soon on ocelot.social!

+

– The ocelot.social Team

+
+
+ +
+ +", + "subject": "Welcome to ocelot.social", + "text": "WELCOME TO OCELOT.SOCIAL! + +Thank you for joining our cause – it's awesome to have you on board. There's +just one tiny step missing before we can start shaping the world together … +Please confirm your e-mail address by clicking the button below: + +Confirm your e-mail address +[http://webapp:3000/registration?email=user%40example.org&nonce=123456&method=invite-mail] + +If the above button doesn't work, you can also copy the following code into your +browser window: 123456 + +However, this only works if you have registered through our website. + +If you didn't sign up for ocelot.social we recommend you to check it out! It's a +social network from people for people who want to connect and change the world +together. + +PS: If you ignore this e-mail we will not create an account for you. ;) + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendRegistrationMail without invite code German renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social", + "html": " + + + + + + + + +
+
+
+
+
+

Willkommen bei ocelot.social!

+
+
+

Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können … Bitte bestätige Deine E-Mail Adresse:

Bestätige Deine E-Mail Adresse +

Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: 123456

+

Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert hast.

+

Falls Du Dich nicht selbst bei ocelot.social angemeldet hast, schau doch mal vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen. +

+

PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese E-Mail einfach ignorieren. ;)

+
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team

+
+
+ +
+ +", + "subject": "Willkommen bei ocelot.social", + "text": "WILLKOMMEN BEI OCELOT.SOCIAL! + +Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt +fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können +… Bitte bestätige Deine E-Mail Adresse: + +Bestätige Deine E-Mail Adresse +[http://webapp:3000/registration?email=user%40example.org&nonce=123456&method=invite-mail] + +Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in +Dein Browserfenster kopieren: 123456 + +Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert +hast. + +Falls Du Dich nicht selbst bei ocelot.social angemeldet hast, schau doch mal +vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen. + +PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese E-Mail einfach +ignorieren. ;) + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; diff --git a/backend/src/emails/__snapshots__/sendResetPasswordMail.spec.ts.snap b/backend/src/emails/__snapshots__/sendResetPasswordMail.spec.ts.snap new file mode 100644 index 000000000..3d8c6ac27 --- /dev/null +++ b/backend/src/emails/__snapshots__/sendResetPasswordMail.spec.ts.snap @@ -0,0 +1,260 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`sendResetPasswordMail English renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social", + "html": " + + + + + + + + +
+
+
+
+
+

Hello Jenny Rostock,

+
+
+

So, you forgot your password? No problem! Just click the button below to reset it within the next 24 hours:

Confirm your e-mail address +

If you didn't request a new password feel free to ignore this e-mail.

+

If the above button doesn't work you can also copy the following code into your browser window: 123456

+
+

See you soon on ocelot.social!

+

– The ocelot.social Team

+
+
+ +
+ +", + "subject": "Reset Password ocelot.social", + "text": "HELLO JENNY ROSTOCK, + +So, you forgot your password? No problem! Just click the button below to reset +it within the next 24 hours: + +Confirm your e-mail address +[http://webapp:3000/password-reset/change-password?email=user%40example.org&nonce=123456] + +If you didn't request a new password feel free to ignore this e-mail. + +If the above button doesn't work you can also copy the following code into your +browser window: 123456 + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendResetPasswordMail German renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social", + "html": " + + + + + + + + +
+
+
+
+
+

Hallo Jenny Rostock,

+
+
+

Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button kannst du innerhalb der nächsten 24 Stunden dein Passwort zurücksetzen:

Bestätige Deine E-Mail Adresse +

Falls du kein neues Passwort angefordert hast, kannst du diese E-Mail einfach ignorieren.

+

Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in Dein Browserfenster kopieren: 123456

+
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team

+
+
+ +
+ +", + "subject": "Neues Passwort ocelot.social", + "text": "HALLO JENNY ROSTOCK, + +Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button +kannst du innerhalb der nächsten 24 Stunden dein Passwort zurücksetzen: + +Bestätige Deine E-Mail Adresse +[http://webapp:3000/password-reset/change-password?email=user%40example.org&nonce=123456] + +Falls du kein neues Passwort angefordert hast, kannst du diese E-Mail einfach +ignorieren. + +Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in +Dein Browserfenster kopieren: 123456 + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; diff --git a/backend/src/emails/__snapshots__/sendWrongEmail.spec.ts.snap b/backend/src/emails/__snapshots__/sendWrongEmail.spec.ts.snap new file mode 100644 index 000000000..72acc52cd --- /dev/null +++ b/backend/src/emails/__snapshots__/sendWrongEmail.spec.ts.snap @@ -0,0 +1,255 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`sendWrongEmail English renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social", + "html": " + + + + + + + + +
+
+
+
+
+

Welcome to ocelot.social!

+
+
+

You requested a password reset but unfortunately we couldn't find an account associated with your e-mail address. Did you maybe use another one when you signed up?

Try a different e-mail +

If you don't have an account at ocelot.social yet or if you didn't want to reset your password, please ignore this e-mail. +

+
+

See you soon on ocelot.social!

+

– The ocelot.social Team

+
+
+ +
+ +", + "subject": "Wrong E-mail? ocelot.social", + "text": "WELCOME TO OCELOT.SOCIAL! + +You requested a password reset but unfortunately we couldn't find an account +associated with your e-mail address. Did you maybe use another one when you +signed up? + +Try a different e-mail [http://webapp:3000/password-reset/request] + +If you don't have an account at ocelot.social yet or if you didn't want to reset +your password, please ignore this e-mail. + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendWrongEmail German renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social", + "html": " + + + + + + + + +
+
+
+
+
+

Willkommen bei ocelot.social!

+
+
+

Du hast bei uns ein neues Passwort angefordert – leider haben wir aber keinen Account mit deiner E-Mailadresse gefunden. Kann es sein, dass du mit einer anderen Adresse bei uns angemeldet bist?

Versuch' es mit einer anderen E-Mail +

Wenn du noch keinen Account bei ocelot.social hast oder dein Password gar nicht ändern willst, kannst du diese E-Mail einfach ignorieren! +

+
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team

+
+
+ +
+ +", + "subject": "Falsche Mailaddresse? ocelot.social", + "text": "WILLKOMMEN BEI OCELOT.SOCIAL! + +Du hast bei uns ein neues Passwort angefordert – leider haben wir aber keinen +Account mit deiner E-Mailadresse gefunden. Kann es sein, dass du mit einer +anderen Adresse bei uns angemeldet bist? + +Versuch' es mit einer anderen E-Mail [http://webapp:3000/password-reset/request] + +Wenn du noch keinen Account bei ocelot.social hast oder dein Password gar nicht +ändern willst, kannst du diese E-Mail einfach ignorieren! + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; diff --git a/backend/src/emails/locales/de.json b/backend/src/emails/locales/de.json index d09991262..9e0ce843a 100644 --- a/backend/src/emails/locales/de.json +++ b/backend/src/emails/locales/de.json @@ -7,12 +7,32 @@ "followedUserPosted": "Neuer Beitrag von gefolgtem Nutzer", "mentionedInComment": "Erwähnung in Kommentar", "mentionedInPost": "Erwähnung in Beitrag", + "newEmail": "Neue E-Mail Addresse", "removedUserFromGroup": "Aus Gruppe entfernt", "postInGroup": "Neuer Beitrag in Gruppe", + "resetPassword": "Neues Passwort", "userJoinedGroup": "Nutzer tritt Gruppe bei", - "userLeftGroup": "Nutzer verlässt Gruppe" + "userLeftGroup": "Nutzer verlässt Gruppe", + "wrongEmail": "Falsche Mailaddresse?" + }, + "registration": { + "introduction": "Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können … Bitte bestätige Deine E-Mail Adresse:", + "codeHint": "Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: ", + "codeHintException": "Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert hast.", + "notYouStart": "Falls Du Dich nicht selbst bei ", + "notYouEnd": " angemeldet hast, schau doch mal vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen.", + "ps": "PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese E-Mail einfach ignorieren. ;)" + }, + "emailVerification": { + "codeHint": "Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: ", + "introduction": "Du möchtest also deine E-Mail ändern? Kein Problem! Mit Klick auf diesen Button kannst Du Deine neue E-Mail Adresse bestätigen:", + "doNotChange": "Falls Du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese Nachricht einfach ignorieren. " }, "buttons": { + "confirmEmail": "Bestätige Deine E-Mail Adresse", + "resetPassword": "Passwort zurücksetzen", + "tryAgain": "Versuch' es mit einer anderen E-Mail", + "verifyEmail": "E-Mail Adresse bestätigen", "viewChat": "Chat anzeigen", "viewComment": "Kommentar ansehen", "viewGroup": "Gruppe ansehen", @@ -23,7 +43,19 @@ "seeYou": "Bis bald bei ", "yourTeam": "– Dein {team} Team", "settingsHint": "PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine ", - "settingsName": "Benachrichtigungseinstellungen" + "settingsName": "Benachrichtigungseinstellungen", + "welcome": "Willkommen bei" + }, + "resetPassword": { + "codeHint": "Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in Dein Browserfenster kopieren: ", + "ignore": "Falls du kein neues Passwort angefordert hast, kannst du diese E-Mail einfach ignorieren.", + "introduction": "Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button kannst du innerhalb der nächsten 24 Stunden dein Passwort zurücksetzen:" + }, + "wrongEmail": { + "codeHint": "Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: ", + "ignoreEnd": " hast oder dein Password gar nicht ändern willst, kannst du diese E-Mail einfach ignorieren!", + "ignoreStart": "Wenn du noch keinen Account bei ", + "introduction": "Du hast bei uns ein neues Passwort angefordert – leider haben wir aber keinen Account mit deiner E-Mailadresse gefunden. Kann es sein, dass du mit einer anderen Adresse bei uns angemeldet bist?" }, "changedGroupMemberRole": "deine Rolle in der Gruppe „{groupName}“ wurde geändert. Klicke auf den Knopf, um diese Gruppe zu sehen:", "chatMessageStart": "du hast eine neue Chat-Nachricht von ", diff --git a/backend/src/emails/locales/en.json b/backend/src/emails/locales/en.json index f14f469ae..30ca64655 100644 --- a/backend/src/emails/locales/en.json +++ b/backend/src/emails/locales/en.json @@ -7,12 +7,32 @@ "followedUserPosted": "New post by followd user", "mentionedInComment": "Mentioned in comment", "mentionedInPost": "Mentioned in post", + "newEmail": "New E-Mail Address", "removedUserFromGroup": "Removed from group", "postInGroup": "New post in group", + "resetPassword": "Reset Password", "userJoinedGroup": "User joined group", - "userLeftGroup": "User left group" + "userLeftGroup": "User left group", + "wrongEmail": "Wrong E-mail?" + }, + "registration": { + "introduction": "Thank you for joining our cause – it's awesome to have you on board. There's just one tiny step missing before we can start shaping the world together … Please confirm your e-mail address by clicking the button below:", + "codeHint": "If the above button doesn't work, you can also copy the following code into your browser window: ", + "codeHintException": "However, this only works if you have registered through our website.", + "notYouStart": "If you didn't sign up for ", + "notYouEnd": " we recommend you to check it out! It's a social network from people for people who want to connect and change the world together.", + "ps": "PS: If you ignore this e-mail we will not create an account for you. ;)" + }, + "emailVerification": { + "codeHint": "If the above button doesn't work, you can also copy the following code into your browser window: ", + "introduction": "So, you want to change your e-mail? No problem! Just click the button below to verify your new address:", + "doNotChange": "If you don't want to change your e-mail address feel free to ignore this message. " }, "buttons": { + "confirmEmail": "Confirm your e-mail address", + "resetPassword": "Reset password", + "tryAgain": "Try a different e-mail", + "verifyEmail": "Verify e-mail address", "viewChat": "Show Chat", "viewComment": "View comment", "viewGroup": "View group", @@ -23,7 +43,18 @@ "seeYou": "See you soon on ", "yourTeam": "– The {team} Team", "settingsHint": "PS: If you don't want to receive e-mails anymore, change your ", - "settingsName": "notification settings" + "settingsName": "notification settings", + "welcome": "Welcome to" + }, + "resetPassword": { + "codeHint": "If the above button doesn't work you can also copy the following code into your browser window: ", + "ignore": "If you didn't request a new password feel free to ignore this e-mail.", + "introduction": "So, you forgot your password? No problem! Just click the button below to reset it within the next 24 hours:" + }, + "wrongEmail": { + "ignoreEnd": " yet or if you didn't want to reset your password, please ignore this e-mail.", + "ignoreStart": "If you don't have an account at ", + "introduction": "You requested a password reset but unfortunately we couldn't find an account associated with your e-mail address. Did you maybe use another one when you signed up?" }, "changedGroupMemberRole": "your role in the group “{groupName}” has been changed. Click on the button to view this group:", "chatMessageStart": "you have received a new chat message from ", diff --git a/backend/src/emails/sendEmail.ts b/backend/src/emails/sendEmail.ts index 460a3984a..7b7ea76b3 100644 --- a/backend/src/emails/sendEmail.ts +++ b/backend/src/emails/sendEmail.ts @@ -28,6 +28,7 @@ const defaultParams = { ORGANIZATION_URL: CONFIG.ORGANIZATION_URL, supportUrl: CONFIG.SUPPORT_URL, settingsUrl, + renderSettingsUrl: true, } export const transport = createTransport({ @@ -202,3 +203,137 @@ export const sendChatMessageMail = async ( throw new Error(error) } } + +interface VerifyMailInput { + email: string + nonce: string + locale: string +} + +interface RegistrationMailInput extends VerifyMailInput { + inviteCode?: string +} + +export const sendRegistrationMail = async ( + data: RegistrationMailInput, +): Promise => { + const { nonce, locale, inviteCode } = data + const to = data.email + const actionUrl = new URL('/registration', CONFIG.CLIENT_URI) + actionUrl.searchParams.set('email', to) + actionUrl.searchParams.set('nonce', nonce) + if (inviteCode) { + actionUrl.searchParams.set('inviteCode', inviteCode) + actionUrl.searchParams.set('method', 'invite-code') + } else { + actionUrl.searchParams.set('method', 'invite-mail') + } + + try { + const { originalMessage } = await email.send({ + template: path.join(__dirname, 'templates', 'registration'), + message: { + to, + }, + locals: { + ...defaultParams, + locale, + actionUrl, + nonce, + renderSettingsUrl: false, + }, + }) + return originalMessage as OriginalMessage + } catch (error) { + throw new Error(error) + } +} + +interface EmailVerificationInput extends VerifyMailInput { + name: string +} + +export const sendEmailVerification = async ( + data: EmailVerificationInput, +): Promise => { + const { nonce, locale, name } = data + const to = data.email + const actionUrl = new URL('/settings/my-email-address/verify', CONFIG.CLIENT_URI) + actionUrl.searchParams.set('email', to) + actionUrl.searchParams.set('nonce', nonce) + + try { + const { originalMessage } = await email.send({ + template: path.join(__dirname, 'templates', 'emailVerification'), + message: { + to, + }, + locals: { + ...defaultParams, + locale, + actionUrl, + nonce, + name, + renderSettingsUrl: false, + }, + }) + return originalMessage as OriginalMessage + } catch (error) { + throw new Error(error) + } +} + +export const sendResetPasswordMail = async ( + data: EmailVerificationInput, +): Promise => { + const { nonce, locale, name } = data + const to = data.email + const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI) + actionUrl.searchParams.set('email', to) + actionUrl.searchParams.set('nonce', nonce) + try { + const { originalMessage } = await email.send({ + template: path.join(__dirname, 'templates', 'resetPassword'), + message: { + to, + }, + locals: { + ...defaultParams, + locale, + actionUrl, + nonce, + name, + renderSettingsUrl: false, + }, + }) + return originalMessage as OriginalMessage + } catch (error) { + throw new Error(error) + } +} + +export const sendWrongEmail = async (data: { + locale: string + email: string +}): Promise => { + const { locale } = data + const to = data.email + const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI) + try { + const { originalMessage } = await email.send({ + template: path.join(__dirname, 'templates', 'wrongEmail'), + message: { + to, + }, + locals: { + ...defaultParams, + locale, + actionUrl, + renderSettingsUrl: false, + }, + }) + return originalMessage as OriginalMessage + } catch (error) { + throw new Error(error) + } +} diff --git a/backend/src/emails/sendEmailVerification.spec.ts b/backend/src/emails/sendEmailVerification.spec.ts new file mode 100644 index 000000000..0863dd9db --- /dev/null +++ b/backend/src/emails/sendEmailVerification.spec.ts @@ -0,0 +1,35 @@ +import { sendEmailVerification } from './sendEmail' + +describe('sendEmailVerification', () => { + const data: { + email: string + nonce: string + locale: string + name: string + } = { + email: 'user@example.org', + nonce: '123456', + locale: 'en', + name: 'User', + } + + describe('English', () => { + beforeEach(() => { + data.locale = 'en' + }) + + it('renders correctly', async () => { + await expect(sendEmailVerification(data)).resolves.toMatchSnapshot() + }) + }) + + describe('German', () => { + beforeEach(() => { + data.locale = 'de' + }) + + it('renders correctly', async () => { + await expect(sendEmailVerification(data)).resolves.toMatchSnapshot() + }) + }) +}) diff --git a/backend/src/emails/sendRegistrationMail.spec.ts b/backend/src/emails/sendRegistrationMail.spec.ts new file mode 100644 index 000000000..ea66771c2 --- /dev/null +++ b/backend/src/emails/sendRegistrationMail.spec.ts @@ -0,0 +1,63 @@ +import { sendRegistrationMail } from './sendEmail' + +describe('sendRegistrationMail', () => { + const data: { + email: string + nonce: string + locale: string + inviteCode?: string + } = { + email: 'user@example.org', + nonce: '123456', + locale: 'en', + inviteCode: 'welcome', + } + + describe('with invite code', () => { + describe('English', () => { + beforeEach(() => { + data.locale = 'en' + data.inviteCode = 'welcome' + }) + + it('renders correctly', async () => { + await expect(sendRegistrationMail(data)).resolves.toMatchSnapshot() + }) + }) + + describe('German', () => { + beforeEach(() => { + data.locale = 'de' + data.inviteCode = 'welcome' + }) + + it('renders correctly', async () => { + await expect(sendRegistrationMail(data)).resolves.toMatchSnapshot() + }) + }) + }) + + describe('without invite code', () => { + describe('English', () => { + beforeEach(() => { + data.locale = 'en' + delete data.inviteCode + }) + + it('renders correctly', async () => { + await expect(sendRegistrationMail(data)).resolves.toMatchSnapshot() + }) + }) + + describe('German', () => { + beforeEach(() => { + data.locale = 'de' + delete data.inviteCode + }) + + it('renders correctly', async () => { + await expect(sendRegistrationMail(data)).resolves.toMatchSnapshot() + }) + }) + }) +}) diff --git a/backend/src/emails/sendResetPasswordMail.spec.ts b/backend/src/emails/sendResetPasswordMail.spec.ts new file mode 100644 index 000000000..e37af2e7b --- /dev/null +++ b/backend/src/emails/sendResetPasswordMail.spec.ts @@ -0,0 +1,35 @@ +import { sendResetPasswordMail } from './sendEmail' + +describe('sendResetPasswordMail', () => { + const data: { + email: string + nonce: string + locale: string + name: string + } = { + email: 'user@example.org', + nonce: '123456', + locale: 'en', + name: 'Jenny Rostock', + } + + describe('English', () => { + beforeEach(() => { + data.locale = 'en' + }) + + it('renders correctly', async () => { + await expect(sendResetPasswordMail(data)).resolves.toMatchSnapshot() + }) + }) + + describe('German', () => { + beforeEach(() => { + data.locale = 'de' + }) + + it('renders correctly', async () => { + await expect(sendResetPasswordMail(data)).resolves.toMatchSnapshot() + }) + }) +}) diff --git a/backend/src/emails/sendWrongEmail.spec.ts b/backend/src/emails/sendWrongEmail.spec.ts new file mode 100644 index 000000000..854d935f9 --- /dev/null +++ b/backend/src/emails/sendWrongEmail.spec.ts @@ -0,0 +1,31 @@ +import { sendWrongEmail } from './sendEmail' + +describe('sendWrongEmail', () => { + const data: { + email: string + locale: string + } = { + email: 'user@example.org', + locale: 'en', + } + + describe('English', () => { + beforeEach(() => { + data.locale = 'en' + }) + + it('renders correctly', async () => { + await expect(sendWrongEmail(data)).resolves.toMatchSnapshot() + }) + }) + + describe('German', () => { + beforeEach(() => { + data.locale = 'de' + }) + + it('renders correctly', async () => { + await expect(sendWrongEmail(data)).resolves.toMatchSnapshot() + }) + }) +}) diff --git a/backend/src/emails/templates/emailVerification/html.pug b/backend/src/emails/templates/emailVerification/html.pug new file mode 100644 index 000000000..7483106e4 --- /dev/null +++ b/backend/src/emails/templates/emailVerification/html.pug @@ -0,0 +1,10 @@ +extend ../layout.pug + +block content + .content + p= t('emailVerification.introduction') + a.button(href=actionUrl)= t('buttons.verifyEmail') + p= t('emailVerification.doNotChange') + + p= t('emailVerification.codeHint') + span= nonce diff --git a/backend/src/emails/templates/emailVerification/subject.pug b/backend/src/emails/templates/emailVerification/subject.pug new file mode 100644 index 000000000..5fc98a7b9 --- /dev/null +++ b/backend/src/emails/templates/emailVerification/subject.pug @@ -0,0 +1 @@ += `${t('subjects.newEmail')} ${APPLICATION_NAME}` \ No newline at end of file diff --git a/backend/src/emails/templates/includes/greeting.pug b/backend/src/emails/templates/includes/greeting.pug index 26ae259c5..6b682fc2d 100644 --- a/backend/src/emails/templates/includes/greeting.pug +++ b/backend/src/emails/templates/includes/greeting.pug @@ -3,12 +3,15 @@ - var organizationUrl = ORGANIZATION_URL - var team = APPLICATION_NAME - var settingsUrl = settingsUrl + - var renderSettingsUrl = renderSettingsUrl p= t('general.seeYou') a.organization(href=organizationUrl)= team | ! p= t('general.yourTeam', { team }) - br - p= t('general.settingsHint') - a.settings(href=settingsUrl)= t('general.settingsName') - | ! + + if renderSettingsUrl + br + p= t('general.settingsHint') + a.settings(href=settingsUrl)= t('general.settingsName') + | ! diff --git a/backend/src/emails/templates/includes/webflow.css b/backend/src/emails/templates/includes/webflow.css index c7ea12921..1dc1f0b24 100644 --- a/backend/src/emails/templates/includes/webflow.css +++ b/backend/src/emails/templates/includes/webflow.css @@ -50,6 +50,10 @@ a.button { border-radius: 4px; } +span { + color: #17b53e; +} + .text-block { margin-top: 20px; color: #000000; diff --git a/backend/src/emails/templates/includes/welcome.pug b/backend/src/emails/templates/includes/welcome.pug new file mode 100644 index 000000000..f4ec6f8bd --- /dev/null +++ b/backend/src/emails/templates/includes/welcome.pug @@ -0,0 +1 @@ +h2= `${t('general.welcome')} ${APPLICATION_NAME}!` \ No newline at end of file diff --git a/backend/src/emails/templates/layout.pug b/backend/src/emails/templates/layout.pug index 898776323..faaadb5d3 100644 --- a/backend/src/emails/templates/layout.pug +++ b/backend/src/emails/templates/layout.pug @@ -13,11 +13,15 @@ html(lang=locale) .wf-force-outline-none[tabindex="-1"]:focus{outline:none;} style include includes/webflow.css - + + - var name = name body div.container include includes/header.pug - include includes/salutation.pug + if name + include includes/salutation.pug + else + include includes/welcome.pug .wrapper block content diff --git a/backend/src/emails/templates/registration/html.pug b/backend/src/emails/templates/registration/html.pug new file mode 100644 index 000000000..b50aaca31 --- /dev/null +++ b/backend/src/emails/templates/registration/html.pug @@ -0,0 +1,15 @@ +extend ../layout.pug + +block content + .content + p= t('registration.introduction') + a.button(href=actionUrl)= t('buttons.confirmEmail') + p= t('registration.codeHint') + span= nonce + p= t('registration.codeHintException') + + p= t('registration.notYouStart') + a(href=ORGANIZATION_LINK)= APPLICATION_NAME + = t('registration.notYouEnd') + + p= t('registration.ps') \ No newline at end of file diff --git a/backend/src/emails/templates/registration/subject.pug b/backend/src/emails/templates/registration/subject.pug new file mode 100644 index 000000000..7e9dbec7f --- /dev/null +++ b/backend/src/emails/templates/registration/subject.pug @@ -0,0 +1 @@ += `${t('general.welcome')} ${APPLICATION_NAME}` \ No newline at end of file diff --git a/backend/src/emails/templates/resetPassword/html.pug b/backend/src/emails/templates/resetPassword/html.pug new file mode 100644 index 000000000..f10ee01c2 --- /dev/null +++ b/backend/src/emails/templates/resetPassword/html.pug @@ -0,0 +1,9 @@ +extend ../layout.pug + +block content + .content + p= t('resetPassword.introduction') + a.button(href=actionUrl)= t('buttons.confirmEmail') + p= t('resetPassword.ignore') + p= t('resetPassword.codeHint') + span= nonce diff --git a/backend/src/emails/templates/resetPassword/subject.pug b/backend/src/emails/templates/resetPassword/subject.pug new file mode 100644 index 000000000..047af2052 --- /dev/null +++ b/backend/src/emails/templates/resetPassword/subject.pug @@ -0,0 +1 @@ += `${t('subjects.resetPassword')} ${APPLICATION_NAME}` \ No newline at end of file diff --git a/backend/src/emails/templates/wrongEmail/html.pug b/backend/src/emails/templates/wrongEmail/html.pug new file mode 100644 index 000000000..79f97833f --- /dev/null +++ b/backend/src/emails/templates/wrongEmail/html.pug @@ -0,0 +1,10 @@ +extend ../layout.pug + +block content + .content + p= t('wrongEmail.introduction') + a.button(href=actionUrl)= t('buttons.tryAgain') + + p= t('wrongEmail.ignoreStart') + a(href=ORGANIZATION_LINK)= APPLICATION_NAME + = t('wrongEmail.ignoreEnd') diff --git a/backend/src/emails/templates/wrongEmail/subject.pug b/backend/src/emails/templates/wrongEmail/subject.pug new file mode 100644 index 000000000..b6bc2d01c --- /dev/null +++ b/backend/src/emails/templates/wrongEmail/subject.pug @@ -0,0 +1 @@ += `${t('subjects.wrongEmail')} ${APPLICATION_NAME}` \ No newline at end of file diff --git a/backend/src/graphql/resolvers/emails.ts b/backend/src/graphql/resolvers/emails.ts index be721dda5..0491c86ad 100644 --- a/backend/src/graphql/resolvers/emails.ts +++ b/backend/src/graphql/resolvers/emails.ts @@ -69,6 +69,7 @@ export default { ) return result.records.map((record) => ({ name: record.get('user').properties.name, + locale: record.get('user').properties.locale, ...record.get('email').properties, })) }) diff --git a/backend/src/graphql/resolvers/passwordReset.spec.ts b/backend/src/graphql/resolvers/passwordReset.spec.ts index d5d08265c..3bc4d53ba 100644 --- a/backend/src/graphql/resolvers/passwordReset.spec.ts +++ b/backend/src/graphql/resolvers/passwordReset.spec.ts @@ -71,14 +71,14 @@ describe('passwordReset', () => { describe('requestPasswordReset', () => { const mutation = gql` - mutation ($email: String!) { - requestPasswordReset(email: $email) + mutation ($email: String!, $locale: String!) { + requestPasswordReset(email: $email, locale: $locale) } ` describe('with invalid email', () => { beforeEach(() => { - variables = { ...variables, email: 'non-existent@example.org' } + variables = { ...variables, email: 'non-existent@example.org', locale: 'de' } }) it('resolves anyways', async () => { @@ -96,7 +96,7 @@ describe('passwordReset', () => { describe('with a valid email', () => { beforeEach(() => { - variables = { ...variables, email: 'user@example.org' } + variables = { ...variables, email: 'user@example.org', locale: 'de' } }) it('resolves', async () => { diff --git a/backend/src/graphql/resolvers/registration.spec.ts b/backend/src/graphql/resolvers/registration.spec.ts index ccf5a9e10..d959b348a 100644 --- a/backend/src/graphql/resolvers/registration.spec.ts +++ b/backend/src/graphql/resolvers/registration.spec.ts @@ -50,14 +50,14 @@ afterEach(async () => { describe('Signup', () => { const mutation = gql` - mutation ($email: String!, $inviteCode: String) { - Signup(email: $email, inviteCode: $inviteCode) { + mutation ($email: String!, $locale: String!, $inviteCode: String) { + Signup(email: $email, locale: $locale, inviteCode: $inviteCode) { email } } ` beforeEach(() => { - variables = { ...variables, email: 'someuser@example.org' } + variables = { ...variables, email: 'someuser@example.org', locale: 'de' } }) describe('unauthenticated', () => { diff --git a/backend/src/graphql/types/type/EmailAddress.gql b/backend/src/graphql/types/type/EmailAddress.gql index b2e65eafa..261b97207 100644 --- a/backend/src/graphql/types/type/EmailAddress.gql +++ b/backend/src/graphql/types/type/EmailAddress.gql @@ -9,7 +9,11 @@ type Query { } type Mutation { - Signup(email: String!, inviteCode: String = null): EmailAddress + Signup( + email: String! + locale: String! + inviteCode: String = null + ): EmailAddress SignupVerification( nonce: String! email: String! diff --git a/backend/src/graphql/types/type/User.gql b/backend/src/graphql/types/type/User.gql index 81dd9cf5b..83de35c37 100644 --- a/backend/src/graphql/types/type/User.gql +++ b/backend/src/graphql/types/type/User.gql @@ -245,7 +245,7 @@ type Mutation { updateOnlineStatus(status: OnlineStatus!): Boolean! - requestPasswordReset(email: String!): Boolean! + requestPasswordReset(email: String!, locale: String!): Boolean! resetPassword(email: String!, nonce: String!, newPassword: String!): Boolean! changePassword(oldPassword: String!, newPassword: String!): String! diff --git a/backend/src/middleware/login/loginMiddleware.ts b/backend/src/middleware/login/loginMiddleware.ts index b67e5f60a..35f3df702 100644 --- a/backend/src/middleware/login/loginMiddleware.ts +++ b/backend/src/middleware/login/loginMiddleware.ts @@ -2,43 +2,41 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import { sendMail } from '@middleware/helpers/email/sendMail' import { - signupTemplate, - resetPasswordTemplate, - wrongAccountTemplate, - emailVerificationTemplate, -} from '@middleware/helpers/email/templateBuilder' + sendRegistrationMail, + sendEmailVerification, + sendResetPasswordMail, +} from '@src/emails/sendEmail' const sendSignupMail = async (resolve, root, args, context, resolveInfo) => { - const { inviteCode } = args + const { inviteCode, locale } = args const response = await resolve(root, args, context, resolveInfo) const { email, nonce } = response if (nonce) { // emails that already exist do not have a nonce - if (inviteCode) { - await sendMail(signupTemplate({ email, variables: { nonce, inviteCode } })) - } else { - await sendMail(signupTemplate({ email, variables: { nonce } })) - } + await sendRegistrationMail({ email, nonce, locale, inviteCode }) } delete response.nonce return response } const sendPasswordResetMail = async (resolve, root, args, context, resolveInfo) => { - const { email } = args + const { email, locale } = args const { email: userFound, nonce, name } = await resolve(root, args, context, resolveInfo) - const template = userFound ? resetPasswordTemplate : wrongAccountTemplate - await sendMail(template({ email, variables: { nonce, name } })) + if (userFound) { + await sendResetPasswordMail({ email, nonce, name, locale }) + } else { + // this is an antifeature allowing unauthenticated users to spam any email with wrong-email notifications + // await sendWrongEmail({ email, locale }) + } return true } const sendEmailVerificationMail = async (resolve, root, args, context, resolveInfo) => { const response = await resolve(root, args, context, resolveInfo) - const { email, nonce, name } = response + const { email, nonce, name, locale } = response if (nonce) { - await sendMail(emailVerificationTemplate({ email, variables: { nonce, name } })) + await sendEmailVerification({ email, nonce, name, locale }) } delete response.nonce return response diff --git a/backend/src/middleware/permissionsMiddleware.spec.ts b/backend/src/middleware/permissionsMiddleware.spec.ts index ca45005fe..e8089b7f3 100644 --- a/backend/src/middleware/permissionsMiddleware.spec.ts +++ b/backend/src/middleware/permissionsMiddleware.spec.ts @@ -177,8 +177,8 @@ describe('authorization', () => { describe('access Signup', () => { const signupMutation = gql` - mutation ($email: String!, $inviteCode: String) { - Signup(email: $email, inviteCode: $inviteCode) { + mutation ($email: String!, $locale: String!, $inviteCode: String) { + Signup(email: $email, locale: $locale, inviteCode: $inviteCode) { email } } @@ -189,6 +189,7 @@ describe('authorization', () => { variables = { email: 'some@email.org', inviteCode: 'ABCDEF', + locale: 'de', } CONFIG.INVITE_REGISTRATION = false CONFIG.PUBLIC_REGISTRATION = false @@ -231,6 +232,7 @@ describe('authorization', () => { variables = { email: 'some@email.org', inviteCode: 'ABCDEF', + locale: 'de', } CONFIG.INVITE_REGISTRATION = false CONFIG.PUBLIC_REGISTRATION = true @@ -269,6 +271,7 @@ describe('authorization', () => { variables = { email: 'some@email.org', inviteCode: 'ABCDEF', + locale: 'de', } authenticatedUser = null }) @@ -288,6 +291,7 @@ describe('authorization', () => { variables = { email: 'some@email.org', inviteCode: 'no valid invite code', + locale: 'de', } authenticatedUser = null }) diff --git a/webapp/components/PasswordReset/Request.spec.js b/webapp/components/PasswordReset/Request.spec.js index 50d6495bd..e2f082242 100644 --- a/webapp/components/PasswordReset/Request.spec.js +++ b/webapp/components/PasswordReset/Request.spec.js @@ -59,7 +59,12 @@ describe('Request', () => { }) it('delivers email to backend', () => { - const expected = expect.objectContaining({ variables: { email: 'mail@example.org' } }) + const expected = expect.objectContaining({ + variables: { + email: 'mail@example.org', + locale: 'en', + }, + }) expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected) }) @@ -92,7 +97,12 @@ describe('Request', () => { }) it('normalizes email to lower case letters', () => { - const expected = expect.objectContaining({ variables: { email: 'mail@gmail.com' } }) + const expected = expect.objectContaining({ + variables: { + email: 'mail@gmail.com', + locale: 'en', + }, + }) expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected) }) }) diff --git a/webapp/components/PasswordReset/Request.vue b/webapp/components/PasswordReset/Request.vue index 5398c13ed..3eebeba65 100644 --- a/webapp/components/PasswordReset/Request.vue +++ b/webapp/components/PasswordReset/Request.vue @@ -85,13 +85,13 @@ export default { }, async handleSubmit() { const mutation = gql` - mutation ($email: String!) { - requestPasswordReset(email: $email) + mutation ($email: String!, $locale: String!) { + requestPasswordReset(email: $email, locale: $locale) } ` try { const { email } = this - await this.$apollo.mutate({ mutation, variables: { email } }) + await this.$apollo.mutate({ mutation, variables: { email, locale: this.$i18n.locale() } }) this.submitted = true setTimeout(() => { diff --git a/webapp/components/Registration/RegistrationSlideEmail.vue b/webapp/components/Registration/RegistrationSlideEmail.vue index 6d6454ac9..96441dee8 100644 --- a/webapp/components/Registration/RegistrationSlideEmail.vue +++ b/webapp/components/Registration/RegistrationSlideEmail.vue @@ -36,8 +36,8 @@ import normalizeEmail from '~/components/utils/NormalizeEmail' import translateErrorMessage from '~/components/utils/TranslateErrorMessage' export const SignupMutation = gql` - mutation ($email: String!, $inviteCode: String) { - Signup(email: $email, inviteCode: $inviteCode) { + mutation ($email: String!, $locale: String!, $inviteCode: String) { + Signup(email: $email, locale: $locale, inviteCode: $inviteCode) { email } } @@ -140,7 +140,7 @@ export default { async onNextClick() { const { email } = this.formData const { inviteCode = null } = this.sliderData.collectedInputData - const variables = { email, inviteCode } + const variables = { email, inviteCode, locale: this.$i18n.locale() } if (this.sliderData.collectedInputData.emailSend && !this.sendEmailAgain) { return true diff --git a/webapp/components/Registration/Signup.spec.js b/webapp/components/Registration/Signup.spec.js index 7ef2dc994..2ee413b8b 100644 --- a/webapp/components/Registration/Signup.spec.js +++ b/webapp/components/Registration/Signup.spec.js @@ -25,6 +25,9 @@ describe('Signup', () => { loading: false, mutate: jest.fn().mockResolvedValue({ data: { Signup: { email: 'mail@example.org' } } }), }, + $i18n: { + locale: () => 'de', + }, } propsData = {} }) @@ -64,7 +67,7 @@ describe('Signup', () => { it('delivers email to backend', () => { const expected = expect.objectContaining({ mutation: SignupMutation, - variables: { email: 'mAIL@exAMPLE.org', inviteCode: null }, + variables: { email: 'mAIL@exAMPLE.org', locale: 'de', inviteCode: null }, }) expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected) }) diff --git a/webapp/components/Registration/Signup.vue b/webapp/components/Registration/Signup.vue index 91b9ecd61..156c43d4e 100644 --- a/webapp/components/Registration/Signup.vue +++ b/webapp/components/Registration/Signup.vue @@ -70,8 +70,8 @@ import { SweetalertIcon } from 'vue-sweetalert-icons' import translateErrorMessage from '~/components/utils/TranslateErrorMessage' export const SignupMutation = gql` - mutation ($email: String!, $inviteCode: String) { - Signup(email: $email, inviteCode: $inviteCode) { + mutation ($email: String!, $locale: String!, $inviteCode: String) { + Signup(email: $email, locale: $locale, inviteCode: $inviteCode) { email } } @@ -121,7 +121,7 @@ export default { try { const response = await this.$apollo.mutate({ mutation: SignupMutation, - variables: { email, inviteCode: null }, + variables: { email, locale: this.$i18n.locale(), inviteCode: null }, }) this.data = response.data setTimeout(() => {