diff --git a/packages/backend/src/graphql/mutation-resolvers.ts b/packages/backend/src/graphql/mutation-resolvers.ts index 5d8b8615..c19411c1 100644 --- a/packages/backend/src/graphql/mutation-resolvers.ts +++ b/packages/backend/src/graphql/mutation-resolvers.ts @@ -13,6 +13,7 @@ import createStep from './mutations/create-step'; import updateStep from './mutations/update-step'; import deleteStep from './mutations/delete-step'; import createUser from './mutations/create-user.ee'; +import deleteUser from './mutations/delete-user.ee'; import updateUser from './mutations/update-user'; import forgotPassword from './mutations/forgot-password.ee'; import resetPassword from './mutations/reset-password.ee'; @@ -34,6 +35,7 @@ const mutationResolvers = { updateStep, deleteStep, createUser, + deleteUser, updateUser, forgotPassword, resetPassword, diff --git a/packages/backend/src/graphql/mutations/delete-user.ee.ts b/packages/backend/src/graphql/mutations/delete-user.ee.ts new file mode 100644 index 00000000..b1f83097 --- /dev/null +++ b/packages/backend/src/graphql/mutations/delete-user.ee.ts @@ -0,0 +1,31 @@ +import User from '../../models/user'; +import deleteUserQueue from '../../queues/delete-user.ee'; +import { Duration } from 'luxon'; + +type Params = { + input: { + id: string; + }; +}; + +const deleteUser = async (_parent: unknown, params: Params) => { + const { id } = params.input; + await User + .query() + .findById(id) + .delete() + .throwIfNotFound(); + + const jobName = `Delete user - ${id}`; + const jobPayload = { id }; + const millisecondsFor30Days = Duration.fromObject({ days: 30 }).toMillis(); + const jobOptions = { + delay: millisecondsFor30Days + }; + + await deleteUserQueue.add(jobName, jobPayload, jobOptions); + + return true; +}; + +export default deleteUser; diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 1e09a90e..d40af41f 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -54,6 +54,7 @@ type Mutation { updateStep(input: UpdateStepInput): Step deleteStep(input: DeleteStepInput): Step createUser(input: CreateUserInput): User + deleteUser(input: DeleteUserInput): Boolean updateUser(input: UpdateUserInput): User forgotPassword(input: ForgotPasswordInput): Boolean resetPassword(input: ResetPasswordInput): Boolean @@ -339,6 +340,10 @@ input CreateUserInput { password: String! } +input DeleteUserInput { + id: String +} + input UpdateUserInput { email: String password: String diff --git a/packages/backend/src/helpers/create-bull-board-handler.ts b/packages/backend/src/helpers/create-bull-board-handler.ts index fde42c85..28a39330 100644 --- a/packages/backend/src/helpers/create-bull-board-handler.ts +++ b/packages/backend/src/helpers/create-bull-board-handler.ts @@ -4,6 +4,8 @@ import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'; import flowQueue from '../queues/flow'; import triggerQueue from '../queues/trigger'; import actionQueue from '../queues/action'; +import emailQueue from '../queues/email'; +import deleteUserQueue from '../queues/delete-user.ee'; import appConfig from '../config/app'; const serverAdapter = new ExpressAdapter(); @@ -21,6 +23,8 @@ const createBullBoardHandler = async (serverAdapter: ExpressAdapter) => { new BullMQAdapter(flowQueue), new BullMQAdapter(triggerQueue), new BullMQAdapter(actionQueue), + new BullMQAdapter(emailQueue), + new BullMQAdapter(deleteUserQueue), ], serverAdapter: serverAdapter, }); diff --git a/packages/backend/src/queues/delete-user.ee.ts b/packages/backend/src/queues/delete-user.ee.ts new file mode 100644 index 00000000..d67a59b3 --- /dev/null +++ b/packages/backend/src/queues/delete-user.ee.ts @@ -0,0 +1,25 @@ +import process from 'process'; +import { Queue } from 'bullmq'; +import redisConfig from '../config/redis'; +import logger from '../helpers/logger'; + +const CONNECTION_REFUSED = 'ECONNREFUSED'; + +const redisConnection = { + connection: redisConfig, +}; + +const deleteUserQueue = new Queue('delete-user', redisConnection); + +process.on('SIGTERM', async () => { + await deleteUserQueue.close(); +}); + +deleteUserQueue.on('error', (err) => { + if ((err as any).code === CONNECTION_REFUSED) { + logger.error('Make sure you have installed Redis and it is running.', err); + process.exit(); + } +}); + +export default deleteUserQueue; diff --git a/packages/backend/src/worker.ts b/packages/backend/src/worker.ts index 8a5cb447..9e0d5646 100644 --- a/packages/backend/src/worker.ts +++ b/packages/backend/src/worker.ts @@ -4,6 +4,7 @@ import './workers/flow'; import './workers/trigger'; import './workers/action'; import './workers/email'; +import './workers/delete-user.ee'; import telemetry from './helpers/telemetry'; telemetry.setServiceType('worker'); diff --git a/packages/backend/src/workers/delete-user.ee.ts b/packages/backend/src/workers/delete-user.ee.ts new file mode 100644 index 00000000..887149a2 --- /dev/null +++ b/packages/backend/src/workers/delete-user.ee.ts @@ -0,0 +1,44 @@ +import { Worker } from 'bullmq'; +import redisConfig from '../config/redis'; +import logger from '../helpers/logger'; +import User from '../models/user'; +import Execution from '../models/execution'; +import ExecutionStep from '../models/execution-step'; + +export const worker = new Worker( + 'delete-user', + async (job) => { + const { id } = job.data; + + const user = await User.query().findById(id).throwIfNotFound(); + + const executionIds = ( + await user.$relatedQuery('executions').select('executions.id') + ).map((execution: Execution) => execution.id); + + await ExecutionStep.query().hardDelete().whereIn('execution_id', executionIds); + await user.$relatedQuery('executions').hardDelete(); + await user.$relatedQuery('steps').hardDelete(); + await user.$relatedQuery('flows').hardDelete(); + await user.$relatedQuery('connections').hardDelete(); + + await user.$query().hardDelete(); + }, + { connection: redisConfig } +); + +worker.on('completed', (job) => { + logger.info( + `JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has been deleted!` + ); +}); + +worker.on('failed', (job, err) => { + logger.info( + `JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has failed to be deleted! ${err.message}` + ); +}); + +process.on('SIGTERM', async () => { + await worker.close(); +});