From f8c25ae508102aced8d12c992181f1e25a1762ba Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Tue, 3 Sep 2024 15:50:44 +0300 Subject: [PATCH] feat: Implement rest API endpoint to remove current user --- .../api/v1/users/delete-current-user.js | 5 +++ .../api/v1/users/delete-current-user.test.js | 21 +++++++++++ packages/backend/src/models/user.js | 36 +++++++++++++++++++ packages/backend/src/routes/api/v1/users.js | 2 ++ 4 files changed, 64 insertions(+) create mode 100644 packages/backend/src/controllers/api/v1/users/delete-current-user.js create mode 100644 packages/backend/src/controllers/api/v1/users/delete-current-user.test.js diff --git a/packages/backend/src/controllers/api/v1/users/delete-current-user.js b/packages/backend/src/controllers/api/v1/users/delete-current-user.js new file mode 100644 index 00000000..9bf51419 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/users/delete-current-user.js @@ -0,0 +1,5 @@ +export default async (request, response) => { + await request.currentUser.softRemove(); + + response.status(204).end(); +}; diff --git a/packages/backend/src/controllers/api/v1/users/delete-current-user.test.js b/packages/backend/src/controllers/api/v1/users/delete-current-user.test.js new file mode 100644 index 00000000..45b6a1e3 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/users/delete-current-user.test.js @@ -0,0 +1,21 @@ +import { describe, it, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js'; +import { createUser } from '../../../../../test/factories/user.js'; + +describe('DELETE /api/v1/users/:userId', () => { + let currentUser, token; + + beforeEach(async () => { + currentUser = await createUser(); + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should remove user and return 204 no content', async () => { + await request(app) + .delete(`/api/v1/users/${currentUser.id}`) + .set('Authorization', token) + .expect(204); + }); +}); diff --git a/packages/backend/src/models/user.js b/packages/backend/src/models/user.js index 483cf403..a906cdb2 100644 --- a/packages/backend/src/models/user.js +++ b/packages/backend/src/models/user.js @@ -12,6 +12,7 @@ import AccessToken from './access-token.js'; import Connection from './connection.js'; import Config from './config.js'; import Execution from './execution.js'; +import ExecutionStep from './execution-step.js'; import Flow from './flow.js'; import Identity from './identity.ee.js'; import Permission from './permission.js'; @@ -23,6 +24,7 @@ import Billing from '../helpers/billing/index.ee.js'; import NotAuthorizedError from '../errors/not-authorized.js'; import deleteUserQueue from '../queues/delete-user.ee.js'; +import flowQueue from '../queues/flow.js'; import emailQueue from '../queues/email.js'; import { REMOVE_AFTER_30_DAYS_OR_150_JOBS, @@ -258,6 +260,40 @@ class User extends Base { }; await deleteUserQueue.add(jobName, jobPayload, jobOptions); + await this.softRemoveAssociations(); + } + + async softRemoveAssociations() { + const flows = await this.$relatedQuery('flows').where({ + active: true, + }); + + const repeatableJobs = await flowQueue.getRepeatableJobs(); + + for (const flow of flows) { + const job = repeatableJobs.find((job) => job.id === flow.id); + + if (job) { + await flowQueue.removeRepeatableByKey(job.key); + } + } + + const executionIds = ( + await this.$relatedQuery('executions').select('executions.id') + ).map((execution) => execution.id); + const flowIds = flows.map((flow) => flow.id); + + await ExecutionStep.query().delete().whereIn('execution_id', executionIds); + await this.$relatedQuery('executions').delete(); + await this.$relatedQuery('steps').delete(); + await Flow.query().whereIn('id', flowIds).delete(); + await this.$relatedQuery('connections').delete(); + await this.$relatedQuery('identities').delete(); + + if (appConfig.isCloud) { + await this.$relatedQuery('subscriptions').delete(); + await this.$relatedQuery('usageData').delete(); + } } async sendResetPasswordEmail() { diff --git a/packages/backend/src/routes/api/v1/users.js b/packages/backend/src/routes/api/v1/users.js index 153b0e59..2f42aeb5 100644 --- a/packages/backend/src/routes/api/v1/users.js +++ b/packages/backend/src/routes/api/v1/users.js @@ -4,6 +4,7 @@ import { authorizeUser } from '../../../helpers/authorization.js'; import checkIsCloud from '../../../helpers/check-is-cloud.js'; import getCurrentUserAction from '../../../controllers/api/v1/users/get-current-user.js'; import updateCurrentUserAction from '../../../controllers/api/v1/users/update-current-user.js'; +import deleteCurrentUserAction from '../../../controllers/api/v1/users/delete-current-user.js'; import getUserTrialAction from '../../../controllers/api/v1/users/get-user-trial.ee.js'; import getAppsAction from '../../../controllers/api/v1/users/get-apps.js'; import getInvoicesAction from '../../../controllers/api/v1/users/get-invoices.ee.js'; @@ -19,6 +20,7 @@ router.get('/me', authenticateUser, getCurrentUserAction); router.patch('/:userId', authenticateUser, updateCurrentUserAction); router.get('/:userId/apps', authenticateUser, authorizeUser, getAppsAction); router.get('/invoices', authenticateUser, checkIsCloud, getInvoicesAction); +router.delete('/:userId', authenticateUser, deleteCurrentUserAction); router.get( '/:userId/trial',