diff --git a/docs/api/parent.md b/docs/api/parent.md index 556157c..e0cd563 100644 --- a/docs/api/parent.md +++ b/docs/api/parent.md @@ -194,3 +194,27 @@ If the ``secondPasswordHash`` is invalid: HTTP status code 401 Unauthorized If the server does not support this request: HTTP status code 404 On success: ``{"token": "some string"}``; you should not make any assumptions about the token string + +## POST /parent/delete-account + +Use this to delete an account. This includes the complete family registration +with users and devices. Due to that, all parents with a linked mail address +have to authenticate this action. + +## request + +see [this JSON schema](../schema/deleteaccountpayload.md) + +## response + +On success: HTTP status code 200 + +On a invalid request body: HTTP status code 400 Bad Request + +On unknown device auth token: HTTP status code 401 Unauthorized + +On missing parent authentication: HTTP status code 401 Unauthorized + +On unrelated parent authentication: HTTP status code 401 Unauthorized + +If a newer endpoint must be used/the client is too old: HTTP status code 410 Gone diff --git a/docs/schema/DeleteAccountPayload.schema.json b/docs/schema/DeleteAccountPayload.schema.json new file mode 100644 index 0000000..3d82f61 --- /dev/null +++ b/docs/schema/DeleteAccountPayload.schema.json @@ -0,0 +1,23 @@ +{ + "type": "object", + "properties": { + "deviceAuthToken": { + "type": "string" + }, + "mailAuthTokens": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "required": [ + "deviceAuthToken", + "mailAuthTokens" + ], + "definitions": {}, + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "DeleteAccountPayload", + "$id": "https://timelimit.io/DeleteAccountPayload" +} \ No newline at end of file diff --git a/docs/schema/README.md b/docs/schema/README.md index 06a9092..6344daa 100644 --- a/docs/schema/README.md +++ b/docs/schema/README.md @@ -14,6 +14,8 @@ * [CreateRegisterDeviceTokenRequest](./createregisterdevicetokenrequest.md) – `https://timelimit.io/CreateRegisterDeviceTokenRequest` +* [DeleteAccountPayload](./deleteaccountpayload.md) – `https://timelimit.io/DeleteAccountPayload` + * [FinishPurchaseByGooglePlayRequest](./finishpurchasebygoogleplayrequest.md) – `https://timelimit.io/FinishPurchaseByGooglePlayRequest` * [IdentityTokenPayload](./identitytokenpayload.md) – `https://timelimit.io/IdentityTokenPayload` @@ -282,6 +284,8 @@ * [Untitled array in ClientPushChangesRequest](./clientpushchangesrequest-properties-actions.md) – `https://timelimit.io/ClientPushChangesRequest#/properties/actions` +* [Untitled array in DeleteAccountPayload](./deleteaccountpayload-properties-mailauthtokens.md) – `https://timelimit.io/DeleteAccountPayload#/properties/mailAuthTokens` + * [Untitled array in SerializedAppLogicAction](./serializedapplogicaction-definitions-serializedaddinstalledappsaction-properties-apps.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAddInstalledAppsAction/properties/apps` * [Untitled array in SerializedAppLogicAction](./serializedapplogicaction-definitions-serializedaddusedtimeactionversion2-properties-i.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAddUsedTimeActionVersion2/properties/i` diff --git a/docs/schema/deleteaccountpayload-definitions.md b/docs/schema/deleteaccountpayload-definitions.md new file mode 100644 index 0000000..a3e9baf --- /dev/null +++ b/docs/schema/deleteaccountpayload-definitions.md @@ -0,0 +1,15 @@ +# Untitled undefined type in DeleteAccountPayload Schema + +```txt +https://timelimit.io/DeleteAccountPayload#/definitions +``` + + + +| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In | +| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :-------------------------------------------------------------------------------------------- | +| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [DeleteAccountPayload.schema.json\*](DeleteAccountPayload.schema.json "open original schema") | + +## definitions Type + +unknown diff --git a/docs/schema/deleteaccountpayload-properties-deviceauthtoken.md b/docs/schema/deleteaccountpayload-properties-deviceauthtoken.md new file mode 100644 index 0000000..a31e059 --- /dev/null +++ b/docs/schema/deleteaccountpayload-properties-deviceauthtoken.md @@ -0,0 +1,15 @@ +# Untitled string in DeleteAccountPayload Schema + +```txt +https://timelimit.io/DeleteAccountPayload#/properties/deviceAuthToken +``` + + + +| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In | +| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :-------------------------------------------------------------------------------------------- | +| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [DeleteAccountPayload.schema.json\*](DeleteAccountPayload.schema.json "open original schema") | + +## deviceAuthToken Type + +`string` diff --git a/docs/schema/deleteaccountpayload-properties-mailauthtokens-items.md b/docs/schema/deleteaccountpayload-properties-mailauthtokens-items.md new file mode 100644 index 0000000..3a0b83b --- /dev/null +++ b/docs/schema/deleteaccountpayload-properties-mailauthtokens-items.md @@ -0,0 +1,15 @@ +# Untitled string in DeleteAccountPayload Schema + +```txt +https://timelimit.io/DeleteAccountPayload#/properties/mailAuthTokens/items +``` + + + +| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In | +| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :-------------------------------------------------------------------------------------------- | +| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [DeleteAccountPayload.schema.json\*](DeleteAccountPayload.schema.json "open original schema") | + +## items Type + +`string` diff --git a/docs/schema/deleteaccountpayload-properties-mailauthtokens.md b/docs/schema/deleteaccountpayload-properties-mailauthtokens.md new file mode 100644 index 0000000..fa419c8 --- /dev/null +++ b/docs/schema/deleteaccountpayload-properties-mailauthtokens.md @@ -0,0 +1,15 @@ +# Untitled array in DeleteAccountPayload Schema + +```txt +https://timelimit.io/DeleteAccountPayload#/properties/mailAuthTokens +``` + + + +| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In | +| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :-------------------------------------------------------------------------------------------- | +| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [DeleteAccountPayload.schema.json\*](DeleteAccountPayload.schema.json "open original schema") | + +## mailAuthTokens Type + +`string[]` diff --git a/docs/schema/deleteaccountpayload.md b/docs/schema/deleteaccountpayload.md new file mode 100644 index 0000000..30e6cda --- /dev/null +++ b/docs/schema/deleteaccountpayload.md @@ -0,0 +1,60 @@ +# DeleteAccountPayload Schema + +```txt +https://timelimit.io/DeleteAccountPayload +``` + + + +| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In | +| :------------------ | :--------- | :------------- | :----------- | :---------------- | :-------------------- | :------------------ | :------------------------------------------------------------------------------------------ | +| Can be instantiated | Yes | Unknown status | No | Forbidden | Forbidden | none | [DeleteAccountPayload.schema.json](DeleteAccountPayload.schema.json "open original schema") | + +## DeleteAccountPayload Type + +`object` ([DeleteAccountPayload](deleteaccountpayload.md)) + +# DeleteAccountPayload Properties + +| Property | Type | Required | Nullable | Defined by | +| :---------------------------------- | :------- | :------- | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| [deviceAuthToken](#deviceauthtoken) | `string` | Required | cannot be null | [DeleteAccountPayload](deleteaccountpayload-properties-deviceauthtoken.md "https://timelimit.io/DeleteAccountPayload#/properties/deviceAuthToken") | +| [mailAuthTokens](#mailauthtokens) | `array` | Required | cannot be null | [DeleteAccountPayload](deleteaccountpayload-properties-mailauthtokens.md "https://timelimit.io/DeleteAccountPayload#/properties/mailAuthTokens") | + +## deviceAuthToken + + + +`deviceAuthToken` + +* is required + +* Type: `string` + +* cannot be null + +* defined in: [DeleteAccountPayload](deleteaccountpayload-properties-deviceauthtoken.md "https://timelimit.io/DeleteAccountPayload#/properties/deviceAuthToken") + +### deviceAuthToken Type + +`string` + +## mailAuthTokens + + + +`mailAuthTokens` + +* is required + +* Type: `string[]` + +* cannot be null + +* defined in: [DeleteAccountPayload](deleteaccountpayload-properties-mailauthtokens.md "https://timelimit.io/DeleteAccountPayload#/properties/mailAuthTokens") + +### mailAuthTokens Type + +`string[]` + +# DeleteAccountPayload Definitions diff --git a/other/mail/account-deleted/html.ejs b/other/mail/account-deleted/html.ejs new file mode 100644 index 0000000..6a25581 --- /dev/null +++ b/other/mail/account-deleted/html.ejs @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+
TimeLimit
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+

