Add account deletion API

This commit is contained in:
Jonas Lochmann
2023-04-09 14:30:26 +02:00
parent 05fac79849
commit e46f5bea3f
20 changed files with 705 additions and 119 deletions
+107
View File
@@ -0,0 +1,107 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2023 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Unauthorized } from 'http-errors'
import { DeleteAccountPayload } from '../../api/schema'
import { Database } from '../../database'
import { sendAccountDeletedMail } from '../../util/mail'
import { WebsocketApi } from '../../websocket'
import { requireMailAndLocaleByAuthToken } from '../authentication'
import { deleteFamilies } from './delete-families'
export async function deleteAccount({ request, database, websocket }: {
request: DeleteAccountPayload
database: Database
websocket: WebsocketApi
}) {
await database.transaction(async (transaction) => {
const deviceEntryUnsafe = await database.device.findOne({
where: { deviceAuthToken: request.deviceAuthToken },
attributes: ['familyId'],
transaction
})
if (!deviceEntryUnsafe) {
throw new Unauthorized()
}
const deviceEntry = {
familyId: deviceEntryUnsafe.familyId
}
const userEntries = (await database.user.findAll({
where: {
familyId: deviceEntry.familyId,
type: 'parent'
},
attributes: ['mail'],
transaction
})).map((item) => ({ mail: item.mail }))
const registeredMailAddresses = new Set<string>()
userEntries.forEach((item) => {
if (item.mail !== '') registeredMailAddresses.add(item.mail)
})
const authenticatedMailAddresses = new Set<string>()
for (const mailAuthToken of request.mailAuthTokens) {
const info = await requireMailAndLocaleByAuthToken({
mailAuthToken,
database,
transaction,
invalidate: true
})
if (!registeredMailAddresses.has(info.mail)) throw new Unauthorized()
authenticatedMailAddresses.add(info.mail)
}
if (registeredMailAddresses.size !== authenticatedMailAddresses.size) throw new Unauthorized()
registeredMailAddresses.forEach((mail) => {
if (!authenticatedMailAddresses.has(mail)) throw new Unauthorized()
})
const deviceEntries = (await database.device.findAll({
where: {
familyId: deviceEntry.familyId
},
transaction,
attributes: ['deviceAuthToken']
})).map((item) => ({ deviceAuthToken: item.deviceAuthToken }))
await deleteFamilies({ database, transaction, familiyIds: [deviceEntry.familyId] })
transaction.afterCommit(() => {
for (const device of deviceEntries) {
websocket.triggerSyncByDeviceAuthToken({
deviceAuthToken: device.deviceAuthToken,
isImportant: true
})
}
registeredMailAddresses.forEach((receiver) => {
sendAccountDeletedMail({ receiver }).catch((ex) => {
console.warn('failure while sending account deletion confirmation', ex)
})
})
})
})
}
+108 -109
View File
@@ -17,10 +17,11 @@
import { difference } from 'lodash'
import * as Sequelize from 'sequelize'
import { Database } from '../../database'
import { Database, Transaction } from '../../database'
export async function deleteFamilies ({ database, familiyIds }: {
export async function deleteFamilies ({ database, transaction, familiyIds }: {
database: Database
transaction: Transaction
familiyIds: Array<string>
// no transaction here because this should run isolated
}) {
@@ -28,128 +29,126 @@ export async function deleteFamilies ({ database, familiyIds }: {
return
}
await database.transaction(async (transaction) => {
// category
await database.category.destroy({
where: {
familyId: {
[Sequelize.Op.in]: familiyIds
}
},
transaction
})
// category
await database.category.destroy({
where: {
familyId: {
[Sequelize.Op.in]: familiyIds
}
},
transaction
})
// categoryapp
await database.categoryApp.destroy({
where: {
familyId: {
[Sequelize.Op.in]: familiyIds
}
},
transaction
})
// categoryapp
await database.categoryApp.destroy({
where: {
familyId: {
[Sequelize.Op.in]: familiyIds
}
},
transaction
})
// purchase
await database.purchase.destroy({
where: {
familyId: {
[Sequelize.Op.in]: familiyIds
}
},
transaction
})
// purchase
await database.purchase.destroy({
where: {
familyId: {
[Sequelize.Op.in]: familiyIds
}
},
transaction
})
// timelimitrule
await database.timelimitRule.destroy({
where: {
familyId: {
[Sequelize.Op.in]: familiyIds
}
},
transaction
})
// timelimitrule
await database.timelimitRule.destroy({
where: {
familyId: {
[Sequelize.Op.in]: familiyIds
}
},
transaction
})
// usedtime
await database.usedTime.destroy({
where: {
familyId: {
[Sequelize.Op.in]: familiyIds
}
},
transaction
})
// usedtime
await database.usedTime.destroy({
where: {
familyId: {
[Sequelize.Op.in]: familiyIds
}
},
transaction
})
// session durations
await database.sessionDuration.destroy({
where: {
familyId: {
[Sequelize.Op.in]: familiyIds
}
},
transaction
})
// session durations
await database.sessionDuration.destroy({
where: {
familyId: {
[Sequelize.Op.in]: familiyIds
}
},
transaction
})
// user
await database.user.destroy({
where: {
familyId: {
[Sequelize.Op.in]: familiyIds
}
},
transaction
})
// user
await database.user.destroy({
where: {
familyId: {
[Sequelize.Op.in]: familiyIds
}
},
transaction
})
// device
const oldDeviceAuthTokens = (await database.device.findAll({
// device
const oldDeviceAuthTokens = (await database.device.findAll({
where: {
familyId: {
[Sequelize.Op.in]: familiyIds
}
},
attributes: ['deviceAuthToken'],
transaction
})).map((item) => item.deviceAuthToken)
await database.device.destroy({
where: {
familyId: {
[Sequelize.Op.in]: familiyIds
}
},
transaction
})
// olddevice
if (oldDeviceAuthTokens.length > 0) {
const knownOldDeviceAuthTokens = (await database.oldDevice.findAll({
where: {
familyId: {
[Sequelize.Op.in]: familiyIds
deviceAuthToken: {
[Sequelize.Op.in]: oldDeviceAuthTokens
}
},
attributes: ['deviceAuthToken'],
transaction
})).map((item) => item.deviceAuthToken)
await database.device.destroy({
where: {
familyId: {
[Sequelize.Op.in]: familiyIds
}
},
transaction
})
const oldDeviceAuthTokensToAdd = difference(oldDeviceAuthTokens, knownOldDeviceAuthTokens)
// olddevice
if (oldDeviceAuthTokens.length > 0) {
const knownOldDeviceAuthTokens = (await database.oldDevice.findAll({
where: {
deviceAuthToken: {
[Sequelize.Op.in]: oldDeviceAuthTokens
}
},
transaction
})).map((item) => item.deviceAuthToken)
const oldDeviceAuthTokensToAdd = difference(oldDeviceAuthTokens, knownOldDeviceAuthTokens)
if (oldDeviceAuthTokensToAdd.length > 0) {
await database.oldDevice.bulkCreate(
oldDeviceAuthTokensToAdd.map((item) => ({
deviceAuthToken: item
})),
{ transaction }
)
}
if (oldDeviceAuthTokensToAdd.length > 0) {
await database.oldDevice.bulkCreate(
oldDeviceAuthTokensToAdd.map((item) => ({
deviceAuthToken: item
})),
{ transaction }
)
}
}
// family
await database.family.destroy({
where: {
familyId: {
[Sequelize.Op.in]: familiyIds
}
},
transaction
})
// family
await database.family.destroy({
where: {
familyId: {
[Sequelize.Op.in]: familiyIds
}
},
transaction
})
}
+7 -4
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann
* Copyright (C) 2019 - 2023 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -26,9 +26,12 @@ export async function deleteOldFamilies (database: Database) {
if (oldFamilyIds.length > 0) {
const familyIdsToDelete = oldFamilyIds.slice(0, 256) /* limit to 256 families per execution */
await deleteFamilies({
database,
familiyIds: familyIdsToDelete
await database.transaction(async (transaction) => {
await deleteFamilies({
database,
transaction,
familiyIds: familyIdsToDelete
})
})
}
}