mirror of
https://codeberg.org/timelimit/timelimit-server.git
synced 2026-08-31 19:03:45 +02:00
Add account deletion API
This commit is contained in:
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user