diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index a234c6245..d3e4f72c3 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "3.2.27", + "version": "3.2.28", "bin": "bin/hydrooj.js", "main": "src/loader", "module": "src/loader", diff --git a/packages/hydrooj/src/handler/blog.ts b/packages/hydrooj/src/handler/blog.ts index 9c0068bd0..f4fb74155 100644 --- a/packages/hydrooj/src/handler/blog.ts +++ b/packages/hydrooj/src/handler/blog.ts @@ -100,10 +100,10 @@ class BlogEditHandler extends BlogHandler { @param('content', Types.Content) async postUpdate(domainId: string, did: ObjectID, title: string, content: string) { if (!this.user.own(this.ddoc)) this.checkPriv(PRIV.PRIV_EDIT_SYSTEM); - await blog.edit(did, title, content); - await oplog.add({ - ...this.ddoc, operator: this.user._id, type: 'edit', - }); + await Promise.all([ + blog.edit(did, title, content), + oplog.log(this, 'blog.edit', this.ddoc), + ]); this.response.body = { did }; this.response.redirect = this.url('blog_detail', { uid: this.user._id, did }); } @@ -111,10 +111,10 @@ class BlogEditHandler extends BlogHandler { @param('did', Types.ObjectID) async postDelete(domainId: string, did: ObjectID) { if (!this.user.own(this.ddoc)) this.checkPriv(PRIV.PRIV_EDIT_SYSTEM); - await blog.del(did); - await oplog.add({ - ...this.ddoc, operator: this.user._id, operateIp: this.request.ip, type: 'delete', - }); + await Promise.all([ + blog.del(did), + oplog.log(this, 'blog.delete', this.ddoc), + ]); this.response.redirect = this.url('blog_main', { uid: this.ddoc.owner }); } } diff --git a/packages/hydrooj/src/handler/discussion.ts b/packages/hydrooj/src/handler/discussion.ts index 12055b6cb..761a6c234 100644 --- a/packages/hydrooj/src/handler/discussion.ts +++ b/packages/hydrooj/src/handler/discussion.ts @@ -251,10 +251,10 @@ class DiscussionDetailHandler extends DiscussionHandler { async postEditReply(domainId: string, drid: ObjectID, content: string) { this.checkPerm(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF); if (!this.user.own(this.drdoc)) throw new PermissionError(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF); - await discussion.editReply(domainId, drid, content); - await oplog.add({ - ...this.drdoc, operator: this.user._id, operateIp: this.request.ip, type: 'edit', - }); + await Promise.all([ + discussion.editReply(domainId, drid, content), + oplog.log(this, 'discussion.reply.edit', this.drdoc), + ]); this.back(); } @@ -266,10 +266,10 @@ class DiscussionDetailHandler extends DiscussionHandler { this.checkPerm(PERM.PERM_DELETE_DISCUSSION_REPLY); } else this.checkPerm(PERM.PERM_DELETE_DISCUSSION_SELF); } - await discussion.delReply(domainId, drid); - await oplog.add({ - ...this.drdoc, operator: this.user._id, operateIp: this.request.ip, type: 'delete', - }); + await Promise.all([ + discussion.delReply(domainId, drid), + oplog.log(this, 'discussion.reply.delete', this.drdoc), + ]); this.back(); } @@ -279,11 +279,10 @@ class DiscussionDetailHandler extends DiscussionHandler { async postEditTailReply(domainId: string, drid: ObjectID, drrid: ObjectID, content: string) { this.checkPerm(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF); if (!this.user.own(this.drrdoc)) throw new PermissionError(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF); - await discussion.editTailReply(domainId, drid, drrid, content); - // TODO: history? - await oplog.add({ - ...this.drrdoc, operator: this.user._id, operateIp: this.request.ip, type: 'edit', - }); + await Promise.all([ + discussion.editTailReply(domainId, drid, drrid, content), + oplog.log(this, 'discussion.tailReply.edit', this.drrdoc), + ]); this.back(); } @@ -291,10 +290,10 @@ class DiscussionDetailHandler extends DiscussionHandler { @param('drrid', Types.ObjectID) async postDeleteTailReply(domainId: string, drid: ObjectID, drrid: ObjectID) { if (!this.user.own(this.drrdoc)) this.checkPerm(PERM.PERM_DELETE_DISCUSSION_REPLY); - await discussion.delTailReply(domainId, drid, drrid); - await oplog.add({ - ...this.drrdoc, operator: this.user._id, operateIp: this.request.ip, type: 'delete', - }); + await Promise.all([ + discussion.delTailReply(domainId, drid, drrid), + oplog.log(this, 'discussion.tailReply.delete', this.drrdoc), + ]); this.back(); } @@ -346,10 +345,10 @@ class DiscussionEditHandler extends DiscussionHandler { else this.checkPerm(PERM.PERM_EDIT_DISCUSSION_SELF); if (!this.user.hasPerm(PERM.PERM_HIGHLIGHT_DISCUSSION)) highlight = this.ddoc.highlight; if (!this.user.hasPerm(PERM.PERM_PIN_DISCUSSION)) pin = this.ddoc.pin; - await discussion.edit(domainId, did, title, content, highlight, pin); - await oplog.add({ - ...this.ddoc, operator: this.user._id, type: 'edit', - }); + await Promise.all([ + discussion.edit(domainId, did, title, content, highlight, pin), + oplog.log(this, 'discussion.edit', this.ddoc), + ]); this.response.body = { did }; this.response.redirect = this.url('discussion_detail', { did }); } @@ -358,10 +357,10 @@ class DiscussionEditHandler extends DiscussionHandler { async postDelete(domainId: string, did: ObjectID) { if (!this.user.own(this.ddoc)) this.checkPerm(PERM.PERM_DELETE_DISCUSSION); else this.checkPerm(PERM.PERM_DELETE_DISCUSSION_SELF); - await discussion.del(domainId, did); - await oplog.add({ - ...this.ddoc, operator: this.user._id, operateIp: this.request.ip, type: 'delete', - }); + await Promise.all([ + oplog.log(this, 'discussion.delete', this.ddoc), + discussion.del(domainId, did), + ]); this.response.body = { type: this.ddoc.parentType, parent: this.ddoc.parentId }; this.response.redirect = this.url('discussion_node', { type: discussion.typeDisplay[this.ddoc.parentType], diff --git a/packages/hydrooj/src/handler/domain.ts b/packages/hydrooj/src/handler/domain.ts index 52d0e6ac4..a1286296c 100644 --- a/packages/hydrooj/src/handler/domain.ts +++ b/packages/hydrooj/src/handler/domain.ts @@ -11,6 +11,7 @@ import paginate from '../lib/paginate'; import { PERM, PERMS_BY_FAMILY, PRIV } from '../model/builtin'; import * as discussion from '../model/discussion'; import domain from '../model/domain'; +import * as oplog from '../model/oplog'; import { DOMAIN_SETTINGS, DOMAIN_SETTINGS_BY_KEY } from '../model/setting'; import * as system from '../model/system'; import user from '../model/user'; @@ -114,14 +115,20 @@ class DomainUserHandler extends ManageHandler { @post('uid', Types.Int) @post('role', Types.Name) async postSetUser(domainId: string, uid: number, role: string) { - await domain.setUserRole(domainId, uid, role); + await Promise.all([ + domain.setUserRole(domainId, uid, role), + oplog.log(this, 'domain.setRole', { uid, role }), + ]); this.back(); } @param('uid', Types.NumericArray) @param('role', Types.Name) async postSetUsers(domainId: string, uid: number[], role: string) { - await domain.setUserRole(domainId, uid, role); + await Promise.all([ + domain.setUserRole(domainId, uid, role), + oplog.log(this, 'domain.setRole', { uid, role }), + ]); this.back(); } } @@ -242,7 +249,10 @@ class DomainJoinHandler extends Handler { throw new InvalidJoinInvitationCodeError(this.domain._id); } } - await domain.setUserRole(this.domain._id, this.user._id, this.joinSettings.role); + await Promise.all([ + domain.setUserRole(this.domain._id, this.user._id, this.joinSettings.role), + oplog.log(this, 'domain.join', {}), + ]); this.response.redirect = this.url('homepage', { query: { notification: 'Successfully joined domain.' } }); } } diff --git a/packages/hydrooj/src/handler/misc.ts b/packages/hydrooj/src/handler/misc.ts index f24a03c47..d7ae26485 100644 --- a/packages/hydrooj/src/handler/misc.ts +++ b/packages/hydrooj/src/handler/misc.ts @@ -91,14 +91,8 @@ export class FSDownloadHandler extends Handler { this.response.addHeader('Cache-Control', 'public'); const target = `user/${uid}/${filename}`; const file = await storage.getMeta(target); - await oplog.add({ - type: 'download', - time: new Date(), - uid: this.user._id, - ip: this.request.ip, - fileType: 'user', + await oplog.log(this, 'download.file.user', { target, - referer: this.request.referer, size: file?.size || 0, }); if (!file) { diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index 36a3c6cc8..37f33b86e 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -514,14 +514,8 @@ export class ProblemFilesHandler extends ProblemDetailHandler { ?.filter((i) => files.has(i.name)) ?.map((i) => i.size), ) || 0; - await oplog.add({ - type: 'bulkDownload', - time: new Date(), - uid: this.user._id, - ip: this.request.ip, - fileType: 'problem', + await oplog.log(this, 'download.problem.bulk', { target: Array.from(files).map((file) => `problem/${this.pdoc.domainId}/${this.pdoc.docId}/${type}/${file}`), - referer: this.request.referer, size, }); for (const file of files) { @@ -618,14 +612,8 @@ export class ProblemFileDownloadHandler extends ProblemDetailHandler { } const target = `problem/${this.pdoc.domainId}/${this.pdoc.docId}/${type}/${filename}`; const file = await storage.getMeta(target); - await oplog.add({ - type: 'download', - time: new Date(), - uid: this.user._id, - ip: this.request.ip, - fileType: 'problem', + await oplog.log(this, 'download.problem.single', { target, - referer: this.request.referer, size: file?.size || 0, }); if (!file) { diff --git a/packages/hydrooj/src/handler/user.ts b/packages/hydrooj/src/handler/user.ts index 8cd29b05b..e8d5203a9 100644 --- a/packages/hydrooj/src/handler/user.ts +++ b/packages/hydrooj/src/handler/user.ts @@ -14,6 +14,7 @@ import BlackListModel from '../model/blacklist'; import { PERM, PRIV, STATUS } from '../model/builtin'; import domain from '../model/domain'; import oauth from '../model/oauth'; +import * as oplog from '../model/oplog'; import problem, { ProblemDoc } from '../model/problem'; import * as system from '../model/system'; import task from '../model/task'; @@ -118,7 +119,10 @@ class UserLoginHandler extends Handler { let udoc = await user.getByEmail(domainId, uname); if (!udoc) udoc = await user.getByUname(domainId, uname); if (!udoc) throw new UserNotFoundError(uname); - await this.limitRate('user_login', 60, 5); + await Promise.all([ + this.limitRate('user_login', 60, 5), + oplog.log(this, 'user.login', { redirect }), + ]); if (udoc._tfa && !verifyToken(udoc._tfa, tfa)) throw new InvalidTokenError('2FA token invalid.'); udoc.checkPassword(password); await user.setById(udoc._id, { loginat: new Date(), loginip: this.request.ip }); diff --git a/packages/hydrooj/src/model/oplog.ts b/packages/hydrooj/src/model/oplog.ts index 337c7204e..79d2e0360 100644 --- a/packages/hydrooj/src/model/oplog.ts +++ b/packages/hydrooj/src/model/oplog.ts @@ -1,6 +1,7 @@ import { ObjectID } from 'mongodb'; import { OplogDoc } from '../interface'; import db from '../service/db'; +import type { Handler } from '../service/server'; export const coll = db.collection('oplog'); @@ -14,8 +15,26 @@ export async function add(data: Partial & { type: string }): Promise(handler: T, type: string, data: any) { + const res = await coll.insertOne({ + ...data, + _id: new ObjectID(), + type, + time: new Date(), + domainId: handler.domainId, + ua: handler.request.headers?.['user-agent'], + referer: handler.request.headers?.referer, + args: handler.args, + operator: handler.user?.id, + operateIp: handler.request.ip, + }); + return res.insertedId; +} + export async function get(id: ObjectID): Promise { return await coll.findOne({ _id: id }); } -global.Hydro.model.oplog = { coll, add, get }; +global.Hydro.model.oplog = { + coll, add, get, log, +};