mirror of
https://github.com/wassname/talk.git
synced 2026-09-09 11:38:08 +08:00
added deletion graph endpoints
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user