From 0f1f5f26e1d866f130d74fc1e48102ce73473578 Mon Sep 17 00:00:00 2001 From: undefined Date: Tue, 21 Jul 2020 10:41:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=99=84=E5=8A=A0=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E5=BC=80=E5=8F=91=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 3 +- README.md | 14 ++- TODO | 6 +- docs/addon.md | 195 +++++++++++++++++++++++++++++++++++++++ docs/deploy.md | 31 +++++++ docs/development.md | 208 +----------------------------------------- tool/install.js | 17 ---- 7 files changed, 242 insertions(+), 232 deletions(-) create mode 100644 docs/addon.md delete mode 100644 tool/install.js diff --git a/.vscode/settings.json b/.vscode/settings.json index 15f10111..881d5ae8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,8 @@ { "files.autoGuessEncoding": false, - "files.autoSave": "onFocusChange", "files.encoding": "utf8", "files.eol": "\n", - "editor.detectIndentation": true, + "editor.detectIndentation": false, "editor.tabSize": 4, "editor.formatOnSave": true, "editor.renderWhitespace": "boundary" diff --git a/README.md b/README.md index 7550524a..0ea1c01c 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,23 @@ # Hydro -Hydro是一个高效的信息学在线测评系统。 -特点: 易于部署,轻量,功能强大。 +Hydro是一个高效的信息学在线测评系统。特点:易于部署,轻量,功能强大且易于扩展。 -[中文文档](docs/deploy.md) +[部署说明](docs/deploy.md) +[附加组件开发说明](docs/addon.md) +[Hydro开发说明](docs/development.md) +[Hydro UI 传送门](https://github.com/hydro-dev/ui-default) 如果您认为本项目有价值,欢迎 star 。 +相关文档若说明的不够详细,请提交 Pull Request或联系开发组说明。 +bug和功能建议请在 Issues 提出。 -[在 Gitpod 尝试本项目](https://gitpod.io/#https://github.com/hydro-dev/Hydro) +[在 Gitpod 打开已配置完成的测试环境](https://gitpod.io/#https://github.com/hydro-dev/Hydro) ## 联系我们 QQ [3402182471](https://wpa.qq.com/msgrd?v=3&uin=3402182471&site=qq&menu=yes) +hydro-dev 群:709572015 +Hydro 用户群:1085853538 Telegram [@webpack_exports_undefined](https://t.me/webpack_exports_undefined) ## 鸣谢 diff --git a/TODO b/TODO index e0222bba..0de05d87 100644 --- a/TODO +++ b/TODO @@ -1,12 +1,10 @@ @requireCsrfToken -为model.system添加缓存 -修复discussion前端权限节点显示异常 -web端运行脚本似乎异常 +discussion前端权限节点显示异常? +web端运行脚本异常? 讨论回复通知 比赛举办者发布通知 举报功能 SETTINGS_PRIVACY.allowRegisteredUsers model.setting.ui.name pdoc.secretConfig? -在线IDE 更换颜色主题, fix resize move to @hydrooj/core? diff --git a/docs/addon.md b/docs/addon.md new file mode 100644 index 00000000..bc272d89 --- /dev/null +++ b/docs/addon.md @@ -0,0 +1,195 @@ +# Hydro 附加组件开发 + +前置条件:NodeJS>10.10 +此教程将以编写剪贴板插件为例进行说明。 + +## Step1 初始化项目 + +在一个空文件夹中运行 `yarn init` 并按照提示填写相关信息。 + +```sh +/workspace/hydro-plugin $ yarn init +yarn init v1.22.4 +question name (hydro-plugin): @hydrooj/pastebin +question version (1.0.0): 0.0.1 +question description: HydroOJ的剪贴板组件 +question entry point (index.js): package.json +question repository url: https://github.com/hydro-dev/pastebin.git +question author: undefined +question license (MIT): MIT +question private: +success Saved package.json +``` + +## Step2 准备编写组件 + +分析:剪贴板组件需要以下功能: + +- 与数据库交互来存储/检索相应文档。 +- 提供 /paste/create 路由以创建新文档。 +- 提供 /paste/show/:ID 来查看已创建的文档。 +- 根据用户ID进行鉴权,允许将文档设置为私密以防止他人查看。 + +Hydro的推荐架构如下: + +- handler.js: 用于处理路由 +- model.js: 数据库模型 +- lib.js: 不依赖于数据库等的库(如md5函数) +- script.js: 可能会被用户多次使用到的脚本(如重新计算rating) +- locale/: 翻译文件 +- template/: 页面模板 +- setting.yaml: 模块所用到的设置,格式在下方说明 + +## Step3 model.js + +提示:由于模块中不便于使用 require() 引入 Hydro 的文件,可以从 global.Hydro 中取出需要的模块。 + +```js +const { db } = global.Hydro.service; // 数据库连接 +const coll = db.collection('paste'); + +/** + * 添加一个文档 + * @param {number} userId + * @param {string} content + * @param {boolean} isPrivate + * @return {Promise} + */ +async function add(userId, content, isPrivate) { + const pasteId = String.random(16); // Hydro提供了此方法,创建一个长度为16的随机字符串 + // 使用 mongodb 为数据库驱动,相关操作参照其文档 + const result = await coll.insertOne({ + _id: pasteId, + owner: userId, + content, + isPrivate, + }); + return result.insertedId; // 返回插入的文档ID +} + +/** + * 查询一个文档 + * @param {string} pasteId + * @return {Promise} + */ +async function get(pasteId) { + return await coll.findOne({ _id: pasteId }); +} + +// 暴露这些接口 +global.Hydro.model.pastebin = { add, get }; + +``` + +## Step4 handler.js + +在路由中定义所有的函数应均为异步函数,支持的函数有:prepare, get, post, post[Operation], cleanup +具体流程如下: + +``` +先执行 prepare(args) (如果存在) +args 为传入的参数集合(包括 QueryString, Body, Path)中的全部参数, +再执行 prepare(args) (如果存在) +检查请求类型: + +为 GET ? + -> 执行 get(args) +为 POST ? + -> 执行 post(args) + -> 含有 operation 字段? + -> 执行 post[Operation] +``` + +执行 cleanup() + +如果在 this.response.template 指定模板则渲染,否则直接返回 this.response.body 中的内容。 + +* 在表单提交时的 operation 字段使用下划线,函数名使用驼峰命名。 + +如 `` 对应 `postConfirmDelete` 函数。 + +应当提供 `apply` 函数,并与定义的 Handler 一同挂载到 `global.Hydro.handler[模块名]` 位置。 +`apply` 函数将在初始化阶段被调用。 + +```js +const { Route, Handler } = global.Hydro.service.server; // 注册路由所用工具 +const { PRIV } = global.Hydro.model.builtin; // 内置 Privilege 权限节点 +const pastebin = global.Hydro.model.pastebin; // 刚刚编写的pastebin模型 +const { checkContent } = global.Hydro.lib.validator; // 用于检查用户输入是否合法 +const { NotFoundError } = global.Hydro.error; + +// 创建新路由 +class PasteCreateHandler extends Handler { + // Get请求时触发该函数 + async get() { + // 检查用户是否登录,此处为多余(因为底部注册路由时已声明所需权限) + // 此方法适用于权限的动态检查 + // this.checkPriv(PRIV.PRIV_USER_PROFILE); + this.response.template = 'paste_create.html'; // 返回此页面 + } + + async post({ content, private = false }) { // 从用户提交的表单中取出content和private字段 + checkContent(content); // 检查输入 + // 在HTML表单提交的多选框中,选中值为 'on',未选中则为空,需要进行转换 + await pastebin.add(this.user._id, content, !!private); + // 将用户重定向到创建完成的url + this.response.redirect = this.url('paste_show', { id: pasteid }); + } +} + +class PasteShowHandler extends Handler { + async get({ id }) { + const doc = await pastebin.get(id); + if (!doc) throw new NotFoundError(id); + if (doc.isPrivate) { + if (this.user._id !== doc.owner) throw new PermissionError(); + } + this.response.body = { doc }; + this.response.template = 'paste_show.html'; + } + + async postDelete({ id }){ + // 当提交表单并存在 operation 值为 delete 时执行。 + // 本例中未实现删除功能,仅作为说明。 + } +} + +// Hydro会在服务初始化完成后调用该函数。 +async function apply(){ + // 注册一个名为 paste_create 的路由,匹配 '/paste/create', + // 使用PasteCreateHandler处理,访问改路由需要PRIV.PRIV_USER_PROFILE权限 + // 提示:路由匹配基于 path-to-regexp + Route('paste_create', '/paste/create', PasteCreateHandler, PRIV.PRIV_USER_PROFILE); + Route('paste_show', '/paste/show/:id', PasteShowHandler); +} + +global.Hydro.handler.pastebin = apply; +``` + +对于 Typescript 用户的额外说明:Hydro为Typescript提供了表单验证API。 +`@param` 会修改 arguments,首个参数为请求所在的 domainId,剩余参数为指定的内容。 +使用如下: + +```ts +const { Handler, Route, Types, param } = global.Hydro.service.server; +const { user } = global.Hydro.model; +const { isPassword } = global.Hydro.lib.validator; + +class UserLoginHandler extends Handler { + @param('username', Types.String) + @param('password', Types.String, isPassword) + async post(domainId:string, username:string, password:string) { + const udoc = await user.getByUname(username); + udoc.checkPassword(password); + this.response.body = { udoc }; + } +} +``` + +## Step5 template + +TODO + +## Step6 Locale + +用于提供多国翻译。格式与 Hydro 的 locale 文件夹格式相同。 diff --git a/docs/deploy.md b/docs/deploy.md index 33c8cffc..ad2118eb 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -36,6 +36,37 @@ Hydro 会自行初始化并监听 8888 端口(可使用 `--port=1234` 指定 之后的进阶配置可在 管理 面板进行。 +## 附加组件 + +警告:附加组件对站点所有内容具有完全的访问权限。请不要安装来历不明的组件。 + +#### 安装附加组件: + +先全局安装所需模块,再向 hydrooj 注册即可。 例:安装 @hydrooj/geoip + +```sh +yarn global add @hydrooj/geoip +hydrooj addon add @hydrooj/geoip +``` + +#### 附加组件列表 + +Hydro官方目前提供了以下附加组件: + +| ID | 描述 | 大小 | +| ---------------------- | ------------------------------ | ----- | +| @hydrooj/ui-default | Hydro的默认用户界面 | ~10MB | +| @hydrooj/geoip | GeoIP 支持,用于显示用户登录地 | ~60MB | +| @hydrooj/migrate-vijos | 从vijos4的自动升级工具 | <1MB | +| @hydrooj/hydrojudge | 评测组件 | ~2MB | + +#### 卸载附加组件 + +```sh +yarn global remove @hydrooj/geoip +hydrooj addon remove @hydrooj/geoip +``` + ## 杂项 [OAuth 配置](./oauth.md) diff --git a/docs/development.md b/docs/development.md index 3b930c68..72953b18 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,206 +1,4 @@ -# Basic | basic +# Hydro 开发 -Hydro 后端使用 NodeJS 编写,前端使用 JQuery + React。 -代码风格遵循 airbnb 标准(详见 .eslintrc.js)。 - -由于模块中不能使用 require() 引入 Hydro 的文件,因此需要从 global.Hydro 中取出需要的模块。 -例: - -```js -const { db } = global.Hydro.service; -const { problem } = global.Hydro.model; -``` - -# Module | module - -## 文件架构 - -Hydro 的模块由以下几个部分组成: - -service: 服务 -script: 脚本 -handler: 访问路由 -lib: 库 -model: 数据库模型 -file: 额外文件 -locale: 多国化 -template: UI 模板 -README.md: 介绍 -hydro.json: 声明文件(必须) - -## 模块编译 - -使用 `hydro-build` 进行编译。 -安装:`yarn add hydro-build -D` -编译:`hydro-build` -如果命运的齿轮没有出差错,您应该可以找到生成的.hydro文件了。 - -# Hydro.json | hydro - -Hydro.json 是模块的声明文件,格式如下。 - -```json -{ - "id": "模块ID", - "version": "模块版本", - "description": "模块描述" -} -``` - -# Handler | handler - -通常用于提供页面路由。 - -例:注册新路由: - -```js -const { Handler, Route } = global.Hydro.service.server; -const { user, builtin } = global.Hydro.model; - -class CustomHandler extends Handler { - async prepare() { - this.checkPriv(builtin.PRIV.PRIV_USER_PROFILE); - // this.checkPerm(), this.user.hasPerm(), this.user.hasPriv(), etc. - } - - async get(){ - this.response.template = 'user_login.html'; - } - - async post({ username, password }) { - const udoc = await user.getByUname(username); - udoc.checkPassword(password); - this.response.body = { udoc }; - } - - async postConfirm() { - // 当提交表单并存在 operation 值为 confirm 时执行。 - } -} - -async function apply() { - Route('/route/:username', CustomHandler); -} - -global.Hydro.handler.handlerName = apply; -``` - -在路由中定义所有的函数应均为异步函数,支持的函数如下: - -prepare, get, post, post[Operation], cleanup - -具体流程如下: - -先执行 prepare(args) (如果存在) -args 为传入的参数集合(包括 QueryString, Body, Path)中的全部参数, -再执行 prepare(args) (如果存在) -检查请求类型: - -``` -为 GET ? - -> 执行 get(args) -为 POST ? - -> 执行 post(args) - -> 含有 operation 字段? - -> 执行 post[Operation] -``` - -执行 cleanup() - -如果在 this.response.template 指定模板则渲染,否则直接返回 this.response.body 中的内容。 - -* 在表单提交时的 operation 字段使用下划线,函数名使用驼峰命名。 - -如 `` 对应 `postConfirmDelete` 函数。 - -应当提供 `apply` 函数,并与定义的 Handler 一同挂载到 `global.Hydro.handler[模块名]` 位置。 -`apply` 函数将在初始化阶段被调用。 - -### 表单验证 - -若使用 Typescript 开发插件,可使用 Hydro 提供的验证工具。 - -`@param` 会修改 arguments,首个参数为请求所在的 domainId,剩余参数为指定的内容。 - -```ts -const { Handler, Route, Types, param } = global.Hydro.service.server; -const { user, builtin } = global.Hydro.model; - -class CustomHandler extends Handler { - async prepare() { - this.checkPriv(builtin.PRIV.PRIV_USER_PROFILE); - } - - async get(){ - this.response.template = 'user_login.html'; - } - - @param('username', Types.String) - @param('password', Types.String) - async post(domainId:string, username:string, password:string) { - const udoc = await user.getByUname(username); - udoc.checkPassword(password); - this.response.body = { udoc }; - } -} - -export async function apply() { - Route('/route/:username', CustomHandler); -} - -global.Hydro.handler.handlerName = apply; -``` - -若使用 Javascript 开发插件,则可使用 `global.Hydro.lib.validator` 中提供的相关工具。 - -# Service | service - -通常用于提供与其他程序对接的接口或启动其他外部程序。(如内置的 MongoDB / 外置的沙箱模块等) - -# Lib | lib - -库文件。通常用于提供一些功能(废话)。 - -# File | file - -file 文件夹下的所有文件将被自动解压到 `$TMPDIR/hydro/模块ID/路径` 的位置(权限755,通常用于启动子进程)。 - -# Locale | locale - -用于提供多国翻译。格式与 Hydro 的 locale 文件夹格式相同。 - -# Template | template - -页面模板,使用 [nunjucks](https://mozilla.github.io/nunjucks/cn/templating.html) 语法。 -传入了 _ 翻译函数与 model 等。 - -```html -{% set page_name = "page_name" %} -{% extends "layout/basic.html" %} -{% block content %} -
-
-
-
-
-

{{ _('Oops!') }}

-

{{ _(error.message).format(error.params) }}

-

{{ _('Technical Information') }}:

-

{{ _('Type') }}: {{ error.code }}

-

{{ _('Arguments') }}: -

    - {% for param in error.params %} -
  1. {{ param }}
  2. - {% endfor %} -
-

-
-
-{% endblock %} -``` - -
请不要覆盖已有模板。
- -# README.md | readme - -项目的说明文件。 +Hydro 使用 Typescript 开发。代码风格遵循 airbnb 标准,indent=4spaces。 +详见 .eslintrc.js diff --git a/tool/install.js b/tool/install.js deleted file mode 100644 index 50701cf5..00000000 --- a/tool/install.js +++ /dev/null @@ -1,17 +0,0 @@ -const zlib = require('zlib'); -const fs = require('fs'); - -async function install() { - if (!global.Hydro) throw new Error('Data missing'); - if (!fs.existsSync('hydro')) fs.mkdirSync('hydro'); - const hydro = JSON.parse(zlib.gunzipSync(global.Hydro).toString()); - fs.writeFileSync('app.js', hydro.app); - for (const i in hydro.modules) { - fs.writeFileSync(`${i}.hydro`, Buffer.from(hydro.modules[i], 'base64')); - } -} - -install().catch((e) => { - console.error(e); - process.exit(1); -});