Send mail notifications for new devices and password resets

This commit is contained in:
Jonas Lochmann
2021-12-27 01:00:00 +01:00
parent 50c0982bd3
commit 03890f209a
15 changed files with 583 additions and 22 deletions
+9 -6
View File
@@ -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
}
+4 -4
View File
@@ -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: '',
@@ -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 }
+4 -4
View File
@@ -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 })
+11 -3
View File
@@ -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
})
})
})
}
+12 -3
View File
@@ -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
+49
View File
@@ -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]