Skip to content

Commit

Permalink
db actions: delete user: prompt for confirmation when the user has si…
Browse files Browse the repository at this point in the history
…gns of activity

and add a db-actions:get-user-stats script, allowing to easily
access those aggregated stats, without having to pretend to delete the user
  • Loading branch information
maxlath committed Feb 18, 2021
1 parent cf23df5 commit 0762a38
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 25 deletions.
2 changes: 2 additions & 0 deletions docs/instance_administration.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,5 @@ Delete a user account in the same way a user could do, namely by also cleaning u
```sh
npm run db-actions:delete-user <user id>
```

The script will ask for confirmation for any user that has shown signs of activity
11 changes: 9 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
"couch2elastic4sync:load": "./scripts/couch2elastic4sync/load.js",
"couch2elastic4sync:reset": "./scripts/couch2elastic4sync/reset.js",
"create-dumps": "./scripts/dumps/prepare_dumps",
"db-actions:delete-user": "./scripts/db_actions/delete_user.js",
"db-actions:get-user-stats": "./scripts/db_actions/get_user_stats.js",
"db-actions:increment-user-undelivered-emails-count": "./scripts/db_actions/increment_user_undelivered_emails_count.js",
"db-actions:update-user-role": "./scripts/db_actions/update_user_role.js",
"db-actions:stop-emails-to-address": "./scripts/db_actions/stop_emails_to_address.js",
"db-actions:delete-user": "./scripts/db_actions/delete_user.js",
"db-actions:update-user-role": "./scripts/db_actions/update_user_role.js",
"debug": "DEBUG=express:* node server/server.js",
"delete-api-test-databases": "./tests/api/scripts/delete_databases",
"indexation:load": "./scripts/indexation/load.js",
Expand Down Expand Up @@ -81,6 +82,7 @@
"passport": "^0.2.2",
"passport-http": "^0.3.0",
"passport-local": "^1.0.0",
"read": "^1.0.7",
"rss": "^1.2.2",
"serve-favicon": "^2.4.5",
"split": "^1.0.1",
Expand Down
42 changes: 40 additions & 2 deletions scripts/db_actions/delete_user.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,43 @@
#!/usr/bin/env node
const __ = require('config').universalPath
const CONFIG = require('config')
const __ = CONFIG.universalPath
const _ = __.require('builders', 'utils')
const deleteUserAndCleanup = __.require('controllers', 'user/lib/delete_user_and_cleanup')
const getUserStats = __.require('controllers', 'user/lib/get_user_stats')
const actionByUserId = require('./lib/action_by_user_id')
actionByUserId(deleteUserAndCleanup)
const { prompt } = __.require('scripts', 'scripts_utils')
const { red, yellow } = require('chalk')

actionByUserId(async userId => {
const stats = await getUserStats(userId)
_.log(stats, `user ${userId} stats`)
if (stats.total > 0) {
const confirmed = await promptForConfirmation(stats)
if (confirmed) {
_.warn('deletion confirmed')
return deleteUserAndCleanup(userId)
} else {
_.warn('aborted delete')
}
} else {
return deleteUserAndCleanup(userId)
}
})

const promptForConfirmation = async stats => {
const { username, days, total } = stats
const increaseDifficulty = days > 180 || total > 10
const expectedAnswer = increaseDifficulty ? 'Enter username to confirm:' : 'Y/n'
const res = await prompt(`Are you sure you want to delete the account of ${yellow(username.toLowerCase())} who has been a user for ${yellow(days)} days, with ${yellow(total)} activity signs?\n${expectedAnswer}\n`)
if (increaseDifficulty) {
if (res.trim().toLowerCase() !== username.toLowerCase()) {
console.log(red('username does not match'))
return false
} else {
return true
}
} else {
if (res.trim().toLowerCase() === 'y') return true
else return false
}
}
5 changes: 5 additions & 0 deletions scripts/db_actions/get_user_stats.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node
const __ = require('config').universalPath
const getUserStats = __.require('controllers', 'user/lib/get_user_stats')
const actionByUserId = require('./lib/action_by_user_id')
actionByUserId(getUserStats)
5 changes: 4 additions & 1 deletion scripts/scripts_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const _ = __.require('builders', 'utils')
const { red } = require('chalk')
const { promisify } = require('util')
const exec = promisify(require('child_process').exec)
const read = promisify(require('read'))

module.exports = {
logErrorAndExit: (label, err) => {
Expand All @@ -25,7 +26,9 @@ module.exports = {
stdout: stdout.trim(),
stderr: stderr.trim()
}
}
},

