+nginx.conf; update scratchpad

This commit is contained in:
undefined committed 2020-07-16 15:18:55 +08:00
1 parent 6ad41dc2ae
commit 2d5eef8d08
28 files changed
+180 -204

No files matched your search

+2
View File
@@ -7,3 +7,5 @@ web端运行脚本似乎异常
举报功能 举报功能
SETTINGS_PRIVACY.allowRegisteredUsers SETTINGS_PRIVACY.allowRegisteredUsers
model.setting.ui.name model.setting.ui.name
pdoc.secretConfig?
在线IDE 更换颜色主题
+9
View File
@@ -0,0 +1,9 @@
listen:
port: 8080
hosts:
"*":
paths:
/:
proxy.reverse.url: "http://[unix:/home/hydro/hydro.sock]"
proxy.preserve-host: ON
proxy.websocket: ON
+17
View File
@@ -0,0 +1,17 @@
server {
listen 8080;
server_name localhost;
root /tmp/hydro/public;
location / {
try_files $uri @proxy_to_app;
}
location @proxy_to_app {
proxy_pass http://unix:/home/hydro/hydro.sock;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Forwarded-For $remote_addr;
}
}
+1 -1
View File
@@ -85,7 +85,7 @@ export const CsrfTokenError = Err('CsrfTokenError', ForbiddenError, 'CsrfTokenEr
export const UserNotFoundError = Err('UserNotFoundError', NotFoundError, 'User {0} not found.'); export const UserNotFoundError = Err('UserNotFoundError', NotFoundError, 'User {0} not found.');
export const NoProblemError = Err('NoProblemError', NotFoundError, 'No problem.'); export const NoProblemError = Err('NoProblemError', NotFoundError, 'No problem.');
export const RecordNotFoundError = Err('RecordNotFoundError', NotFoundError, 'Record {0} not found.'); export const RecordNotFoundError = Err('RecordNotFoundError', NotFoundError, 'Record {0} not found.');
export const ProblemDataNotFoundError = Err('ProblemDataNotFoundError', NotFoundError, 'Data of problem {1} not found.'); export const ProblemDataNotFoundError = Err('ProblemDataNotFoundError', NotFoundError, 'Data of problem {0} not found.');
export const MessageNotFoundError = Err('MessageNotFoundError', NotFoundError, 'Message {0} not found.'); export const MessageNotFoundError = Err('MessageNotFoundError', NotFoundError, 'Message {0} not found.');
export const DocumentNotFoundError = Err('DocumentNotFoundError', NotFoundError, 'Document {2} not found.'); export const DocumentNotFoundError = Err('DocumentNotFoundError', NotFoundError, 'Document {2} not found.');
+6 -1
View File
@@ -18,6 +18,7 @@ import * as system from '../model/system';
import { import {
Route, Handler, Types, param, Route, Handler, Types, param,
} from '../service/server'; } from '../service/server';
import * as bus from '../service/bus';
const ContestHandler = contest.ContestHandlerMixin(Handler); const ContestHandler = contest.ContestHandlerMixin(Handler);
@@ -281,7 +282,11 @@ class ContestProblemSubmitHandler extends ContestProblemHandler {
lang, lang,
code, code,
}, true); }, true);
await contest.updateStatus(domainId, this.tdoc.docId, this.user._id, rid, this.pdoc.docId); const [rdoc] = await Promise.all([
record.get(domainId, rid),
contest.updateStatus(domainId, this.tdoc.docId, this.user._id, rid, this.pdoc.docId),
]);
bus.publish('record_change', rdoc);
if (!this.canShowRecord(this.tdoc)) { if (!this.canShowRecord(this.tdoc)) {
this.response.body = { tid }; this.response.body = { tid };
this.response.redirect = this.url('contest_detail', { tid }); this.response.redirect = this.url('contest_detail', { tid });
+8 -3
View File
@@ -11,6 +11,7 @@ import { PenaltyRules } from '../interface';
import { import {
Route, Handler, Types, param, Route, Handler, Types, param,
} from '../service/server'; } from '../service/server';
import * as bus from '../service/bus';
import { PERM } from '../model/builtin'; import { PERM } from '../model/builtin';
import * as system from '../model/system'; import * as system from '../model/system';
import * as user from '../model/user'; import * as user from '../model/user';
@@ -175,7 +176,7 @@ class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler {
@param('code', Types.String) @param('code', Types.String)
@param('lang', Types.String) @param('lang', Types.String)
async post(domainId: string, tid: ObjectID, pid: number, code: string, lang: string) { async post(domainId: string, tid: ObjectID, pid: number, code: string, lang: string) {
this.limitRate('add_record', 60, 100); this.limitRate('add_record', 3600, 100);
const tsdoc = await contest.getStatus(domainId, tid, this.user._id, document.TYPE_HOMEWORK); const tsdoc = await contest.getStatus(domainId, tid, this.user._id, document.TYPE_HOMEWORK);
if (!tsdoc.attend) throw new HomeworkNotAttendedError(tid); if (!tsdoc.attend) throw new HomeworkNotAttendedError(tid);
if (!contest.isOngoing(this.tdoc)) throw new HomeworkNotLiveError(tid); if (!contest.isOngoing(this.tdoc)) throw new HomeworkNotLiveError(tid);
@@ -183,8 +184,12 @@ class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler {
const rid = await record.add(domainId, { const rid = await record.add(domainId, {
pid, lang, code, uid: this.user._id, tid, hidden: true, ttype: document.TYPE_HOMEWORK, pid, lang, code, uid: this.user._id, tid, hidden: true, ttype: document.TYPE_HOMEWORK,
}, true); }, true);
await contest.updateStatus(domainId, tid, this.user._id, const [rdoc] = await Promise.all([
rid, pid, false, 0, document.TYPE_HOMEWORK); record.get(domainId, rid),
contest.updateStatus(domainId, tid, this.user._id,
rid, pid, false, 0, document.TYPE_HOMEWORK),
]);
bus.publish('record_change', rdoc);
this.response.body = { tid, rid }; this.response.body = { tid, rid };
if (this.canShowRecord(this.tdoc)) this.response.redirect = this.url('record_detail', { rid }); if (this.canShowRecord(this.tdoc)) this.response.redirect = this.url('record_detail', { rid });
else this.response.redirect = this.url('homework_detail', { tid }); else this.response.redirect = this.url('homework_detail', { tid });
+1
View File
@@ -140,6 +140,7 @@ class UiSettingsHandler extends Handler {
header_background_2x, header_background_2x,
}); });
this.response.type = 'text/css'; this.response.type = 'text/css';
this.ctx.set('nolog', '1');
} }
} }
+18 -10
View File
@@ -19,6 +19,7 @@ import {
import { import {
NoProblemError, ProblemDataNotFoundError, BadRequestError, NoProblemError, ProblemDataNotFoundError, BadRequestError,
SolutionNotFoundError, SolutionNotFoundError,
ProblemNotFoundError,
} from '../error'; } from '../error';
import { Pdoc, User, Rdoc } from '../interface'; import { Pdoc, User, Rdoc } from '../interface';
@@ -220,7 +221,11 @@ class ProblemSubmitHandler extends ProblemDetailHandler {
const rid = await record.add(domainId, { const rid = await record.add(domainId, {
uid: this.user._id, lang, code, pid: this.pdoc.docId, uid: this.user._id, lang, code, pid: this.pdoc.docId,
}, true); }, true);
await domain.incUserInDomain(domainId, this.user._id, 'nSubmit'); const [rdoc] = await Promise.all([
record.get(domainId, rid),
domain.incUserInDomain(domainId, this.user._id, 'nSubmit'),
]);
bus.publish('record_change', rdoc);
this.response.body = { rid }; this.response.body = { rid };
this.response.redirect = this.url('record_detail', { rid }); this.response.redirect = this.url('record_detail', { rid });
} }
@@ -229,13 +234,14 @@ class ProblemSubmitHandler extends ProblemDetailHandler {
class ProblemPretestHandler extends ProblemDetailHandler { class ProblemPretestHandler extends ProblemDetailHandler {
@param('lang', Types.String) @param('lang', Types.String)
@param('code', Types.String) @param('code', Types.String)
@param('input', Types.String) @param('input', Types.String, true)
async post(domainId: string, lang: string, code: string, input: string) { async post(domainId: string, lang: string, code: string, input: string = '') {
this.limitRate('add_record', 60, 100); this.limitRate('add_record', 3600, 100);
const rid = await record.add(domainId, { const rid = await record.add(domainId, {
uid: this.user._id, lang, code, pid: this.pdoc.docId, input, uid: this.user._id, lang, code, pid: this.pdoc.docId, input,
}, true); }, true);
await record.judge(domainId, rid); const rdoc = await record.get(domainId, rid);
bus.publish('record_change', rdoc);
this.response.body = { rid }; this.response.body = { rid };
} }
} }
@@ -247,7 +253,9 @@ class ProblemPretestConnectionHandler extends ConnectionHandler {
@param('pid', Types.String) @param('pid', Types.String)
async prepare(domainId: string, pid: string) { async prepare(domainId: string, pid: string) {
this.pid = pid; const pdoc = await problem.get(domainId, pid);
if (!pdoc) throw new ProblemNotFoundError(domainId, pid);
this.pid = pdoc.docId.toString();
this.domainId = domainId; this.domainId = domainId;
bus.subscribe(['record_change'], this, 'onRecordChange'); bus.subscribe(['record_change'], this, 'onRecordChange');
} }
@@ -310,11 +318,11 @@ class ProblemSettingsHandler extends ProblemManageHandler {
@param('pid', Types.String, null, parsePid) @param('pid', Types.String, null, parsePid)
@param('hidden', Types.Boolean) @param('hidden', Types.Boolean)
@param('category', Types.String, null, parseCategory) @param('category', Types.String, true, null, parseCategory)
@param('tag', Types.String, null, parseCategory) @param('tag', Types.String, true, null, parseCategory)
async postSetting( async postSetting(
domainId: string, pid: string | number, hidden = false, domainId: string, pid: string | number, hidden = false,
category: string[], tag: string[], category: string[] = [], tag: string[] = [],
) { ) {
const pdoc = await problem.get(domainId, pid); const pdoc = await problem.get(domainId, pid);
await problem.edit(domainId, pdoc.docId, { hidden, category, tag }); await problem.edit(domainId, pdoc.docId, { hidden, category, tag });
@@ -554,7 +562,7 @@ export async function apply() {
Route('problem_solution_raw', '/p/:pid/solution/:psid/raw', ProblemSolutionRawHandler); Route('problem_solution_raw', '/p/:pid/solution/:psid/raw', ProblemSolutionRawHandler);
Route('problem_solution_reply_raw', '/p/:pid/solution/:psid/:psrid/raw', ProblemSolutionReplyRawHandler); Route('problem_solution_reply_raw', '/p/:pid/solution/:psid/:psrid/raw', ProblemSolutionReplyRawHandler);
Route('problem_create', '/problem/create', ProblemCreateHandler); Route('problem_create', '/problem/create', ProblemCreateHandler);
Connection('problem_pretest_conn', '/p/:pid/pretest-conn', ProblemPretestConnectionHandler); Connection('problem_pretest_conn', '/conn/pretest', ProblemPretestConnectionHandler);
} }
global.Hydro.handler.problem = apply; global.Hydro.handler.problem = apply;
+1 -1
View File
@@ -42,7 +42,7 @@ export interface User extends Dictionary<any> {
priv: number, priv: number,
regat: Date, regat: Date,
loginat: Date, loginat: Date,
perm: bigint, perm: () => bigint,
role: string, role: string,
regip: () => string, regip: () => string,
loginip: () => string, loginip: () => string,
-10
View File
@@ -503,14 +503,6 @@ export const CATEGORIES = {
], ],
}; };
export const FOOTER_EXTRA_HTMLS = [];
export const VIEW_LANGS = [
{ code: 'zh_CN', name: '简体中文' },
{ code: 'zh_TW', name: '正體中文' },
{ code: 'en', name: 'English' },
];
export const LANG_TEXTS = { export const LANG_TEXTS = {
c: 'C', c: 'C',
cc: 'C++', cc: 'C++',
@@ -623,8 +615,6 @@ global.Hydro.model.builtin = {
BUILTIN_ROLES, BUILTIN_ROLES,
DEFAULT_NODES, DEFAULT_NODES,
CATEGORIES, CATEGORIES,
VIEW_LANGS,
FOOTER_EXTRA_HTMLS,
LANG_TEXTS, LANG_TEXTS,
LANG_HIGHLIGHT_ID, LANG_HIGHLIGHT_ID,
STATUS, STATUS,
+1 -1
View File
@@ -29,7 +29,7 @@ export async function add(
domainId: string, pid: string = null, title: string, content: string, owner: number, domainId: string, pid: string = null, title: string, content: string, owner: number,
tag: string[] = [], category: string[] = [], data: ObjectID = null, hidden = false, tag: string[] = [], category: string[] = [], data: ObjectID = null, hidden = false,
) { ) {
const d = await domain.inc(domainId, 'pid_counter', 1); const d = await domain.inc(domainId, 'pidCounter', 1);
if (!pid) pid = d.pidCounter.toString(); if (!pid) pid = d.pidCounter.toString();
return await document.add( return await document.add(
domainId, content, owner, document.TYPE_PROBLEM, d.pidCounter, null, null, domainId, content, owner, document.TYPE_PROBLEM, d.pidCounter, null, null,
+1 -1
View File
@@ -46,7 +46,7 @@ export interface JudgeTask {
type?: string, type?: string,
} }
export async function add(domainId: string, data: RdocBase, addTask: boolean) { export async function add(domainId: string, data: RdocBase, addTask: boolean): Promise<ObjectID> {
_.defaults(data, { _.defaults(data, {
status: STATUS.STATUS_WAITING, status: STATUS.STATUS_WAITING,
type: 'judge', type: 'judge',
+11 -9
View File
@@ -13,7 +13,7 @@ for (const country of countries) {
for (const t of tz) tzs.add(t); for (const t of tz) tzs.add(t);
} }
const timezones = Array.from(tzs).sort().map((tz) => [tz, tz]) as [string, string][]; const timezones = Array.from(tzs).sort().map((tz) => [tz, tz]) as [string, string][];
const langRange: Dictionary<string> = {}; export const langRange: Dictionary<string> = {};
for (const lang in global.Hydro.locales) { for (const lang in global.Hydro.locales) {
langRange[lang] = global.Hydro.locales[lang].__langname; langRange[lang] = global.Hydro.locales[lang].__langname;
@@ -78,8 +78,8 @@ export const SystemSetting = (...settings: _Setting[]) => {
PreferenceSetting( PreferenceSetting(
Setting('setting_display', 'viewLang', langRange, 'zh_CN', 'select', 'UI Language'), Setting('setting_display', 'viewLang', langRange, 'zh_CN', 'select', 'UI Language'),
Setting('setting_display', 'timeZone', timezones, 'Asia/Shanghai', 'select', 'Timezone'), Setting('setting_display', 'timeZone', timezones, 'Asia/Shanghai', 'select', 'Timezone'),
Setting('setting_usage', 'codeLang', builtin.LANG_TEXTS, null, 'select', 'Default Code Language'), Setting('setting_usage', 'codeLang', builtin.LANG_TEXTS, 'c', 'select', 'Default Code Language'),
Setting('setting_usage', 'codeTemplate', null, null, 'textarea', 'Default Code Template', Setting('setting_usage', 'codeTemplate', null, '', 'textarea', 'Default Code Template',
'If left blank, the built-in template of the corresponding language will be used.'), 'If left blank, the built-in template of the corresponding language will be used.'),
); );
@@ -87,7 +87,7 @@ AccountSetting(
Setting('setting_info', 'gravatar', null, null, 'text', 'Gravatar Email', Setting('setting_info', 'gravatar', null, null, 'text', 'Gravatar Email',
'We use <a href="https://en.gravatar.com/" target="_blank">Gravatar</a> to present your avatar icon.'), 'We use <a href="https://en.gravatar.com/" target="_blank">Gravatar</a> to present your avatar icon.'),
Setting('setting_info', 'qq', null, null, 'text', 'QQ'), Setting('setting_info', 'qq', null, null, 'text', 'QQ'),
Setting('setting_info', 'gender', builtin.USER_GENDER_RANGE, null, 'select', 'Gender'), Setting('setting_info', 'gender', builtin.USER_GENDER_RANGE, builtin.USER_GENDER_OTHER, 'select', 'Gender'),
Setting('setting_info', 'bio', null, null, 'markdown', 'Bio'), Setting('setting_info', 'bio', null, null, 'markdown', 'Bio'),
Setting('setting_customize', 'backgroundImage', null, Setting('setting_customize', 'backgroundImage', null,
'/components/profile/backgrounds/1.jpg', 'text', 'Profile Background Image', '/components/profile/backgrounds/1.jpg', 'text', 'Profile Background Image',
@@ -99,11 +99,11 @@ DomainSetting(
Setting('setting_domain', 'name', null, 'New domain', 'text', 'name'), Setting('setting_domain', 'name', null, 'New domain', 'text', 'name'),
Setting('setting_domain', 'gravatar', null, '', 'text', 'gravatar', 'Will be used as the domain icon.'), Setting('setting_domain', 'gravatar', null, '', 'text', 'gravatar', 'Will be used as the domain icon.'),
Setting('setting_domain', 'bulletin', null, '', 'markdown', 'Bulletin'), Setting('setting_domain', 'bulletin', null, '', 'markdown', 'Bulletin'),
Setting('storage', 'pid_counter', null, 0, 'number', 'Problem ID Counter', null, FLAG_HIDDEN | FLAG_DISABLED), Setting('setting_storage', 'pidCounter', null, 0, 'number', 'Problem ID Counter', null, FLAG_HIDDEN | FLAG_DISABLED),
Setting('storage', 'nAccept', null, 0, 'number', 'nAccept', null, FLAG_HIDDEN | FLAG_DISABLED), Setting('setting_storage', 'nAccept', null, 0, 'number', 'nAccept', null, FLAG_HIDDEN | FLAG_DISABLED),
Setting('storage', 'nSubmit', null, 0, 'number', 'nSubmit', null, FLAG_HIDDEN | FLAG_DISABLED), Setting('setting_storage', 'nSubmit', null, 0, 'number', 'nSubmit', null, FLAG_HIDDEN | FLAG_DISABLED),
Setting('storage', 'nLike', null, 0, 'number', 'nLike', null, FLAG_HIDDEN | FLAG_DISABLED), Setting('setting_storage', 'nLike', null, 0, 'number', 'nLike', null, FLAG_HIDDEN | FLAG_DISABLED),
Setting('storage', 'rating', null, 1500, 'number', 'rating', null, FLAG_HIDDEN | FLAG_DISABLED), Setting('setting_storage', 'rating', null, 1500, 'number', 'rating', null, FLAG_HIDDEN | FLAG_DISABLED),
); );
SystemSetting( SystemSetting(
@@ -126,6 +126,7 @@ SystemSetting(
Setting('setting_session', 'session.saved_expire_seconds', null, 3600 * 24 * 30, 'number', 'Saved session expire seconds'), Setting('setting_session', 'session.saved_expire_seconds', null, 3600 * 24 * 30, 'number', 'Saved session expire seconds'),
Setting('setting_session', 'session.unsaved_expire_seconds', null, 3600 * 3, 'number', 'Unsaved session expire seconds'), Setting('setting_session', 'session.unsaved_expire_seconds', null, 3600 * 3, 'number', 'Unsaved session expire seconds'),
Setting('setting_ui', 'ui.name', null, 'Hydro', 'text', 'Site Name'), Setting('setting_ui', 'ui.name', null, 'Hydro', 'text', 'Site Name'),
Setting('setting_ui', 'ui.footer_extra_html', null, '', 'textarea', 'Footer Extra HTML'),
Setting('setting_ui', 'ui.header_logo', null, '/components/header/header-logo.png', 'text', 'Header Logo'), Setting('setting_ui', 'ui.header_logo', null, '/components/header/header-logo.png', 'text', 'Header Logo'),
Setting('setting_ui', 'ui.header_logo_2x', null, '/components/header/header-logo@2x.png', 'text', 'Header Logo@2x'), Setting('setting_ui', 'ui.header_logo_2x', null, '/components/header/header-logo@2x.png', 'text', 'Header Logo@2x'),
Setting('setting_ui', 'ui.header_background', null, '/components/header/header-background.png', 'text', 'Header Background'), Setting('setting_ui', 'ui.header_background', null, '/components/header/header-background.png', 'text', 'Header Background'),
@@ -168,6 +169,7 @@ global.Hydro.postInit.push(
); );
global.Hydro.model.setting = { global.Hydro.model.setting = {
langRange,
Setting, Setting,
PreferenceSetting, PreferenceSetting,
AccountSetting, AccountSetting,
+3 -3
View File
@@ -43,7 +43,7 @@ export class User implements _User {
loginat: Date; loginat: Date;
perm: bigint; perm: () => bigint;
role: string; role: string;
@@ -67,7 +67,7 @@ export class User implements _User {
this.regip = () => udoc.regip; this.regip = () => udoc.regip;
this.loginat = udoc.loginat; this.loginat = udoc.loginat;
this.loginip = () => udoc.loginip; this.loginip = () => udoc.loginip;
this.perm = dudoc.perm; this.perm = () => dudoc.perm;
this.role = dudoc.role || 'default'; this.role = dudoc.role || 'default';
for (const key in setting.SETTINGS_BY_KEY) { for (const key in setting.SETTINGS_BY_KEY) {
@@ -86,7 +86,7 @@ export class User implements _User {
} }
hasPerm(p: bigint) { hasPerm(p: bigint) {
return (this.perm & p) === p; return (this.perm() & p) === p;
} }
hasPriv(p: number) { hasPriv(p: number) {
+21 -22
View File
@@ -12,7 +12,7 @@ import Body from 'koa-body';
import Router from 'koa-router'; import Router from 'koa-router';
import cache from 'koa-static-cache'; import cache from 'koa-static-cache';
import sockjs from 'sockjs'; import sockjs from 'sockjs';
import serialize from 'serialize-javascript'; import serialize, { SerializeJSOptions } from 'serialize-javascript';
import parse from 'yargs-parser'; import parse from 'yargs-parser';
import { lrucache } from '../utils'; import { lrucache } from '../utils';
import { User, DomainDoc } from '../interface'; import { User, DomainDoc } from '../interface';
@@ -23,6 +23,7 @@ import {
NotFoundError, NotFoundError,
} from '../error'; } from '../error';
import { render } from '../lib/template'; import { render } from '../lib/template';
import hash from '../lib/hash.hydro';
import * as misc from '../lib/misc'; import * as misc from '../lib/misc';
import * as user from '../model/user'; import * as user from '../model/user';
import * as domain from '../model/domain'; import * as domain from '../model/domain';
@@ -30,10 +31,8 @@ import * as system from '../model/system';
import * as blacklist from '../model/blacklist'; import * as blacklist from '../model/blacklist';
import * as token from '../model/token'; import * as token from '../model/token';
import * as opcount from '../model/opcount'; import * as opcount from '../model/opcount';
import hash from '../lib/hash.hydro';
const argv = parse(process.argv.slice(2)); const argv = parse(process.argv.slice(2));
let enableLog = true;
const app = new Koa(); const app = new Koa();
let server; let server;
const router = new Router(); const router = new Router();
@@ -43,14 +42,6 @@ interface H {
[key: string]: any, [key: string]: any,
} }
interface IHandler {
// @ts-ignore
new(ctx: Koa.Context): Handler;
}
interface IConnectionHandler {
// @ts-ignore
new(conn: sockjs.Connection): ConnectionHandler;
}
type MethodDecorator = (target: any, name: string, obj: any) => any; type MethodDecorator = (target: any, name: string, obj: any) => any;
type Converter = (value: any) => any; type Converter = (value: any) => any;
type Validator = (value: any) => boolean; type Validator = (value: any) => boolean;
@@ -268,7 +259,6 @@ export class Handler {
} }
async renderHTML(name: string, context: any): Promise<string> { async renderHTML(name: string, context: any): Promise<string> {
if (enableLog) console.time(name);
const UserContext = { const UserContext = {
...this.user, ...this.user,
gravatar: misc.gravatar(this.user.gravatar || '', 128), gravatar: misc.gravatar(this.user.gravatar || '', 128),
@@ -281,7 +271,6 @@ export class Handler {
_: this.translate.bind(this), _: this.translate.bind(this),
...context, ...context,
}); });
if (enableLog) console.timeEnd(name);
return res; return res;
} }
@@ -406,6 +395,7 @@ export class Handler {
), ),
]); ]);
this.csrfToken = this.getCsrfToken(this.session._id || String.random(32)); this.csrfToken = this.getCsrfToken(this.session._id || String.random(32));
this.UIContext.csrfToken = this.csrfToken;
} }
async finish() { async finish() {
@@ -421,12 +411,23 @@ export class Handler {
} }
async renderBody() { async renderBody() {
if (this.response.redirect) {
this.response.body = this.response.body || {};
this.response.body.url = this.response.redirect;
}
if (this.response.type) return; if (this.response.type) return;
if (this.request.json || this.response.redirect || !this.response.template) { if (
this.response.body = serialize(this.response.body); this.request.json || this.response.redirect
|| this.request.query.noTemplate || !this.response.template) {
try {
this.response.body = JSON.stringify(this.response.body);
} catch (e) {
const opt: SerializeJSOptions = { ignoreFunction: true };
if (this.request.query.noTemplate) opt.space = 2;
this.response.body = serialize(this.response.body, opt);
}
this.response.type = 'application/json'; this.response.type = 'application/json';
} else if (this.response.body || this.response.template) { } else if (this.response.body || this.response.template) {
if (this.request.query.noTemplate || this.request.json) return;
const templateName = this.request.query.template || this.response.template; const templateName = this.request.query.template || this.response.template;
if (templateName) { if (templateName) {
this.response.body = this.response.body || {}; this.response.body = this.response.body || {};
@@ -442,10 +443,6 @@ export class Handler {
this.ctx.response.status = 302; this.ctx.response.status = 302;
this.ctx.redirect(this.response.redirect); this.ctx.redirect(this.response.redirect);
} else { } else {
if (this.response.redirect) {
this.response.body = this.response.body || {};
this.response.body.url = this.response.redirect;
}
if (this.response.body != null) { if (this.response.body != null) {
this.ctx.response.body = this.response.body; this.ctx.response.body = this.response.body;
this.ctx.response.status = this.response.status || 200; this.ctx.response.status = this.response.status || 200;
@@ -757,8 +754,10 @@ export function Middleware(middleware: Koa.Middleware) {
export async function start() { export async function start() {
const [disableLog, port] = await system.getMany(['server.log', 'server.port']); const [disableLog, port] = await system.getMany(['server.log', 'server.port']);
if (!disableLog) { if (!disableLog) {
app.use(morgan(':method :url :status :res[content-length] - :response-time ms')); app.use(morgan(':method :url :status :res[content-length] - :response-time ms', {
} else enableLog = false; skip: (req, res) => res.hasHeader('nolog'),
}));
}
app.use(router.routes()).use(router.allowedMethods()); app.use(router.routes()).use(router.allowedMethods());
Route('notfound_handler', '*', Handler); Route('notfound_handler', '*', Handler);
server.listen(argv.port || port); server.listen(argv.port || port);
+1 -1
View File
@@ -234,7 +234,7 @@ Domain {0} is bulit-in and cannot be modified.: 域 {0} 为内置,不可修改
Domain {0} not found.: 域 {0} 不存在。 Domain {0} not found.: 域 {0} 不存在。
Domain ID cannot be changed once the domain is created.: 在创建后不能更改 ID。 Domain ID cannot be changed once the domain is created.: 在创建后不能更改 ID。
Domain ID: 域 ID Domain ID: 域 ID
domain_dashboard: 域概况 domain_dashboard: 管理域
domain_discussion: 讨论节点 domain_discussion: 讨论节点
domain_edit: 编辑域资料 domain_edit: 编辑域资料
domain_join: 加入域 domain_join: 加入域
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "hydrooj", "name": "hydrooj",
"version": "2.8.1", "version": "2.8.2",
"main": "hydro/loader.ts", "main": "hydro/loader.ts",
"repository": "https://github.com/hydro-dev/Hydro.git", "repository": "https://github.com/hydro-dev/Hydro.git",
"author": "masnn", "author": "masnn",
@@ -79,10 +79,10 @@
"build": "node build --development", "build": "node build --development",
"build:development": "node build --development", "build:development": "node build --development",
"build:production": "node build --production", "build:production": "node build --production",
"build:all": "cd ui && yarn && yarn build:production && cd .. && yarn --ignore-optional && yarn build:production", "build:all": "cd ui && yarn && yarn build:production && cd .. && yarn && yarn build:production",
"start": "ts-node hydro/loader.ts", "start": "ts-node hydro/loader.ts",
"pkg": "pkg .", "pkg": "pkg .",
"lint": "eslint build hydro module tool --ext ts,js --fix", "lint": "eslint build hydro tool --ext ts,js --fix",
"snyk-protect": "snyk protect", "snyk-protect": "snyk protect",
"prepare": "yarn run snyk-protect" "prepare": "yarn run snyk-protect"
}, },
+3 -2
View File
@@ -53,8 +53,8 @@
<li class="footer__extra-link-item" data-dropdown-target="#menu-footer-lang"> <li class="footer__extra-link-item" data-dropdown-target="#menu-footer-lang">
<span><span class="icon icon-global"></span> Language <span class="icon icon-expand_less"></span></span> <span><span class="icon icon-global"></span> Language <span class="icon icon-expand_less"></span></span>
<ol class="dropdown-target menu" id="menu-footer-lang"> <ol class="dropdown-target menu" id="menu-footer-lang">
{% for item in model.builtin.VIEW_LANGS %} {% for k, v in model.setting.langRange %}
<li class="menu__item"><a class="menu__link" href="{{ url('switch_language', lang=item.code) }}">{{ item.name }}</a></li> <li class="menu__item"><a class="menu__link" href="{{ url('switch_language', lang=k) }}">{{ v }}</a></li>
{% endfor %} {% endfor %}
</ol> </ol>
</li> </li>
@@ -62,6 +62,7 @@
</div> </div>
<div class="footer__extra-right"> <div class="footer__extra-right">
<ol class="clearfix"> <ol class="clearfix">
<!-- TODO -->
{% for html in model.builtin.FOOTER_EXTRA_HTMLS %} {% for html in model.builtin.FOOTER_EXTRA_HTMLS %}
<li class="footer__extra-link-item">{{ html|safe }}</li> <li class="footer__extra-link-item">{{ html|safe }}</li>
{% endfor %} {% endfor %}
+3 -2
View File
@@ -4,6 +4,7 @@
var Context = {{ { var Context = {{ {
problemId: pdoc.pid, problemId: pdoc.pid,
postSubmitUrl: url('problem_submit', pid=pdoc.pid), postSubmitUrl: url('problem_submit', pid=pdoc.pid),
postPretestUrl: url('problem_pretest', pid=pdoc.pid),
getSubmissionsUrl: url('problem_submit', pid=pdoc.pid), getSubmissionsUrl: url('problem_submit', pid=pdoc.pid),
getRecordDetailUrl: url('record_detail', rid='{rid}'), getRecordDetailUrl: url('record_detail', rid='{rid}'),
codeLang: handler.user.codeLang, codeLang: handler.user.codeLang,
@@ -28,13 +29,13 @@
</blockquote> </blockquote>
{% endif %} {% endif %}
{% if tdoc %} {% if tdoc %}
{% if tdoc['doc_type'] == vj4.model.document.TYPE_CONTEST %} {% if tdoc['docType'] == model.document.TYPE_CONTEST %}
{% if model.contest.isDone(tdoc) %} {% if model.contest.isDone(tdoc) %}
<blockquote class="note"> <blockquote class="note">
<p>{{ _('You cannot submit for this problem because the contest is ended. You can click "Open in Problem Set" to view this problem in normal mode.') }}</p> <p>{{ _('You cannot submit for this problem because the contest is ended. You can click "Open in Problem Set" to view this problem in normal mode.') }}</p>
</blockquote> </blockquote>
{% endif %}{# handler.isDone(tdoc) #} {% endif %}{# handler.isDone(tdoc) #}
{% endif %}{# tdoc['doc_type'] == vj4.model.document.TYPE_CONTEST #} {% endif %}{# tdoc['docType'] == model.document.TYPE_CONTEST #}
{% endif %}{# tdoc #} {% endif %}{# tdoc #}
{{ pdoc['content']|markdown|safe }} {{ pdoc['content']|markdown|safe }}
</div> </div>
+1 -1
View File
@@ -5,7 +5,7 @@
<p><b>{{ _('Current dataset comes from') }} <a href="{{ pdoc.data.split('from:')[1] }}" target="_blank">{{ pdoc.data.split('from:')[1] }}</a></b></p> <p><b>{{ _('Current dataset comes from') }} <a href="{{ pdoc.data.split('from:')[1] }}" target="_blank">{{ pdoc.data.split('from:')[1] }}</a></b></p>
{% else %} {% else %}
<p>{{ _('Current dataset: {0}').format((pdoc['data'] or _('(None)')).toString()) }}</p> <p>{{ _('Current dataset: {0}').format((pdoc['data'] or _('(None)')).toString()) }}</p>
<p>{{ _('MD5') }}: {{ md5|default(_('(None)'))) }}</p> <p>{{ _('MD5') }}: {{ md5|default(_('(None)')) }}</p>
{% endif %} {% endif %}
<p>{{ _('New dataset') }}: <p>{{ _('New dataset') }}:
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
+23 -1
View File
@@ -11,6 +11,7 @@ const IGNORE_MISSING = [
]; ];
const RE_TEMPLATE = /_\(['"]([\s\S])*?['"]\)/gmi; const RE_TEMPLATE = /_\(['"]([\s\S])*?['"]\)/gmi;
const RE_UI = /i18n\(['"]([\s\S])*?['"](,.*?)?\)/gmi; const RE_UI = /i18n\(['"]([\s\S])*?['"](,.*?)?\)/gmi;
const RE_SETTING = /Setting\(['"]([\s\S])*?['"](,[^()]*?)\)/gmi;
const TEMPLATE_ROOT = path.resolve(__dirname, '..', 'templates'); const TEMPLATE_ROOT = path.resolve(__dirname, '..', 'templates');
const LOCALE_ROOT = path.resolve(__dirname, '..', 'locales'); const LOCALE_ROOT = path.resolve(__dirname, '..', 'locales');
const UI_ROOT = path.resolve(__dirname, '..', 'ui'); const UI_ROOT = path.resolve(__dirname, '..', 'ui');
@@ -27,6 +28,8 @@ function _(str, format) {
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const i18n = _; const i18n = _;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const Setting = _;
function scanTemplate(folder, relative = 'templates') { function scanTemplate(folder, relative = 'templates') {
const files = fs.readdirSync(folder); const files = fs.readdirSync(folder);
@@ -65,7 +68,7 @@ function scanUi(folder, relative = 'ui') {
eval(substr); eval(substr);
} catch (e) { } catch (e) {
if (e.message.endsWith('is not defined')) { if (e.message.endsWith('is not defined')) {
global[e.message.split(' ')[0]] = undefined; global[e.message.split(' ')[0]] = () => { };
try { try {
eval(substr); eval(substr);
} catch (err) { } catch (err) {
@@ -78,8 +81,27 @@ function scanUi(folder, relative = 'ui') {
} }
} }
} }
function scanSetting() {
const file = path.resolve(__dirname, '..', 'hydro', 'model', 'setting.ts');
const content = fs.readFileSync(file).toString();
content.replace(RE_SETTING, (substr) => {
try {
eval(substr);
} catch (e) {
if (e.message.endsWith('is not defined')) {
global[e.message.split(' ')[0]] = () => { };
try {
eval(substr);
} catch (err) {
console.error('Cannot parse: ', substr, ' in setting');
}
} else console.error('Cannot parse: ', substr, ' in setting');
}
});
}
scanTemplate(TEMPLATE_ROOT); scanTemplate(TEMPLATE_ROOT);
scanUi(UI_ROOT); scanUi(UI_ROOT);
scanSetting();
if (!name) { if (!name) {
for (const locale of locales) { for (const locale of locales) {
if (!IGNORE_CHECK.includes(locale.split('.')[0])) { if (!IGNORE_CHECK.includes(locale.split('.')[0])) {
@@ -1,28 +1,24 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Tabs, { TabPane } from 'rc-tabs';
import TabContent from 'rc-tabs/lib/TabContent';
import ScrollableInkTabBar from 'rc-tabs/lib/ScrollableInkTabBar';
import i18n from 'vj/utils/i18n'; import i18n from 'vj/utils/i18n';
import Icon from 'vj/components/react/IconComponent'; import Icon from 'vj/components/react/IconComponent';
import Panel from './PanelComponent'; import Panel from './PanelComponent';
import PanelButton from './PanelButtonComponent'; import DataInput from './DataInputComponent';
import ScratchpadPretestTabPane from './ScratchpadPretestTabPaneContainer';
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
current: state.pretest.current, input: state.pretest.input,
tabs: state.pretest.tabs, output: state.pretest.output,
meta: state.pretest.meta, rid: state.pretest.rid,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
handleClickClose() { handleDataChange(type, value) {
dispatch({ dispatch({
type: 'SCRATCHPAD_UI_SET_VISIBILITY', type: 'SCRATCHPAD_PRETEST_DATA_CHANGE',
payload: { payload: {
uiElement: 'pretest', type,
visibility: false, value,
}, },
}); });
}, },
@@ -41,32 +37,18 @@ export default class ScratchpadPretestContainer extends React.PureComponent {
</span> </span>
)} )}
> >
<Tabs <div className="flex-row flex-fill">
className="scratchpad__panel-tab flex-col flex-fill" <DataInput
activeKey={this.props.current} title={i18n('Input')}
onChange={(tabId) => this.props.handleSwitchData(tabId)} value={this.props.input}
animation="slide-horizontal" onChange={(v) => this.props.handleDataChange('input', v)}
renderTabBar={() => (
<ScrollableInkTabBar
extraContent={(
<span>
<PanelButton
onClick={() => this.props.handleClickClose()}
>
<Icon name="close" />
</PanelButton>
</span>
)}
/> />
)} <DataInput
renderTabContent={() => <TabContent />} title={i18n('Output')}
> value={this.props.output}
{this.props.tabs.map((tabId) => ( onChange={(v) => this.props.handleDataChange('output', v)}
<TabPane tab={this.props.meta[tabId].title} key={tabId}> />
<ScratchpadPretestTabPane id={tabId} /> </div>
</TabPane>
))}
</Tabs>
</Panel> </Panel>
); );
} }
@@ -1,49 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import i18n from 'vj/utils/i18n';
import DataInput from './DataInputComponent';
const mapStateToProps = (state) => ({
data: state.pretest.data,
});
const mapDispatchToProps = (dispatch) => ({
handleDataChange(id, type, value) {
dispatch({
type: 'SCRATCHPAD_PRETEST_DATA_CHANGE',
payload: {
id,
type,
value,
},
});
},
});
const mergeProps = (stateProps, dispatchProps, ownProps) => ({
...dispatchProps,
id: ownProps.id,
input: stateProps.data[ownProps.id] ? stateProps.data[ownProps.id].input : '',
output: stateProps.data[ownProps.id] ? stateProps.data[ownProps.id].output : '',
});
@connect(mapStateToProps, mapDispatchToProps, mergeProps)
export default class ScratchpadPretestTabPaneContainer extends React.PureComponent {
render() {
return (
<div className="flex-row flex-fill">
<DataInput
title={i18n('Sample Input')}
value={this.props.input}
onChange={(v) => this.props.handleDataChange(this.props.id, 'input', v)}
/>
<DataInput
title={i18n('Sample Output')}
value={this.props.output}
onChange={(v) => this.props.handleDataChange(this.props.id, 'output', v)}
/>
</div>
);
}
}
@@ -18,6 +18,7 @@ const mapStateToProps = (state) => ({
pretestVisible: state.ui.pretest.visible, pretestVisible: state.ui.pretest.visible,
recordsVisible: state.ui.records.visible, recordsVisible: state.ui.records.visible,
isPosting: state.ui.isPosting, isPosting: state.ui.isPosting,
isRunning: state.pretest.isRunning,
editorLang: state.editor.lang, editorLang: state.editor.lang,
}); });
@@ -36,15 +37,11 @@ const mapDispatchToProps = (dispatch) => ({
}, },
postPretest(context) { postPretest(context) {
const state = context.store.getState(); const state = context.store.getState();
const { pretest } = state; const { input } = state.pretest;
const testCases = pretest.tabs;
const inputs = testCases.map((tabId) => pretest.data[tabId].input);
const outputs = testCases.map((tabId) => pretest.data[tabId].output);
const req = request.post(Context.postPretestUrl, { const req = request.post(Context.postPretestUrl, {
lang: state.editor.lang, lang: state.editor.lang,
code: state.editor.code, code: state.editor.code,
data_input: inputs, input,
data_output: outputs,
}); });
dispatch({ dispatch({
type: 'SCRATCHPAD_POST_PRETEST', type: 'SCRATCHPAD_POST_PRETEST',
@@ -74,7 +71,7 @@ export default class ScratchpadToolbarContainer extends React.PureComponent {
return ( return (
<Toolbar> <Toolbar>
<ToolbarButton <ToolbarButton
disabled={this.props.isPosting || !this.props.pretestValid} disabled={this.props.isPosting || this.props.isRunning}
className="scratchpad__toolbar__pretest" className="scratchpad__toolbar__pretest"
onClick={() => this.props.postPretest(this.context)} onClick={() => this.props.postPretest(this.context)}
data-global-hotkey="f9" data-global-hotkey="f9"
@@ -113,7 +110,7 @@ export default class ScratchpadToolbarContainer extends React.PureComponent {
</ToolbarItem> </ToolbarItem>
<ToolbarSplit /> <ToolbarSplit />
<ToolbarButton <ToolbarButton
activated activated={this.props.pretestVisible}
onClick={() => this.props.togglePanel('pretest')} onClick={() => this.props.togglePanel('pretest')}
data-global-hotkey="alt+p" data-global-hotkey="alt+p"
data-tooltip={`${i18n('Toggle Pretest Panel')} (Alt+P)`} data-tooltip={`${i18n('Toggle Pretest Panel')} (Alt+P)`}
+19 -30
View File
@@ -1,41 +1,30 @@
import { v4 as uuid } from 'uuid';
const initialId = uuid();
export default function reducer(state = { export default function reducer(state = {
counter: 1,
current: initialId,
tabs: [initialId],
meta: {
[initialId]: {
id: initialId,
title: '#1',
},
},
data: {
[initialId]: {
id: initialId,
title: '#1',
input: '', input: '',
output: '', output: '',
}, rid: null,
},
}, action) { }, action) {
switch (action.type) { if (action.type === 'SCRATCHPAD_PRETEST_DATA_CHANGE') {
case 'SCRATCHPAD_PRETEST_DATA_CHANGE': { const { type, value } = action.payload;
const { id, type, value } = action.payload;
return { return {
...state, ...state,
data: {
...state.data,
[id]: {
...state.data[id],
[type]: value, [type]: value,
},
},
}; };
} }
default: if (action.type === 'SCRATCHPAD_RECORDS_PUSH') {
return state; const { rdoc } = action.payload;
if (rdoc._id === state.rid) {
return {
...state,
output: (rdoc.stdout || '') + (rdoc.stderr || ''),
rid: null,
};
} }
}
if (action.type === 'SCRATCHPAD_POST_PRETEST_FULFILLED') {
return {
...state,
rid: action.payload.rid,
};
}
return state;
} }
@@ -15,11 +15,6 @@ export default function reducer(state = {
} }
case 'SCRATCHPAD_RECORDS_PUSH': { case 'SCRATCHPAD_RECORDS_PUSH': {
const { rdoc } = action.payload; const { rdoc } = action.payload;
if (rdoc.uid !== UserContext._id
|| rdoc.pid !== Context.problemId
) {
return state;
}
return { return {
...state, ...state,
rows: [rdoc._id, ...state.rows], rows: [rdoc._id, ...state.rows],
+1 -1
View File
@@ -167,7 +167,7 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
React, render, unmountComponentAtNode, Provider, store, React, render, unmountComponentAtNode, Provider, store,
} = await loadReactRedux(ScratchpadReducer); } = await loadReactRedux(ScratchpadReducer);
const sock = new SockJs(Context.socketUrl); const sock = new SockJs(`/conn/pretest?token=${UiContext.token}&pid=${Context.problemId}`);
sock.onmessage = (message) => { sock.onmessage = (message) => {
const msg = JSON.parse(message.data); const msg = JSON.parse(message.data);
store.dispatch({ store.dispatch({
+2 -2
View File
@@ -22,12 +22,12 @@ clone 本项目到一个空文件夹,并进行编译:
git clone https://github.com/hydro-dev/Hydro.git git clone https://github.com/hydro-dev/Hydro.git
cd Hydro cd Hydro
yarn build:all yarn build:all
yarn start node .build/app.js
``` ```
注意:单个Hydro实例需要约100MB的运行内存,但在载入时可能需要较多内存(取决于安装的模块数量与大小) 注意:单个Hydro实例需要约100MB的运行内存,但在载入时可能需要较多内存(取决于安装的模块数量与大小)
Hydro 会自行初始化并监听 8888 端口。请用浏览器访问并进行相应配置。 Hydro 会自行初始化并监听 8888 端口。请用浏览器访问并进行相应配置。
数据库配置完成后,会自动创建 Root 用户。(账号 `root` ,密码 `rootroot`),请及时修改。 数据库配置完成后,会自动创建 Root 用户。(账号 `Root` ,密码 `rootroot`),请及时修改。
之后的进阶配置可在 管理 面板进行。 之后的进阶配置可在 管理 面板进行。