added deletion graph endpoints

This commit is contained in:
Wyatt Johnson
2018-04-18 13:13:41 -06:00
parent dcf10643a0
commit 2adec1bc34
10 changed files with 235 additions and 24 deletions
+3 -2
View File
@@ -8,7 +8,7 @@ const {
SET_USER_BAN_STATUS,
SET_USER_SUSPENSION_STATUS,
UPDATE_USER_ROLES,
DELETE_USER,
DELETE_OTHER_USER,
} = require('../../perms/constants');
const setUserUsernameStatus = async (ctx, id, status) => {
@@ -155,6 +155,7 @@ module.exports = ctx => {
setUsername: () => Promise.reject(new ErrNotAuthorized()),
stopIgnoringUser: () => Promise.reject(new ErrNotAuthorized()),
del: () => Promise.reject(new ErrNotAuthorized()),
delSelf: () => Promise.reject(new ErrNotAuthorized()),
},
};
@@ -191,7 +192,7 @@ module.exports = ctx => {
setUserSuspensionStatus(ctx, id, until, message);
}
if (ctx.user.can(DELETE_USER)) {
if (ctx.user.can(DELETE_OTHER_USER)) {
mutators.User.del = id => delUser(ctx, id);
}
}
+1 -1
View File
@@ -18,5 +18,5 @@ module.exports = {
UPDATE_ASSET_SETTINGS: 'UPDATE_ASSET_SETTINGS',
UPDATE_ASSET_STATUS: 'UPDATE_ASSET_STATUS',
UPDATE_SETTINGS: 'UPDATE_SETTINGS',
DELETE_USER: 'DELETE_USER',
DELETE_OTHER_USER: 'DELETE_OTHER_USER',
};
+1
View File
@@ -36,6 +36,7 @@ module.exports = (user, perm) => {
case types.UPDATE_USER_ROLES:
case types.CREATE_TOKEN:
case types.REVOKE_TOKEN:
case types.DELETE_OTHER_USER:
return check(user, ['ADMIN']);
default:
+1 -14
View File
@@ -2,24 +2,11 @@ const { SEARCH_OTHER_USERS } = require('../../../perms/constants');
const { ErrNotFound, ErrAlreadyExists } = require('../../../errors');
const pluralize = require('pluralize');
const sc = require('snake-case');
// const { CREATE_MONGO_INDEXES } = require('../../../config');
function getReactionConfig(reaction) {
// Ensure that the reaction is a lowercase string.
reaction = reaction.toLowerCase();
// if (CREATE_MONGO_INDEXES) {
// // Create the index on the comment model based on the reaction config.
// CommentModel.collection.createIndex(
// {
// created_at: 1,
// [`action_counts.${sc(reaction)}`]: 1,
// },
// {
// background: true,
// }
// );
// }
const reactionPlural = pluralize(reaction);
const Reaction = reaction.charAt(0).toUpperCase() + reaction.slice(1);
const REACTION = reaction.toUpperCase();
@@ -7,6 +7,7 @@
"private": false,
"dependencies": {
"archiver": "^2.1.1",
"cron": "^1.3.0",
"csv-stringify": "^3.0.0"
}
}
@@ -1,7 +1,13 @@
const path = require('path');
const moment = require('moment');
const { CronJob } = require('cron');
module.exports = connectors => {
const { services: { Mailer } } = connectors;
const {
services: { Mailer },
models: { User },
graph: { Context },
} = connectors;
// Setup the mail templates.
['txt', 'html'].forEach(format => {
@@ -11,4 +17,84 @@ module.exports = connectors => {
format
);
});
// Setup the cron job that will scan for accounts to delete every 30 minutes.
new CronJob({
cronTime: '0,30 * * * *',
timeZone: 'America/New_York',
start: true,
runOnInit: true,
onTick: async () => {
// Create the context we'll use to perform user deletions.
const ctx = Context.forSystem();
// rescheduledDeletionDate is the date in the future that we'll set the
// user's account to be deleted on if this delete fails.
const rescheduledDeletionDate = moment()
.add(1, 'hours')
.toDate();
try {
// Keep running for each user we can pull.
while (true) {
// We'll find any user that has an account deletion date before now
// and update the user such that their deletion time is 1 hour from
// now. This will ensure that only one instance can pull the same
// user at a time, and if the delete fails, it will be retried an
// hour from now. If the deletion was successful, well, it can't be
// retried because the reference to the scheduledDeletionDate will
// get deleted along with the user.
const user = await User.findOneAndUpdate(
{
'metadata.scheduledDeletionDate': { $lte: new Date() },
},
{
$set: {
'metadata.scheduledDeletionDate': rescheduledDeletionDate,
},
}
);
if (!user) {
// There are no more users that meet the search criteria! We're
// done!
ctx.log.info('no more users are scheduled for deletion');
break;
}
ctx.log.info(
{
userID: user.id,
scheduledDeletionDate: user.metadata.scheduledDeletionDate,
},
'starting user delete'
);
// Delete the user using the existing graph call.
const { data, errors } = await ctx.graphql(
`
mutation DeleteUser($user_id: ID!) {
delUser(id: $user_id) {
errors {
translation_key
}
}
}
`,
{ user_id: user.id }
);
if (errors) {
throw errors;
}
if (data.errors) {
throw data.errors;
}
ctx.log.info({ userID: user.id }, 'user was deleted successfully');
}
} catch (err) {
ctx.log.error({ err }, 'could not handle user deletions');
}
},
});
};
@@ -15,4 +15,29 @@ class ErrDownloadToken extends TalkError {
}
}
module.exports = { ErrDownloadToken };
// ErrDeletionAlreadyScheduled is returned when a user requests that their
// account get deleted when their account is already scheduled for deletion.
class ErrDeletionAlreadyScheduled extends TalkError {
constructor() {
super('Deletion is already scheduled', {
translation_key: 'DELETION_ALREADY_SCHEDULED',
status: 400,
});
}
}
// ErrDeletionNotScheduled is returned when a user requests that their
// account deletion to be canceled when it was not scheduled for deletion.
class ErrDeletionNotScheduled extends TalkError {
constructor() {
super('Deletion was not scheduled', {
translation_key: 'DELETION_NOT_SCHEDULED',
status: 400,
});
}
}
module.exports = {
ErrDownloadToken,
ErrDeletionAlreadyScheduled,
ErrDeletionNotScheduled,
};
@@ -1,6 +1,12 @@
const { get } = require('lodash');
const moment = require('moment');
const uuid = require('uuid/v4');
const { DOWNLOAD_LINK_SUBJECT } = require('./constants');
const {
ErrDeletionAlreadyScheduled,
ErrDeletionNotScheduled,
} = require('./errors');
const { ErrNotAuthorized } = require('errors');
async function sendDownloadLink({
user,
@@ -66,8 +72,56 @@ async function sendDownloadLink({
);
}
module.exports = ctx => ({
User: {
requestDownloadLink: () => sendDownloadLink(ctx),
},
});
// requestDeletion will schedule the current user to have their account deleted
// by setting the `scheduledDeletionDate` on the user 12 hours from now.
async function requestDeletion({ user, connectors: { models: { User } } }) {
// Ensure the user doesn't already have a deletion scheduled.
if (get(user, 'metadata.scheduledDeletionDate')) {
throw new ErrDeletionAlreadyScheduled();
}
// Get the date in the future 12 hours from now.
const scheduledDeletionDate = moment()
.add(12, 'hours')
.toDate();
// Amend the scheduledDeletionDate on the user.
await User.update(
{ id: user.id },
{ $set: { 'metadata.scheduledDeletionDate': scheduledDeletionDate } }
);
return scheduledDeletionDate;
}
// cancelDeletion will unset the scheduled deletion date on the user account
// that is used to indicate that the user was scheduled for deletion.
async function cancelDeletion({ user, connectors: { models: { User } } }) {
// Ensure the user has a deletion scheduled.
if (!get(user, 'metadata.scheduledDeletionDate', null)) {
throw new ErrDeletionNotScheduled();
}
// Amend the scheduledDeletionDate on the user.
await User.update(
{ id: user.id },
{ $unset: { 'metadata.scheduledDeletionDate': 1 } }
);
}
module.exports = ctx =>
ctx.user
? {
User: {
requestDownloadLink: () => sendDownloadLink(ctx),
requestDeletion: () => requestDeletion(ctx),
cancelDeletion: () => cancelDeletion(ctx),
},
}
: {
User: {
requestDownloadLink: () => Promise.reject(new ErrNotAuthorized()),
requestDeletion: () => Promise.reject(new ErrNotAuthorized()),
cancelDeletion: () => Promise.reject(new ErrNotAuthorized()),
},
};
@@ -5,6 +5,12 @@ module.exports = {
requestDownloadLink: async (_, args, { mutators: { User } }) => {
await User.requestDownloadLink();
},
requestAccountDeletion: async (_, args, { mutators: { User } }) => ({
scheduledDeletionDate: await User.requestDeletion(),
}),
cancelAccountDeletion: async (_, args, { mutators: { User } }) => {
await User.cancelDeletion();
},
},
User: {
lastAccountDownload: (user, args, { user: currentUser }) => {
@@ -16,5 +22,17 @@ module.exports = {
return get(user, 'metadata.lastAccountDownload', null);
},
scheduledDeletionDate: (user, args, { user: currentUser }) => {
// If the current user is not the requesting user, and the user is not
// an admin or a moderator, return nothing.
if (
user.id !== currentUser.id &&
!['ADMIN', 'MODERATOR'].includes(user.role)
) {
return null;
}
return get(user, 'metadata.scheduledDeletionDate', null);
},
},
};
@@ -3,17 +3,55 @@ type User {
# lastAccountDownload is the date that the user last requested a comment
# download.
lastAccountDownload: Date
# scheduledDeletionDate is the data for which the user account will be deleted
# after. The account may be deleted up to half an hour after this date because
# the job responsible for deleting the scheduled account will only run once
# every half hour.
scheduledDeletionDate: Date
}
# RequestDownloadLinkResponse contains the account download errors relating to
# the request for an account download.
type RequestDownloadLinkResponse implements Response {
# An array of errors relating to the mutation that occurred.
errors: [UserError!]
}
# RequestAccountDeletionResponse contains the account deletion schedule errors
# relating to schedulding an account for deletion.
type RequestAccountDeletionResponse implements Response {
# scheduledDeletionDate is the data for which the user account will be deleted
# after. The account may be deleted up to half an hour after this date because
# the job responsible for deleting the scheduled account will only run once
# every half hour.
scheduledDeletionDate: Date
# An array of errors relating to the mutation that occurred.
errors: [UserError!]
}
# CancelAccountDeletionResponse contains the account deletion errors relating to
# canceling an account deletion that was scheduled.
type CancelAccountDeletionResponse implements Response {
# An array of errors relating to the mutation that occurred.
errors: [UserError!]
}
type RootMutation {
# requestDownloadLink will request a download link be sent to the primary
# users email address.
requestDownloadLink: RequestDownloadLinkResponse
# requestAccountDeletion requests that the current account get deleted. The
# mutation will return the date that the account is scheduled to be deleted.
requestAccountDeletion: RequestAccountDeletionResponse
# cancelAccountDeletion will cancel a pending account deletion that was
# previously scheduled.
cancelAccountDeletion: CancelAccountDeletionResponse
}