Sie haben eine Löschung Ihres Benutzerkontos angefordert. Diese wurde soeben durchgeführt.

+

You requested the deletion of your account. This deletion is finished now.

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

+

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

Sie erhalten diese Nachricht aufgrund Ihrer Anfrage. Falls Sie Fragen haben können Sie einfach auf diese E-Mail antworten.

+

You got this mail due to your request. If you have got any questions, then you can reply to this message.

+

© <%= mailimprint %>

+
+
+
+ +
+
+ +
+ + + diff --git a/other/mail/account-deleted/htmltemplate-src.txt b/other/mail/account-deleted/htmltemplate-src.txt new file mode 100644 index 0000000..18e649b --- /dev/null +++ b/other/mail/account-deleted/htmltemplate-src.txt @@ -0,0 +1,44 @@ + + + + + TimeLimit + + + + + +

+ Sie haben eine Löschung Ihres Benutzerkontos angefordert. Diese wurde soeben durchgeführt. +

+ +

+ You requested the deletion of your account. This deletion is finished now. +

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

+ Sie erhalten diese Nachricht aufgrund Ihrer Anfrage. + Falls Sie Fragen haben können Sie einfach auf diese E-Mail antworten. +

+

+ You got this mail due to your request. + If you have got any questions, then you can reply to this message. +

