From 03890f209a4dec789c3c247838b9fe442efabcd2 Mon Sep 17 00:00:00 2001 From: Jonas Lochmann Date: Mon, 27 Dec 2021 01:00:00 +0100 Subject: [PATCH] Send mail notifications for new devices and password resets --- other/mail/device-linked-by-mail/html.ejs | 190 ++++++++++++++++++ .../htmltemplate-src.txt | 41 ++++ other/mail/device-linked-by-mail/subject.ejs | 1 + other/mail/device-linked-by-mail/text.ejs | 15 ++ other/mail/password-recovery-used/html.ejs | 190 ++++++++++++++++++ .../htmltemplate-src.txt | 39 ++++ other/mail/password-recovery-used/subject.ejs | 1 + other/mail/password-recovery-used/text.ejs | 13 ++ src/function/authentication/index.ts | 15 +- src/function/parent/create-family.ts | 8 +- .../parent/get-status-by-mail-address.ts | 6 +- src/function/parent/link-mail-address.ts | 8 +- .../parent/recover-parent-password.ts | 14 +- src/function/parent/sign-in-into-family.ts | 15 +- src/util/mail.ts | 49 +++++ 15 files changed, 583 insertions(+), 22 deletions(-) create mode 100644 other/mail/device-linked-by-mail/html.ejs create mode 100644 other/mail/device-linked-by-mail/htmltemplate-src.txt create mode 100644 other/mail/device-linked-by-mail/subject.ejs create mode 100644 other/mail/device-linked-by-mail/text.ejs create mode 100644 other/mail/password-recovery-used/html.ejs create mode 100644 other/mail/password-recovery-used/htmltemplate-src.txt create mode 100644 other/mail/password-recovery-used/subject.ejs create mode 100644 other/mail/password-recovery-used/text.ejs diff --git a/other/mail/device-linked-by-mail/html.ejs b/other/mail/device-linked-by-mail/html.ejs new file mode 100644 index 0000000..6ca08df --- /dev/null +++ b/other/mail/device-linked-by-mail/html.ejs @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+
TimeLimit
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+

<%= preText %> <%= deviceName %> <%= postText %>

+

<%= securityText %>

+
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+

+

+ +
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+

Sie erhalten diese Nachricht, da ein Gerät zu Ihrem Konto hinzugefügt wurde. Mit diesem Gerät können die TimeLimit-Einstellungen ohne ein Passwort geändert werden. Falls Sie Fragen haben können Sie einfach auf diese E-Mail antworten.

+

You got this message because a device was added to your account. This device can be used to change the TimeLimit configuration without any password. If you have got any questions, then you can reply to this messagge.

+

© <%= mailimprint %>

+
+
+
+ +
+
+ +
+ + + diff --git a/other/mail/device-linked-by-mail/htmltemplate-src.txt b/other/mail/device-linked-by-mail/htmltemplate-src.txt new file mode 100644 index 0000000..808d939 --- /dev/null +++ b/other/mail/device-linked-by-mail/htmltemplate-src.txt @@ -0,0 +1,41 @@ + + + + + TimeLimit + + + + + +

<%= preText %> <%= deviceName %> <%= postText %>

+

<%= securityText %>

+
+
+
+ + + + + + + + +

+ Sie erhalten diese Nachricht, da ein Gerät zu Ihrem Konto hinzugefügt wurde. + Mit diesem Gerät können die TimeLimit-Einstellungen ohne ein Passwort geändert werden. + Falls Sie Fragen haben können Sie einfach auf diese E-Mail antworten. +

+

+ You got this message because a device was added to your account. + This device can be used to change the TimeLimit configuration without any password. + If you have got any questions, then you can reply to this messagge. +

+

+ © <%= mailimprint %> +

+
+
+
+
+
diff --git a/other/mail/device-linked-by-mail/subject.ejs b/other/mail/device-linked-by-mail/subject.ejs new file mode 100644 index 0000000..1520fa4 --- /dev/null +++ b/other/mail/device-linked-by-mail/subject.ejs @@ -0,0 +1 @@ +<%- subject %> diff --git a/other/mail/device-linked-by-mail/text.ejs b/other/mail/device-linked-by-mail/text.ejs new file mode 100644 index 0000000..69901ee --- /dev/null +++ b/other/mail/device-linked-by-mail/text.ejs @@ -0,0 +1,15 @@ +<%- preText %> <%- deviceName %> <%- postText %> + +<%- securityText %> + +---------------------- + +Sie erhalten diese Nachricht, da ein Gerät zu Ihrem Konto hinzugefügt wurde. +Mit diesem Gerät können die TimeLimit-Einstellungen ohne ein Passwort geändert werden. +Falls Sie Fragen haben können Sie einfach auf diese E-Mail antworten. + +You got this message because a device was added to your account. +This device can be used to change the TimeLimit configuration without any password. +If you have got any questions, then you can reply to this messagge. + + <%- mailimprint %> diff --git a/other/mail/password-recovery-used/html.ejs b/other/mail/password-recovery-used/html.ejs new file mode 100644 index 0000000..baaabca --- /dev/null +++ b/other/mail/password-recovery-used/html.ejs @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+
TimeLimit
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+

