From f8c25ae508102aced8d12c992181f1e25a1762ba Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Tue, 3 Sep 2024 15:50:44 +0300 Subject: [PATCH 1/4] 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', From 560407b9728a00f5d90936c17760394ded2866a8 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Tue, 3 Sep 2024 15:51:55 +0300 Subject: [PATCH 2/4] chore: Comment delete current user mutations as converted --- packages/backend/src/graphql/mutation-resolvers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/graphql/mutation-resolvers.js b/packages/backend/src/graphql/mutation-resolvers.js index 38bb9f07..ada214e7 100644 --- a/packages/backend/src/graphql/mutation-resolvers.js +++ b/packages/backend/src/graphql/mutation-resolvers.js @@ -2,7 +2,6 @@ import createConnection from './mutations/create-connection.js'; import createRole from './mutations/create-role.ee.js'; import createStep from './mutations/create-step.js'; import createUser from './mutations/create-user.ee.js'; -import deleteCurrentUser from './mutations/delete-current-user.ee.js'; import deleteFlow from './mutations/delete-flow.js'; import deleteRole from './mutations/delete-role.ee.js'; import duplicateFlow from './mutations/duplicate-flow.js'; @@ -22,6 +21,7 @@ import updateUser from './mutations/update-user.ee.js'; import deleteStep from './mutations/delete-step.js'; import verifyConnection from './mutations/verify-connection.js'; import createFlow from './mutations/create-flow.js'; +import deleteCurrentUser from './mutations/delete-current-user.ee.js'; const mutationResolvers = { createConnection, From 0df5e5283ec6cbf8f3875f1197fb4d68a901f5ee Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Tue, 3 Sep 2024 16:00:24 +0300 Subject: [PATCH 3/4] refactor: Remove user associations before removing user --- packages/backend/src/models/user.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/models/user.js b/packages/backend/src/models/user.js index a906cdb2..9402475d 100644 --- a/packages/backend/src/models/user.js +++ b/packages/backend/src/models/user.js @@ -250,6 +250,7 @@ class User extends Base { } async softRemove() { + await this.softRemoveAssociations(); await this.$query().delete(); const jobName = `Delete user - ${this.id}`; @@ -260,7 +261,6 @@ class User extends Base { }; await deleteUserQueue.add(jobName, jobPayload, jobOptions); - await this.softRemoveAssociations(); } async softRemoveAssociations() { From 33a2386d74d79dc919a7786971087f37db3f7209 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Tue, 3 Sep 2024 16:03:48 +0300 Subject: [PATCH 4/4] feat: Remove associated access tokens while removing user --- packages/backend/src/models/user.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/backend/src/models/user.js b/packages/backend/src/models/user.js index 9402475d..3b92c99b 100644 --- a/packages/backend/src/models/user.js +++ b/packages/backend/src/models/user.js @@ -283,6 +283,7 @@ class User extends Base { ).map((execution) => execution.id); const flowIds = flows.map((flow) => flow.id); + await this.$relatedQuery('accessTokens').delete(); await ExecutionStep.query().delete().whereIn('execution_id', executionIds); await this.$relatedQuery('executions').delete(); await this.$relatedQuery('steps').delete();