+

+ © <%= mailimprint %> +

+
+
+
+
+
diff --git a/other/mail/account-deleted/subject.ejs b/other/mail/account-deleted/subject.ejs new file mode 100644 index 0000000..e2df167 --- /dev/null +++ b/other/mail/account-deleted/subject.ejs @@ -0,0 +1 @@ +Konto gelöscht/Account deleted diff --git a/other/mail/account-deleted/text.ejs b/other/mail/account-deleted/text.ejs new file mode 100644 index 0000000..a85ffaf --- /dev/null +++ b/other/mail/account-deleted/text.ejs @@ -0,0 +1,13 @@ +Sie haben eine Löschung Ihres Benutzerkontos angefordert. Diese wurde soeben durchgeführt. + +You requested the deletion of your account. This deletion is finished now. + +---------------------- + +Sie erhalten diese Nachricht aufgrund Ihrer Anfrage. +Falls Sie Fragen haben können Sie einfach auf diese E-Mail antworten. + +You got this mail due to your request. +If you have got any questions, then you can reply to this message. + + <%- mailimprint %> diff --git a/scripts/build-schemas.js b/scripts/build-schemas.js index c62df9e..0cbb58c 100644 --- a/scripts/build-schemas.js +++ b/scripts/build-schemas.js @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 - 2022 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 @@ -43,7 +43,8 @@ const types = [ 'RequestWithAuthToken', 'SendMailLoginCodeRequest', 'SignInByMailCodeRequest', - 'IdentityTokenPayload' + 'IdentityTokenPayload', + 'DeleteAccountPayload', ] const docOnlyTypes = [ diff --git a/src/api/parent.ts b/src/api/parent.ts index cbe0543..e0e7eca 100644 --- a/src/api/parent.ts +++ b/src/api/parent.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 - 2022 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 @@ -21,6 +21,7 @@ import { Router } from 'express' import { BadRequest, Forbidden, Unauthorized } from 'http-errors' import { config } from '../config' import { Database, Transaction } from '../database' +import { deleteAccount } from '../function/cleanup/account-deletion' import { removeDevice } from '../function/device/remove-device' import { createAddDeviceToken } from '../function/parent/create-add-device-token' import { createFamily } from '../function/parent/create-family' @@ -36,7 +37,8 @@ import { isCreateFamilyByMailTokenRequest, isCreateRegisterDeviceTokenRequest, isLinkParentMailAddressRequest, isMailAuthTokenRequestBody, isRecoverParentPasswordRequest, - isRemoveDeviceRequest, isSignIntoFamilyRequest, isRequestIdentityTokenRequest + isRemoveDeviceRequest, isSignIntoFamilyRequest, isRequestIdentityTokenRequest, + isDeleteAccountPayload } from './validator' export const createParentRouter = ({ @@ -356,5 +358,19 @@ export const createParentRouter = ({ } }) + router.post('/delete-account', json(), async (req, res, next) => { + try { + if (!isDeleteAccountPayload(req.body)) { + throw new BadRequest() + } + + await deleteAccount({ database, request: req.body, websocket }) + + res.sendStatus(200) + } catch (ex) { + next(ex) + } + }) + return router } diff --git a/src/api/schema.ts b/src/api/schema.ts index 5e16481..afaa5f1 100644 --- a/src/api/schema.ts +++ b/src/api/schema.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 - 2022 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 @@ -176,5 +176,10 @@ export type IdentityTokenPayload = IdentityTokenCreatePayload & { exp: number } +export interface DeleteAccountPayload { + deviceAuthToken: string + mailAuthTokens: Array +} + export { SerializedParentAction, SerializedChildAction, SerializedAppLogicAction } from '../action/serialization' export { ServerDataStatus } from '../object/serverdatastatus' diff --git a/src/api/validator.ts b/src/api/validator.ts index 12f5a01..98f176c 100644 --- a/src/api/validator.ts +++ b/src/api/validator.ts @@ -1,5 +1,5 @@ // tslint:disable -import { ClientPushChangesRequest, ClientPullChangesRequest, MailAuthTokenRequestBody, CreateFamilyByMailTokenRequest, SignIntoFamilyRequest, RecoverParentPasswordRequest, RegisterChildDeviceRequest, SerializedParentAction, SerializedAppLogicAction, SerializedChildAction, CreateRegisterDeviceTokenRequest, CanDoPurchaseRequest, FinishPurchaseByGooglePlayRequest, LinkParentMailAddressRequest, UpdatePrimaryDeviceRequest, RemoveDeviceRequest, RequestIdentityTokenRequest, RequestWithAuthToken, SendMailLoginCodeRequest, SignInByMailCodeRequest, IdentityTokenPayload } from './schema' +import { ClientPushChangesRequest, ClientPullChangesRequest, MailAuthTokenRequestBody, CreateFamilyByMailTokenRequest, SignIntoFamilyRequest, RecoverParentPasswordRequest, RegisterChildDeviceRequest, SerializedParentAction, SerializedAppLogicAction, SerializedChildAction, CreateRegisterDeviceTokenRequest, CanDoPurchaseRequest, FinishPurchaseByGooglePlayRequest, LinkParentMailAddressRequest, UpdatePrimaryDeviceRequest, RemoveDeviceRequest, RequestIdentityTokenRequest, RequestWithAuthToken, SendMailLoginCodeRequest, SignInByMailCodeRequest, IdentityTokenPayload, DeleteAccountPayload } from './schema' import Ajv from 'ajv' const ajv = new Ajv() @@ -3493,3 +3493,24 @@ export const isIdentityTokenPayload: (value: unknown) => value is IdentityTokenP "definitions": definitions, "$schema": "http://json-schema.org/draft-07/schema#" }) +export const isDeleteAccountPayload: (value: unknown) => value is DeleteAccountPayload = ajv.compile({ + "type": "object", + "properties": { + "deviceAuthToken": { + "type": "string" + }, + "mailAuthTokens": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "required": [ + "deviceAuthToken", + "mailAuthTokens" + ], + "definitions": definitions, + "$schema": "http://json-schema.org/draft-07/schema#" +}) diff --git a/src/function/cleanup/account-deletion.ts b/src/function/cleanup/account-deletion.ts new file mode 100644 index 0000000..cf783f5 --- /dev/null +++ b/src/function/cleanup/account-deletion.ts @@ -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 . + */ + +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() + + userEntries.forEach((item) => { + if (item.mail !== '') registeredMailAddresses.add(item.mail) + }) + + const authenticatedMailAddresses = new Set() + + 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) + }) + }) + }) + }) +} diff --git a/src/function/cleanup/delete-families.ts b/src/function/cleanup/delete-families.ts index b5856a6..cf65f86 100644 --- a/src/function/cleanup/delete-families.ts +++ b/src/function/cleanup/delete-families.ts @@ -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 // 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 }) } diff --git a/src/function/cleanup/delete-old-families.ts b/src/function/cleanup/delete-old-families.ts index 6001b3d..2f6bf6b 100644 --- a/src/function/cleanup/delete-old-families.ts +++ b/src/function/cleanup/delete-old-families.ts @@ -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 + }) }) } } diff --git a/src/util/mail.ts b/src/util/mail.ts index 6d25126..02e7e7a 100644 --- a/src/util/mail.ts +++ b/src/util/mail.ts @@ -193,6 +193,19 @@ export const sendPasswordRecoveryUsedMail = async ({ receiver, locale }: { }) } +const accountDeletedMailSender = createMailTemplateSender('account-deleted') + +export const sendAccountDeletedMail = async ({ receiver }: { + receiver: string +}) => { + await accountDeletedMailSender.sendMail({ + receiver, + params: { + 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.'