<%= text %>

+

<%= securityText %>

+
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+

+

+ +
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+

Sie erhalten diese Nachricht, da Ihr TimeLimit-Passwort geändert wurde. Falls Sie Fragen haben können Sie einfach auf diese E-Mail antworten.

+

You got this message because the password reset feature was used for your account. If you have got any questions, then you can reply to this messagge.

+

© <%= mailimprint %>

+
+
+
+ +
+
+ +
+ + + diff --git a/other/mail/password-recovery-used/htmltemplate-src.txt b/other/mail/password-recovery-used/htmltemplate-src.txt new file mode 100644 index 0000000..780b482 --- /dev/null +++ b/other/mail/password-recovery-used/htmltemplate-src.txt @@ -0,0 +1,39 @@ + + + + + TimeLimit + + + + + +

<%= text %>

+

<%= securityText %>

+
+
+
+ + + + + + + + +

+ Sie erhalten diese Nachricht, da Ihr TimeLimit-Passwort geändert wurde. + Falls Sie Fragen haben können Sie einfach auf diese E-Mail antworten. +

+

+ You got this message because the password reset feature was used for your account. + If you have got any questions, then you can reply to this messagge. +

+

+ © <%= mailimprint %> +

+
+
+
+
+
diff --git a/other/mail/password-recovery-used/subject.ejs b/other/mail/password-recovery-used/subject.ejs new file mode 100644 index 0000000..1520fa4 --- /dev/null +++ b/other/mail/password-recovery-used/subject.ejs @@ -0,0 +1 @@ +<%- subject %> diff --git a/other/mail/password-recovery-used/text.ejs b/other/mail/password-recovery-used/text.ejs new file mode 100644 index 0000000..7a025c6 --- /dev/null +++ b/other/mail/password-recovery-used/text.ejs @@ -0,0 +1,13 @@ +<%- text %> + +<%- securityText %> + +---------------------- + +Sie erhalten diese Nachricht, da Ihr TimeLimit-Passwort geändert wurde. +Falls Sie Fragen haben können Sie einfach auf diese E-Mail antworten. + +You got this message because the password reset feature was used for your account. +If you have got any questions, then you can reply to this messagge. + + <%- mailimprint %> diff --git a/src/function/authentication/index.ts b/src/function/authentication/index.ts index eed0f9b..6351acf 100644 --- a/src/function/authentication/index.ts +++ b/src/function/authentication/index.ts @@ -36,7 +36,7 @@ export const createAuthTokenByMailAddress = async ({ return token } -export const getMailByAuthToken = async ({ +export const getMailAndLocaleByAuthToken = async ({ mailAuthToken, database, transaction, invalidate }: { mailAuthToken: string, database: Database, transaction: Transaction, invalidate: boolean @@ -62,22 +62,25 @@ export const getMailByAuthToken = async ({ } } - return entry.mail + return { + mail: entry.mail, + locale: entry.locale + } } else { return null } } -export const requireMailByAuthToken = async ({ +export const requireMailAndLocaleByAuthToken = async ({ mailAuthToken, database, transaction, invalidate }: { mailAuthToken: string, database: Database, transaction: Transaction, invalidate: boolean }) => { - const mail = await getMailByAuthToken({ mailAuthToken, database, transaction, invalidate }) + const result = await getMailAndLocaleByAuthToken({ mailAuthToken, database, transaction, invalidate }) - if (!mail) { + if (!result) { throw new Unauthorized() } - return mail + return result } diff --git a/src/function/parent/create-family.ts b/src/function/parent/create-family.ts index 8d9452f..efdb800 100644 --- a/src/function/parent/create-family.ts +++ b/src/function/parent/create-family.ts @@ -22,7 +22,7 @@ import { maxMailNotificationFlags } from '../../database/user' import { generateAuthToken, generateFamilyId, generateIdWithinFamily, generateVersionId } from '../../util/token' -import { requireMailByAuthToken } from '../authentication' +import { requireMailAndLocaleByAuthToken } from '../authentication' import { prepareDeviceEntry } from '../device/prepare-device-entry' export const createFamily = async ({ database, mailAuthToken, firstParentDevice, password, timeZone, parentName, deviceName }: { @@ -37,12 +37,12 @@ export const createFamily = async ({ database, mailAuthToken, firstParentDevice, }) => { return database.transaction(async (transaction) => { const now = Date.now().toString(10) - const mail = await requireMailByAuthToken({ database, mailAuthToken, transaction, invalidate: true }) + const mailInfo = await requireMailAndLocaleByAuthToken({ database, mailAuthToken, transaction, invalidate: true }) // ensure that no family was created for this mail yet const exisitngUserEntry = await database.user.findOne({ where: { - mail + mail: mailInfo.mail }, transaction }) @@ -77,7 +77,7 @@ export const createFamily = async ({ database, mailAuthToken, firstParentDevice, secondPasswordHash: password.secondHash, secondPasswordSalt: password.secondSalt, type: 'parent', - mail, + mail: mailInfo.mail, timeZone, disableTimelimitsUntil: '0', currentDevice: '', diff --git a/src/function/parent/get-status-by-mail-address.ts b/src/function/parent/get-status-by-mail-address.ts index f98e57c..1f5d967 100644 --- a/src/function/parent/get-status-by-mail-address.ts +++ b/src/function/parent/get-status-by-mail-address.ts @@ -17,7 +17,7 @@ import { Database, Transaction } from '../../database' import { StaticMessageException } from '../../exception' -import { requireMailByAuthToken } from '../authentication' +import { requireMailAndLocaleByAuthToken } from '../authentication' const getStatusByMailAddress = async ({ mail, database, transaction @@ -43,7 +43,9 @@ const getStatusByMailAddress = async ({ export const getStatusByMailToken = async ({ mailAuthToken, database, transaction }: { mailAuthToken: string, database: Database, transaction: Transaction }) => { - const mail = await requireMailByAuthToken({ mailAuthToken, database, transaction, invalidate: false }) + const mailInfo = await requireMailAndLocaleByAuthToken({ mailAuthToken, database, transaction, invalidate: false }) + const mail = mailInfo.mail + const status = await getStatusByMailAddress({ mail, database, transaction }) return { mail, status } diff --git a/src/function/parent/link-mail-address.ts b/src/function/parent/link-mail-address.ts index 4fcd1db..7390cbb 100644 --- a/src/function/parent/link-mail-address.ts +++ b/src/function/parent/link-mail-address.ts @@ -19,7 +19,7 @@ import { Conflict, Unauthorized } from 'http-errors' import { Database } from '../../database' import { generateVersionId } from '../../util/token' import { WebsocketApi } from '../../websocket' -import { requireMailByAuthToken } from '../authentication' +import { requireMailAndLocaleByAuthToken } from '../authentication' import { notifyClientsAboutChangesDelayed } from '../websocket' export const linkMailAddress = async ({ mailAuthToken, deviceAuthToken, parentUserId, parentPasswordSecondHash, database, websocket }: { @@ -45,11 +45,11 @@ export const linkMailAddress = async ({ mailAuthToken, deviceAuthToken, parentUs const familyId = deviceEntry.familyId - const mailAddress = await requireMailByAuthToken({ mailAuthToken, database, transaction, invalidate: true }) + const mailInfo = await requireMailAndLocaleByAuthToken({ mailAuthToken, database, transaction, invalidate: true }) const exisitingUser = await database.user.findOne({ where: { - mail: mailAddress + mail: mailInfo.mail }, transaction }) @@ -83,7 +83,7 @@ export const linkMailAddress = async ({ mailAuthToken, deviceAuthToken, parentUs throw new Conflict() } - parentEntry.mail = mailAddress + parentEntry.mail = mailInfo.mail await parentEntry.save({ transaction }) diff --git a/src/function/parent/recover-parent-password.ts b/src/function/parent/recover-parent-password.ts index ad57de2..6a17f99 100644 --- a/src/function/parent/recover-parent-password.ts +++ b/src/function/parent/recover-parent-password.ts @@ -18,9 +18,10 @@ import { Conflict } from 'http-errors' import { ParentPassword } from '../../api/schema' import { Database } from '../../database' +import { sendPasswordRecoveryUsedMail } from '../../util/mail' import { generateVersionId } from '../../util/token' import { WebsocketApi } from '../../websocket' -import { requireMailByAuthToken } from '../authentication' +import { requireMailAndLocaleByAuthToken } from '../authentication' import { notifyClientsAboutChangesDelayed } from '../websocket' export const recoverParentPassword = async ({ database, websocket, password, mailAuthToken }: { @@ -31,12 +32,12 @@ export const recoverParentPassword = async ({ database, websocket, password, mai // no transaction here because this is directly called from an API endpoint }) => { await database.transaction(async (transaction) => { - const mail = await requireMailByAuthToken({ mailAuthToken, database, transaction, invalidate: true }) + const mailInfo = await requireMailAndLocaleByAuthToken({ mailAuthToken, database, transaction, invalidate: true }) // update the user entry const userEntry = await database.user.findOne({ where: { - mail + mail: mailInfo.mail }, transaction }) @@ -69,5 +70,12 @@ export const recoverParentPassword = async ({ database, websocket, password, mai sourceDeviceId: null, transaction }) + + transaction.afterCommit(async () => { + await sendPasswordRecoveryUsedMail({ + receiver: mailInfo.mail, + locale: mailInfo.locale + }) + }) }) } diff --git a/src/function/parent/sign-in-into-family.ts b/src/function/parent/sign-in-into-family.ts index c774773..9d8073a 100644 --- a/src/function/parent/sign-in-into-family.ts +++ b/src/function/parent/sign-in-into-family.ts @@ -18,9 +18,10 @@ import { Conflict } from 'http-errors' import { NewDeviceInfo } from '../../api/schema' import { Database } from '../../database' +import { sendDeviceLinkedMail } from '../../util/mail' import { generateAuthToken, generateIdWithinFamily, generateVersionId } from '../../util/token' import { WebsocketApi } from '../../websocket' -import { requireMailByAuthToken } from '../authentication' +import { requireMailAndLocaleByAuthToken } from '../authentication' import { prepareDeviceEntry } from '../device/prepare-device-entry' import { notifyClientsAboutChangesDelayed } from '../websocket' @@ -33,11 +34,11 @@ export const signInIntoFamily = async ({ database, mailAuthToken, newDeviceInfo, // no transaction here because this is directly called from an API endpoint }): Promise<{ deviceId: string; deviceAuthToken: string }> => { return database.transaction(async (transaction) => { - const mail = await requireMailByAuthToken({ database, mailAuthToken, transaction, invalidate: true }) + const mailInfo = await requireMailAndLocaleByAuthToken({ database, mailAuthToken, transaction, invalidate: true }) const userEntryUnsafe = await database.user.findOne({ where: { - mail + mail: mailInfo.mail }, attributes: ['familyId', 'userId'], transaction @@ -84,6 +85,14 @@ export const signInIntoFamily = async ({ database, mailAuthToken, newDeviceInfo, transaction }) + transaction.afterCommit(async () => { + await sendDeviceLinkedMail({ + receiver: mailInfo.mail, + locale: mailInfo.locale, + deviceName + }) + }) + return { deviceId, deviceAuthToken diff --git a/src/util/mail.ts b/src/util/mail.ts index f9db73d..23ab288 100644 --- a/src/util/mail.ts +++ b/src/util/mail.ts @@ -108,6 +108,55 @@ export const sendTaskDoneMail = async ({ receiver, child, task }: { }) } +export const sendDeviceLinkedMail = async ({ receiver, deviceName, locale }: { + receiver: string + deviceName: string + locale: string +}) => { + await email.send({ + template: join(__dirname, '../../other/mail/device-linked-by-mail'), + message: { + to: receiver + }, + locals: { + subject: locale === 'de' ? 'Gerät hinzugefügt' : 'Device added', + preText: locale === 'de' ? 'Soeben wurde das Gerät' : 'The device', + deviceName, + postText: locale === 'de' ? 'über Ihre E-Mail-Adresse hinzugefügt.' : 'was added using your mail address.', + securityText: getMailSecurityText(locale), + mailimprint + } + }) +} + +export const sendPasswordRecoveryUsedMail = async ({ receiver, locale }: { + receiver: string + locale: string +}) => { + await email.send({ + template: join(__dirname, '../../other/mail/password-recovery-used'), + message: { + to: receiver + }, + locals: { + subject: locale === 'de' ? 'Passwort-Vergessen-Funktion verwendet' : 'Password reset', + text: locale === 'de' ? + 'Soeben wurde Ihr TimeLimit-Passwort mit der Passwort-Vergessen-Funktion geändert.' : + 'Your password was changed using the password reset feature.', + securityText: getMailSecurityText(locale), + mailimprint + } + }) +} + +function getMailSecurityText (locale: string) { + if (locale === 'de') { + return 'Achten Sie darauf, dass Ihr Kind/Ihre Kinder keinen Zugang zu der E-Mail-Adresse hat/haben, die Sie bei TimeLimit angegeben haben.' + } else { + return 'Make sure that your child/children can not access the mail addresss that you use for TimeLimit.' + } +} + export function isMailServerBlacklisted (mail: string): boolean { const parts = mail.split('@') const domain = parts[parts.length - 1]