From 39ea30664d927c18be92cf34dbbaac2dfb7458db Mon Sep 17 00:00:00 2001 From: undefined Date: Thu, 25 Jun 2020 19:03:26 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E9=99=A4=E7=94=A8=E6=88=B7=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hydro/entry/common.js | 2 +- hydro/handler/discussion.js | 7 +- hydro/handler/problem.js | 22 ++++-- hydro/handler/system.js | 13 ++- hydro/handler/user.js | 22 +++++- hydro/model/problem.js | 2 +- hydro/model/setting.js | 1 - hydro/model/task.js | 8 ++ hydro/script/deleteUser.js | 88 +++++++++++++++++++++ hydro/service/server.js | 6 -- templates/user_delete_pending.html | 10 +++ ui/components/navigation/navigation.page.js | 1 - 12 files changed, 159 insertions(+), 23 deletions(-) create mode 100644 hydro/script/deleteUser.js create mode 100644 templates/user_delete_pending.html diff --git a/hydro/entry/common.js b/hydro/entry/common.js index 7a15e6fde..1095747fd 100644 --- a/hydro/entry/common.js +++ b/hydro/entry/common.js @@ -26,7 +26,7 @@ const builtinHandler = [ const builtinScript = [ 'install', 'uninstall', 'rating', 'recalcRating', 'register', - 'blacklist', 'setSuperadmin', + 'blacklist', 'setSuperadmin', 'deleteUser', ]; async function handler(pending, fail) { diff --git a/hydro/handler/discussion.js b/hydro/handler/discussion.js index 0626a08b8..34ba0bd54 100644 --- a/hydro/handler/discussion.js +++ b/hydro/handler/discussion.js @@ -72,10 +72,13 @@ class DiscussionMainHandler extends Handler { ['Hydro', 'homepage'], ['discussion_main', null], ]; - const vndict = await discussion.getListVnodes(domainId, ddocs, this); + const [vndict, vnodes] = await Promise.all([ + discussion.getListVnodes(domainId, ddocs, this), + discussion.getNodes(domainId), + ]); this.response.template = 'discussion_main_or_node.html'; this.response.body = { - ddocs, dpcount, udict, page, page_name: 'discussion_main', vndict, vnode: {}, path, + ddocs, dpcount, udict, page, page_name: 'discussion_main', vndict, vnode: {}, path, vnodes, }; } } diff --git a/hydro/handler/problem.js b/hydro/handler/problem.js index 881c02cfd..0a2c7af93 100644 --- a/hydro/handler/problem.js +++ b/hydro/handler/problem.js @@ -126,13 +126,23 @@ class ProblemCategoryHandler extends ProblemHandler { } class ProblemRandomHandler extends ProblemHandler { - async get({ domainId }) { - const q = {}; + async get({ domainId, category }) { + const q = category[0] ? { $and: [] } : {}; + for (const name of category) { + if (name) { + q.$and.push({ + $or: [ + { category: { $elemMatch: { $eq: name } } }, + { tag: { $elemMatch: { $eq: name } } }, + ], + }); + } + } if (!this.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false; const pid = await problem.random(domainId, q); if (!pid) throw new NoProblemError(); this.response.body = { pid }; - this.response.redirect = `/p/${pid}`; + this.response.redirect = this.url('problem_detail', { pid }); } } @@ -163,10 +173,6 @@ class ProblemDetailHandler extends ProblemHandler { } class ProblemSubmitHandler extends ProblemDetailHandler { - async prepare() { - this.checkPerm(PERM_SUBMIT_PROBLEM); - } - async get({ domainId, pid }) { this.response.template = 'problem_submit.html'; const rdocs = await record.getUserInProblemMulti(domainId, this.user._id, this.pdoc.docId) @@ -471,7 +477,7 @@ async function apply() { Route('problem_category', '/p/category/:category', ProblemCategoryHandler); Route('problem_random', '/problem/random', ProblemRandomHandler); Route('problem_detail', '/p/:pid', ProblemDetailHandler); - Route('problem_submit', '/p/:pid/submit', ProblemSubmitHandler); + Route('problem_submit', '/p/:pid/submit', ProblemSubmitHandler, PERM_SUBMIT_PROBLEM); Route('problem_pretest', '/p/:pid/pretest', ProblemPretestHandler); Route('problem_settings', '/p/:pid/settings', ProblemSettingsHandler); Route('problem_statistics', '/p/:pid/statistics', ProblemStatisticsHandler); diff --git a/hydro/handler/system.js b/hydro/handler/system.js index aa0178395..f3e002c78 100644 --- a/hydro/handler/system.js +++ b/hydro/handler/system.js @@ -68,7 +68,18 @@ class SystemSettingHandler extends SystemHandler { if (typeof args[key] === 'object') { const subtasks = []; for (const sub in args[key]) { - subtasks.push(system.set(`${key}.${sub}`, args[key][sub])); + if (setting.SYSTEM_SETTINGS_BY_KEY[`${key}.${sub}`]) { + const s = setting.SYSTEM_SETTINGS_BY_KEY[`${key}.${sub}`]; + if (s.ui === 'checkbox') { + if (args[key][sub] === 'on') { + subtasks.push(system.set(`${key}.${sub}`, true)); + } else { + subtasks.push(system.set(`${key}.${sub}`, false)); + } + } else { + subtasks.push(system.set(`${key}.${sub}`, args[key][sub])); + } + } } tasks.push(Promise.all(subtasks)); } else tasks.push(system.set(key, args[key])); diff --git a/hydro/handler/user.js b/hydro/handler/user.js index 0d3e5a529..2abb0ff84 100644 --- a/hydro/handler/user.js +++ b/hydro/handler/user.js @@ -1,7 +1,9 @@ const superagent = require('superagent'); +const moment = require('moment-timezone'); const { Route, Handler } = require('../service/server.js'); const user = require('../model/user'); const token = require('../model/token'); +const task = require('../model/task'); const system = require('../model/system'); const { sendMail } = require('../lib/mail'); const misc = require('../lib/misc'); @@ -10,6 +12,7 @@ const { UserNotFoundError, LoginError, SystemError, PermissionError, BlacklistedError, UserFacingError, } = require('../error'); +const { PERM_LOGGEDIN } = require('../permission.js'); class UserLoginHandler extends Handler { async get() { @@ -112,12 +115,12 @@ class UserLostPassHandler extends Handler { if (!await system.get('smtp.user')) throw new SystemError('Cannot send mail'); const udoc = await user.getByEmail(mail); if (!udoc) throw new UserNotFoundError(mail); - const tid = await token.add( + const [tid] = await token.add( token.TYPE_LOSTPASS, await system.get('lostpass_token_expire_seconds'), { uid: udoc._id }, ); - const m = await this.renderHTML('user_lostpass_mail', { url: `/lostpass/${tid}`, uname: udoc.uname }); + const m = await this.renderHTML('user_lostpass_mail', { url: `lostpass/${tid}`, uname: udoc.uname }); await sendMail(mail, 'Lost Password', 'user_lostpass_mail', m); this.response.template = 'user_lostpass_mail_sent.html'; } @@ -151,6 +154,20 @@ class UserDetailHandler extends Handler { } } +class UserDeleteHandler extends Handler { + async post({ password }) { + this.user.checkPassword(password); + const tid = await task.add({ + executeAfter: moment().add(7, 'days').toDate(), + type: 'script', + id: 'deleteUser', + args: { uid: this.user._id }, + }); + await user.setById(this.user._id, { del: tid }); + this.response.template = 'user_delete_pending.html'; + } +} + class UserSearchHandler extends Handler { async get({ domainId, q, exactMatch = false }) { let udocs; @@ -307,6 +324,7 @@ async function apply() { Route('user_lostpass', '/lostpass', UserLostPassHandler); Route('user_lostpass_with_code', '/lostpass/:code', UserLostPassWithCodeHandler); Route('user_search', '/user/search', UserSearchHandler); + Route('user_delete', '/user/delete', UserDeleteHandler, PERM_LOGGEDIN); Route('user_detail', '/user/:uid', UserDetailHandler); } diff --git a/hydro/model/problem.js b/hydro/model/problem.js index edb0c06d4..300b126e2 100644 --- a/hydro/model/problem.js +++ b/hydro/model/problem.js @@ -110,7 +110,7 @@ function count(domainId, query) { } async function random(domainId, query) { - const cursor = document.getMulti(domainId, query); + const cursor = document.getMulti(domainId, document.TYPE_PROBLEM, query); const pcount = await cursor.count(); if (pcount) { const pdoc = await cursor.skip(Math.floor(Math.random() * pcount)).limit(1).toArray(); diff --git a/hydro/model/setting.js b/hydro/model/setting.js index dac828e73..921cf65a6 100644 --- a/hydro/model/setting.js +++ b/hydro/model/setting.js @@ -1,6 +1,5 @@ const moment = require('moment-timezone'); const builtin = require('./builtin'); -const options = require('../options'); const countries = moment.tz.countries(); const tzs = new Set(); diff --git a/hydro/model/task.js b/hydro/model/task.js index 65c902151..de8086a0d 100644 --- a/hydro/model/task.js +++ b/hydro/model/task.js @@ -1,8 +1,10 @@ +const moment = require('moment-timezone'); const db = require('../service/db'); const coll = db.collection('task'); async function add(task) { + task.executeAfter = task.executeAfter || new Date(); const res = await coll.insertOne(task); return res.insertedId; } @@ -20,9 +22,15 @@ function del(_id) { } async function getFirst(query) { + query.executeAfter = query.executeAfter || { $lte: new Date() }; const res = await coll.find(query).sort('_id', 1).limit(1).toArray(); if (res.length) { await coll.deleteOne({ _id: res[0]._id }); + if (res[0].interval) { + await coll.insertOne({ + ...res[0], executeAfter: moment().add(...res[0].interval).toDate(), + }); + } return res[0]; } return null; diff --git a/hydro/script/deleteUser.js b/hydro/script/deleteUser.js new file mode 100644 index 000000000..0007683ad --- /dev/null +++ b/hydro/script/deleteUser.js @@ -0,0 +1,88 @@ +/* eslint-disable no-await-in-loop */ +const user = require('../model/user'); +const document = require('../model/document'); +const setting = require('../model/setting'); +const db = require('../service/db'); +const paginate = require('../lib/paginate'); +const { STATUS } = require('../model/builtin'); + +const collDocument = db.collection('document'); +const collStatus = db.collection('document.status'); +const collRecord = db.collection('record'); + +const $unset = { + regip: '', regat: '', loginat: '', loginip: '', +}; +for (const s of setting.PREFERENCE_SETTINGS) { + $unset[s.key] = ''; +} + +async function deleteRecord(uid) { + const [, pcount] = await paginate( + collRecord.find(), + 1, + 100, + ); + for (let i = 1; i <= pcount; i++) { + const [rdocs] = await paginate( + collRecord.find(), + 1, + 100, + ); + const tasks = []; + for (const rdoc of rdocs) { + if (rdoc.pid) { + tasks.push(document.inc(rdoc.domainId, document.TYPE_PROBLEM, rdoc.pid, 'nSubmit', -1)); + } + if (rdoc.status === STATUS.STATUS_ACCEPTED) { + tasks.push(document.inc(rdoc.domainId, document.TYPE_PROBLEM, rdoc.pid, 'nAccept', -1)); + } + } + await Promise.all(tasks); + } + await collRecord.deleteMany({ uid }); +} + +async function deleteDiscussion(uid) { + const [, pcount] = await paginate( + collDocument.find({ docType: document.TYPE_DISCUSSION, owner: uid }), + 1, + 100, + ); + for (let i = 1; i <= pcount; i++) { + const [ddocs] = await paginate( + collDocument.find({ docType: document.TYPE_DISCUSSION, owner: uid }), + 1, + 100, + ); + const tasks = []; + for (const ddoc of ddocs) { + tasks.push(document.deleteMulti(ddoc.domainId, document.TYPE_DISCUSSION_REPLY, { + parentId: ddoc.docId, + parentType: document.TYPE_DISCUSSION, + })); + } + await Promise.all(tasks); + } + await collDocument.deleteMany({ docType: document.TYPE_DISCUSSION, owner: uid }); + // TODO delete replies +} + +async function run({ + uid, +}) { + await deleteRecord(uid); + await deleteDiscussion(uid); + await collStatus.deleteMany({ uid, docType: document.TYPE_DOMAIN_USER }); + const uname = String.random(32); + await user.setById(uid, { + uname, + unameLower: uname.toLowerCase(), + priv: 0, + }, $unset); + await user.setPassword(uid, String.random(32)); + // TODO delete contest, homework, etc + return uid; +} + +global.Hydro.script.deleteUser = module.exports = { run }; diff --git a/hydro/service/server.js b/hydro/service/server.js index 45b5506f2..a19632351 100644 --- a/hydro/service/server.js +++ b/hydro/service/server.js @@ -217,14 +217,11 @@ class Handler { let res = '#'; const args = { ...kwargs }; try { - delete args.__keywords; if (this.args.domainId !== 'system') { name += '_with_domainId'; args.domainId = args.domainId || this.args.domainId; } const { anchor, query } = args; - delete args.anchor; - delete args.query; if (query) res = router.url(name, args, { query }); else res = router.url(name, args); if (anchor) return `${res}#${anchor}`; @@ -427,14 +424,11 @@ class ConnectionHandler { let res = '#'; const args = { ...kwargs }; try { - delete args.__keywords; if (this.args.domainId !== 'system') { name += '_with_domainId'; args.domainId = args.domainId || this.args.domainId; } const { anchor, query } = args; - delete args.anchor; - delete args.query; if (query) res = router.url(name, args, { query }); else res = router.url(name, args); if (anchor) return `${res}#${anchor}`; diff --git a/templates/user_delete_pending.html b/templates/user_delete_pending.html new file mode 100644 index 000000000..c76e9385b --- /dev/null +++ b/templates/user_delete_pending.html @@ -0,0 +1,10 @@ +{% extends "layout/immersive.html" %} +{% block content %} +
+

{{ _('Done') }}

+
+ {{ _('Your account will be deleted in 7 days.') }} + {{ _('You can cancel this at user setting page.') }} +
+
+{% endblock %} diff --git a/ui/components/navigation/navigation.page.js b/ui/components/navigation/navigation.page.js index e67370c5d..e6252c66d 100644 --- a/ui/components/navigation/navigation.page.js +++ b/ui/components/navigation/navigation.page.js @@ -7,7 +7,6 @@ import responsiveCutoff from 'vj/breakpoints.json'; import { isAbove } from 'vj/utils/mediaQuery'; import Navigation from '.'; - const nav = Navigation.instance; const { $nav } = nav;