Move user deletion to worker (#15586)
* WIP(delete): remove business logic from controller
* fix(deletion): handle group leave logic on app server still
* fix(lint): unused import
* fix(import): bracket syntax
* fix(test): adapt test for worker flow
* fix(deletion): update delete/feedback form copy
* fix(text): don't break to new paragraph about Gems
* fix(deletion): remove orphaned chat messages
* Revert "fix(deletion): handle group leave logic on app server still"
This reverts commit 9db541f4c3.
* fix(tests): remove tests
These can potentially be tested in the worker's suite? They target functionality that the group leave route handles within the deletion flow
* fix(lint): no-undef
* refactor redis setup into own file and use ioredis
* use bullmq directly to schedule jobs
* add space
* add key prefix
* add semicolon
* fix(jobs): update redis package
---------
Co-authored-by: Phillip Thelen <phillip@habitica.com>
This commit is contained in:
@@ -9,10 +9,6 @@ import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
} from '../../libs/errors';
|
||||
import {
|
||||
basicFields as basicGroupFields,
|
||||
model as Group,
|
||||
} from '../../models/group';
|
||||
import * as Tasks from '../../models/task';
|
||||
import * as passwordUtils from '../../libs/password';
|
||||
import {
|
||||
@@ -22,6 +18,7 @@ import {
|
||||
getUserInfo,
|
||||
sendTxn,
|
||||
} from '../../libs/email';
|
||||
import worker from '../../libs/worker';
|
||||
import * as inboxLib from '../../libs/inbox';
|
||||
import * as userLib from '../../libs/user';
|
||||
import { model as UserHistory } from '../../models/userHistory';
|
||||
@@ -297,21 +294,6 @@ api.deleteUser = {
|
||||
throw new NotAuthorized(res.t('cannotDeleteActiveAccount'));
|
||||
}
|
||||
|
||||
const types = ['party', 'guilds'];
|
||||
const groupFields = basicGroupFields.concat(' leader memberCount purchased');
|
||||
|
||||
const groupsUserIsMemberOf = await Group.getGroups({ user, types, groupFields });
|
||||
|
||||
const groupLeavePromises = groupsUserIsMemberOf.map(group => group.leave(user, 'remove-all'));
|
||||
|
||||
await Promise.all(groupLeavePromises);
|
||||
|
||||
await Tasks.Task.deleteMany({
|
||||
userId: user._id,
|
||||
}).exec();
|
||||
|
||||
await user.deleteOne();
|
||||
|
||||
if (feedback) {
|
||||
sendTxn({ email: TECH_ASSISTANCE_EMAIL }, 'admin-feedback', [
|
||||
{ name: 'PROFILE_NAME', content: user.profile.name },
|
||||
@@ -323,6 +305,15 @@ api.deleteUser = {
|
||||
]);
|
||||
}
|
||||
|
||||
worker.sendJob('deleteUser', {
|
||||
identifier: user._id,
|
||||
data: {
|
||||
userId: user._id,
|
||||
deleteAccount: true,
|
||||
deleteAmplitude: true,
|
||||
},
|
||||
});
|
||||
|
||||
res.respond(200, {});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { sendJob } from '../../libs/worker';
|
||||
import worker from '../../libs/worker';
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
import { ensurePermission } from '../../middlewares/ensureAccessRight';
|
||||
import { TransactionModel as Transaction } from '../../models/transaction';
|
||||
@@ -48,7 +48,8 @@ api.deleteMember = {
|
||||
req.checkQuery('deleteAmplitude').optional().isIn(['true', 'false']);
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
sendJob('delete-user', {
|
||||
await worker.sendJob('deleteUser', {
|
||||
identifier: req.params.memberId,
|
||||
data: {
|
||||
userId: req.params.memberId,
|
||||
deleteAccount: req.query.deleteAccount === 'true',
|
||||
|
||||
@@ -24,7 +24,10 @@ api.getReady = {
|
||||
middlewares: [disableCache],
|
||||
async handler (req, res) {
|
||||
// This allows kubernetes to determine if the server is ready to receive traffic
|
||||
if (!SERVER_STATUS.MONGODB || !SERVER_STATUS.REDIS || !SERVER_STATUS.EXPRESS) {
|
||||
if (!SERVER_STATUS.MONGODB
|
||||
|| !SERVER_STATUS.RATE_LIMITER
|
||||
|| !SERVER_STATUS.WORKER
|
||||
|| !SERVER_STATUS.EXPRESS) {
|
||||
res.respond(503, {
|
||||
status: 'not ready',
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import nconf from 'nconf';
|
||||
import { TAVERN_ID } from '../models/group'; // eslint-disable-line import/no-cycle
|
||||
import { encrypt } from './encryption';
|
||||
import common from '../../common';
|
||||
import { sendJob } from './worker';
|
||||
import worker from './worker';
|
||||
|
||||
const IS_PROD = nconf.get('IS_PROD');
|
||||
const BASE_URL = nconf.get('BASE_URL');
|
||||
@@ -148,7 +148,8 @@ export async function sendTxn (mailingInfoArray, emailType, variables, personalV
|
||||
}
|
||||
|
||||
if (IS_PROD && mailingInfoArray.length > 0) {
|
||||
return sendJob('email', {
|
||||
return worker.sendJob('email', {
|
||||
identifier: emailType,
|
||||
data: {
|
||||
emailType,
|
||||
to: mailingInfoArray,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import IORedis from 'ioredis';
|
||||
|
||||
export default function setupRedis (connectionOptions, config) {
|
||||
const redisConfig = { ...config };
|
||||
if (connectionOptions.username) {
|
||||
redisConfig.username = connectionOptions.username;
|
||||
}
|
||||
if (connectionOptions.password) {
|
||||
redisConfig.password = connectionOptions.password;
|
||||
}
|
||||
if (connectionOptions.db) {
|
||||
redisConfig.db = connectionOptions.db;
|
||||
}
|
||||
let connection;
|
||||
const redisUrl = connectionOptions.url;
|
||||
if (redisUrl) {
|
||||
connection = new IORedis(redisUrl, redisConfig);
|
||||
} else {
|
||||
connection = new IORedis(connectionOptions.port, connectionOptions.host, redisConfig);
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
const SERVER_STATUS = {
|
||||
MONGODB: false,
|
||||
REDIS: false,
|
||||
RATE_LIMITER: false,
|
||||
WORKER: false,
|
||||
EXPRESS: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,33 +1,49 @@
|
||||
import got from 'got';
|
||||
import nconf from 'nconf';
|
||||
import logger from './logger';
|
||||
import { Queue } from 'bullmq';
|
||||
import setupRedis from './redis';
|
||||
import SERVER_STATUS from './serverStatus';
|
||||
|
||||
const EMAIL_SERVER = {
|
||||
url: nconf.get('EMAIL_SERVER_URL'),
|
||||
auth: {
|
||||
user: nconf.get('EMAIL_SERVER_AUTH_USER'),
|
||||
password: nconf.get('EMAIL_SERVER_AUTH_PASSWORD'),
|
||||
},
|
||||
};
|
||||
let redisClient;
|
||||
const queues = {};
|
||||
|
||||
export function sendJob (type, config) {
|
||||
const { data, options } = config;
|
||||
const usedOptions = {
|
||||
backoff: { delay: 10 * 60 * 1000, type: 'exponential' },
|
||||
...options,
|
||||
if (nconf.get('WORKER_REDIS_URL')) {
|
||||
redisClient = setupRedis({
|
||||
url: nconf.get('WORKER_REDIS_URL'),
|
||||
username: nconf.get('WORKER_REDIS_USERNAME'),
|
||||
password: nconf.get('WORKER_REDIS_PASSWORD'),
|
||||
});
|
||||
|
||||
redisClient.on('ready', () => {
|
||||
SERVER_STATUS.WORKER = true;
|
||||
});
|
||||
|
||||
redisClient.on('reconnecting', () => {
|
||||
SERVER_STATUS.WORKER = false;
|
||||
});
|
||||
|
||||
const queueConfig = {
|
||||
connection: redisClient,
|
||||
};
|
||||
if (nconf.get('WORKER_REDIS_KEY_PREFIX')) {
|
||||
queueConfig.prefix = nconf.get('WORKER_REDIS_KEY_PREFIX');
|
||||
}
|
||||
|
||||
return got.post(`${EMAIL_SERVER.url}/job`, {
|
||||
retry: 5, // retry the http request to the email server 5 times
|
||||
timeout: 60000, // wait up to 60s before timing out
|
||||
username: EMAIL_SERVER.auth.user,
|
||||
password: EMAIL_SERVER.auth.password,
|
||||
json: {
|
||||
type,
|
||||
data,
|
||||
options: usedOptions,
|
||||
},
|
||||
}).json().catch(err => logger.error(err, {
|
||||
extraMessage: 'Error while sending an email.',
|
||||
}));
|
||||
queues.email = new Queue('emails', queueConfig);
|
||||
queues.deleteUser = new Queue('DeleteUsers', queueConfig);
|
||||
} else {
|
||||
SERVER_STATUS.WORKER = true;
|
||||
}
|
||||
|
||||
function sendJob (type, config) {
|
||||
if (!queues[type]) {
|
||||
return Promise.reject(new Error(`Queue ${type} does not exist`));
|
||||
}
|
||||
const { identifier, data } = config;
|
||||
return queues[type].add(identifier, data);
|
||||
}
|
||||
|
||||
export function getRedisClient () {
|
||||
return redisClient;
|
||||
}
|
||||
|
||||
export default { sendJob };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import nconf from 'nconf';
|
||||
import redis from 'redis';
|
||||
import redis from 'ioredis';
|
||||
import {
|
||||
RateLimiterRedis,
|
||||
RateLimiterMemory,
|
||||
|
||||
@@ -1457,6 +1457,7 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all', keepC
|
||||
|
||||
if (members.length === 0) {
|
||||
promises.push(group.deleteOne());
|
||||
promises.push(Chat.deleteMany({ groupId: group._id }));
|
||||
return Promise.all(promises);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import nconf from 'nconf';
|
||||
import express from 'express';
|
||||
import http from 'http';
|
||||
import mongoose from 'mongoose';
|
||||
import redis from 'redis';
|
||||
import redis from 'ioredis';
|
||||
import logger from './libs/logger';
|
||||
|
||||
// Setup translations
|
||||
|
||||
Reference in New Issue
Block a user