prompt: async message => read({ prompt: message })
}

const makeSureLogsAreWrittenBeforeExit = () => {
Expand Down
32 changes: 17 additions & 15 deletions server/controllers/entities/lib/patches.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@ const assert_ = __.require('utils', 'assert_types')
const { maxKey } = __.require('lib', 'couch')
const { oneDay } = __.require('lib', 'time')

const getUserTotalContributions = userId => {
return db.view(designDocName, 'byUserId', {
group_level: 1,
// Maybe there is a way to only pass the userId key
// but I couln't find it
startkey: [ userId ],
endkey: [ userId, maxKey ]
})
// Testing the row existance in case we got an invalid user id
.then(res => {
const userRow = res.rows[0]
return userRow ? userRow.value : 0
})
}

module.exports = {
db,
byId: db.get,
Expand All @@ -32,6 +47,8 @@ module.exports = {
return formatPatchesPage({ viewRes, total, limit, offset })
},

getUserTotalContributions,

byDate: async (limit, offset) => {
const viewRes = await db.view(designDocName, 'byDate', {
limit,
Expand Down Expand Up @@ -119,21 +136,6 @@ const sortAndFilterContributions = rows => {
// see server/db/couch/hard_coded_documents.js
const noSpecialUser = row => !row.user.startsWith('000000000000000000000000000000')

const getUserTotalContributions = userId => {
return db.view(designDocName, 'byUserId', {
group_level: 1,
// Maybe there is a way to only pass the userId key
// but I couln't find it
startkey: [ userId ],
endkey: [ userId, maxKey ]
})
// Testing the row existance in case we got an invalid user id
.then(res => {
const userRow = res.rows[0]
return userRow ? userRow.value : 0
})
}

const formatPatchesPage = ({ viewRes, total, limit, offset }) => {
if (total == null) total = viewRes.total_rows
const data = {
Expand Down
6 changes: 3 additions & 3 deletions server/controllers/relations/lib/lists.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ const lists = module.exports = {
.then(db.bulkDelete)
},

getUserFriendsAndCoGroupsMembers: userId => {
return Promise.all([
getUserFriendsAndCoGroupsMembers: async userId => {
const [ friends, coMembers ] = await Promise.all([
lists.getUserFriends(userId),
groups_.findUserGroupsCoMembers(userId)
])
.then(([ friends, coMembers ]) => _.uniq(friends.concat(coMembers)))
return _.uniq(friends.concat(coMembers))
}
}
70 changes: 70 additions & 0 deletions server/controllers/user/lib/get_user_stats.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
const __ = require('config').universalPath
const _ = __.require('builders', 'utils')
const user_ = __.require('controllers', 'user/lib/user')
const { getUserRelations } = __.require('controllers', 'user/lib/relations_status')
const shelves_ = __.require('controllers', 'shelves/lib/shelves')
const groups_ = __.require('controllers', 'groups/lib/groups')
const transactions_ = __.require('controllers', 'transactions/lib/transactions')
const { getUserTotalContributions } = __.require('controllers', 'entities/lib/patches')
const { oneDay } = __.require('lib', 'time')

module.exports = async userId => {
const [
user,
relations,
shelves,
transactions,
groups,
groupsInvitations,
contributions,
] = await Promise.all([
user_.byId(userId),
getUserRelations(userId),
shelves_.byOwners([ userId ]),
transactions_.byUser(userId),
groups_.byUser(userId),
groups_.byInvitedUser(userId),
getUserTotalContributions(userId),
])

const { _id, username, bio, created, roles, snapshot } = user

relations.friends = relations.friends.length
relations.userRequested = relations.userRequested.length
relations.otherRequested = relations.otherRequested.length
relations.none = relations.none.length

const itemsCount = _.sum(_.map(Object.values(snapshot), 'items:count'))

const total = _.sum([
itemsCount,
shelves.length,
transactions.length,
relations.friends,
relations.userRequested,
relations.otherRequested,
relations.none,
groups.length,
groupsInvitations.length,
contributions,
])

return {
_id,
username,
bio,
created: new Date(created).toISOString(),
days: Math.round((Date.now() - created) / oneDay),
roles,
total,
items: itemsCount,
shelves: shelves.length,
transactions: transactions.length,
relations,
groups: {
member: groups.length,
invited: groupsInvitations.length,
},
contributions
}
}

0 comments on commit 0762a38

Please sign in to comment.