diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 11a242c8..03ccdd0f 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -1,6 +1,8 @@ root: true extends: - airbnb-typescript/base +env: + jquery: true globals: Atomics: readonly SharedArrayBuffer: readonly diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 2f00a67f..ebd028f1 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "2.20.49", + "version": "2.20.50", "bin": "bin/hydrooj.js", "main": "dist/loader.js", "typings": "dist/loader.d.ts", diff --git a/packages/hydrooj/src/handler/user.ts b/packages/hydrooj/src/handler/user.ts index 8b85615d..8d202817 100644 --- a/packages/hydrooj/src/handler/user.ts +++ b/packages/hydrooj/src/handler/user.ts @@ -57,7 +57,7 @@ class UserLogoutHandler extends Handler { } } -class UserRegisterHandler extends Handler { +export class UserRegisterHandler extends Handler { async get() { this.response.template = 'user_register.html'; } diff --git a/packages/hydrooj/src/model/builtin.ts b/packages/hydrooj/src/model/builtin.ts index 51523b7d..616ce61b 100644 --- a/packages/hydrooj/src/model/builtin.ts +++ b/packages/hydrooj/src/model/builtin.ts @@ -231,7 +231,7 @@ export const PRIV = { PRIV_REJUDGE: 1 << 13, PRIV_VIEW_USER_SECRET: 1 << 14, PRIV_VIEW_JUDGE_STATISTICS: 1 << 15, - PRIV_UNLIMITED_RATE: 1 << 22, + PRIV_UNLIMITED_ACCESS: 1 << 22, PRIV_ALL: -1, PRIV_DEFAULT: 0, diff --git a/packages/hydrooj/src/service/bus.ts b/packages/hydrooj/src/service/bus.ts index ce7408a6..c5f28f3f 100644 --- a/packages/hydrooj/src/service/bus.ts +++ b/packages/hydrooj/src/service/bus.ts @@ -12,6 +12,7 @@ import type { import type { DomainDoc } from '../loader'; import type { DocType } from '../model/document'; import type { ProblemSolutionHandler } from '../handler/problem'; +import type { UserRegisterHandler } from '../handler/user'; const _hooks: Record any>> = {}; const logger = new Logger('bus', true); @@ -23,7 +24,7 @@ function isBailed(value: any) { export type Disposable = () => void; export type VoidReturn = Promise | any; -export interface EventMap { +export interface EventMap extends Record { 'app/started': () => void 'app/load/lib': () => VoidReturn 'app/load/locale': () => VoidReturn @@ -63,6 +64,10 @@ export interface EventMap { 'handler/create': (thisArg: Handler) => VoidReturn 'handler/init': (thisArg: Handler) => VoidReturn + 'handler/before-prepare/UserRegister': (thisArg: UserRegisterHandler) => VoidReturn + 'handler/before/UserRegister': (thisArg: UserRegisterHandler) => VoidReturn + 'handler/after/UserRegister': (thisArg: UserRegisterHandler) => VoidReturn + 'handler/finish/UserRegister': (thisArg: UserRegisterHandler) => VoidReturn 'handler/solution/get': (thisArg: ProblemSolutionHandler) => VoidReturn 'discussion/before-add': ( diff --git a/packages/hydrooj/src/service/server.ts b/packages/hydrooj/src/service/server.ts index 4227f441..9a6e7025 100644 --- a/packages/hydrooj/src/service/server.ts +++ b/packages/hydrooj/src/service/server.ts @@ -274,7 +274,7 @@ export class HandlerCommon { extraTitleContent?: string; async limitRate(op: string, periodSecs: number, maxOperations: number) { - if (this.user && this.user.hasPriv(PRIV.PRIV_UNLIMITED_RATE)) return; + if (this.user && this.user.hasPriv(PRIV.PRIV_UNLIMITED_ACCESS)) return; await opcount.inc(op, this.request.ip, periodSecs, maxOperations); } @@ -352,6 +352,7 @@ export class Handler extends HandlerCommon { ctx: Koa.Context; request: { + method: string, host: string, hostname: string, ip: string, @@ -384,6 +385,7 @@ export class Handler extends HandlerCommon { super(); this.ctx = ctx; this.request = { + method: ctx.request.method.toLowerCase(), host: ctx.request.host, hostname: ctx.request.hostname, ip: ctx.request.ip, @@ -634,15 +636,16 @@ async function handle(ctx, HandlerClass, checker) { } await bus.serial('handler/init', h); - + await bus.serial(`handler/before-prepare/${HandlerClass.name.replace(/Handler$/, '')}`, h); if (h._prepare) await h._prepare(args); if (h.prepare) await h.prepare(args); - + await bus.serial(`handler/before/${HandlerClass.name.replace(/Handler$/, '')}`, h); if (h[method]) await h[method](args); if (operation) await h[`post${operation}`](args); - + await bus.serial(`handler/after/${HandlerClass.name.replace(/Handler$/, '')}`, h); if (h.cleanup) await h.cleanup(args); if (h.finish) await h.finish(args); + await bus.serial(`handler/finish/${HandlerClass.name.replace(/Handler$/, '')}`, h); } catch (e) { try { await h.onerror(e); diff --git a/packages/hydrooj/src/upgrade.ts b/packages/hydrooj/src/upgrade.ts index 0e29408e..e1fbd897 100644 --- a/packages/hydrooj/src/upgrade.ts +++ b/packages/hydrooj/src/upgrade.ts @@ -231,6 +231,7 @@ const scripts: UpgradeScript[] = [ const _FRESH_INSTALL_IGNORE = 1; await db.collection('domain.user').deleteMany({ uid: null }); await db.collection('domain.user').deleteMany({ uid: 0 }); + return true; }, async function _15_16() { const _FRESH_INSTALL_IGNORE = 1; @@ -242,6 +243,7 @@ const scripts: UpgradeScript[] = [ if (JSON.stringify(af) !== JSON.stringify(pdoc.additional_file)) $set.additional_file = af; if (Object.keys($set).length) await problem.edit(pdoc.domainId, pdoc.docId, $set); }); + return true; }, ]; diff --git a/packages/recaptcha/handler.ts b/packages/recaptcha/handler.ts new file mode 100644 index 00000000..37646067 --- /dev/null +++ b/packages/recaptcha/handler.ts @@ -0,0 +1,26 @@ +import superagent from 'superagent'; +import { ValidationError } from 'hydrooj/dist/error'; +import { PRIV } from 'hydrooj/dist/model/builtin'; +import * as system from 'hydrooj/dist/model/system'; +import * as bus from 'hydrooj/dist/service/bus'; + +bus.on('handler/before/UserRegister', async (thisArg) => { + if (thisArg.request.method !== 'post') { + thisArg.UiContext.recaptchaKey = system.get('recaptcha.key'); + return; + } + if (thisArg.user.hasPriv(PRIV.PRIV_UNLIMITED_ACCESS)) return; + if (!thisArg.args.captcha) throw new ValidationError('captcha'); + const response = await superagent.post('https://recaptcha.net/recaptcha/api/siteverify') + .field('secret', system.get('recaptcha.secret')) + .field('response', thisArg.args.captcha) + .field('remoteip', thisArg.request.ip); + if (!response.body.success) throw new ValidationError('captcha fail'); +}); + +bus.on('handler/after/UserRegister', async (thisArg) => { + thisArg.response.body.captcha = `\ + + + `; +}); diff --git a/packages/recaptcha/package.json b/packages/recaptcha/package.json new file mode 100644 index 00000000..ac4a43f7 --- /dev/null +++ b/packages/recaptcha/package.json @@ -0,0 +1,12 @@ +{ + "name": "@hydrooj/recaptcha", + "version": "1.0.0", + "description": "Google reCAPTCHA", + "main": "package.json", + "repository": "https://github.com/hydro-dev/Hydro.git", + "author": "undefined", + "license": "AGPL-3.0-only", + "dependencies": { + "superagent": "^6.1.0" + } +} \ No newline at end of file diff --git a/packages/recaptcha/public/captcha-main.page.js b/packages/recaptcha/public/captcha-main.page.js new file mode 100644 index 00000000..7461087a --- /dev/null +++ b/packages/recaptcha/public/captcha-main.page.js @@ -0,0 +1,18 @@ +(() => { + const { AutoloadPage } = window.Hydro; + + const page = new AutoloadPage('recaptcha', () => { + function captcha(event) { + event.preventDefault(); + grecaptcha.ready(function () { + grecaptcha.execute(UiContext.recaptchaKey, { action: 'submit' }).then(function (token) { + document.getElementById('_captcha').value = token; + document.getElementById('_submit').click(); + }); + }); + } + document.getElementById('submit').onclick = captcha; + }); + + window.Hydro.extraPages.push(page); +})(); \ No newline at end of file diff --git a/packages/recaptcha/setting.yaml b/packages/recaptcha/setting.yaml new file mode 100644 index 00000000..ccb74570 --- /dev/null +++ b/packages/recaptcha/setting.yaml @@ -0,0 +1,4 @@ +key: + type: text +secret: + type: text \ No newline at end of file