mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
Merge remote-tracking branch 'origin/master' into rejudge_task
This commit is contained in:
96 files changed
+2077
-1691
No files matched your search
@@ -43,7 +43,6 @@
|
||||
height: 30px;
|
||||
box-sizing: border-box;
|
||||
padding: 4px 6px;
|
||||
width: 100%;
|
||||
min-width: 30px;
|
||||
flex-grow: 1;
|
||||
border: 0;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"name": "@hydrooj/components",
|
||||
"version": "1.0.0-beta.0",
|
||||
"version": "1.0.0-beta.2",
|
||||
"main": "./frontend/index.ts",
|
||||
"dependencies": {
|
||||
"allotment": "^1.20.3",
|
||||
"diff": "^7.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -15,6 +16,11 @@
|
||||
"@types/diff": "^7.0.2",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"sass": "^1.86.3"
|
||||
"sass": "^1.87.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-dom": "*",
|
||||
"schemastery": "*"
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "@hydrooj/elastic-search",
|
||||
"version": "1.2.0-beta.0",
|
||||
"version": "1.2.0-beta.2",
|
||||
"main": "index.ts",
|
||||
"repository": "https://github.com/hydro-dev/Hydro",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@elastic/elasticsearch": "^8.17.1"
|
||||
"@elastic/elasticsearch": "^8.18.2"
|
||||
},
|
||||
"engines": {
|
||||
"hydrooj": ">=5.0.0-alpha.0"
|
||||
"hydrooj": ">=5.0.0-alpha.1"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/fps-importer",
|
||||
"version": "1.6.0-beta.0",
|
||||
"version": "1.6.0-beta.1",
|
||||
"description": "Import FPS problems",
|
||||
"main": "index.ts",
|
||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"maxmind": "^4.3.24"
|
||||
"maxmind": "^4.3.25"
|
||||
},
|
||||
"preferUnplugged": true
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@hydrooj/hydrojudge",
|
||||
"bin": "bin/hydrojudge.js",
|
||||
"version": "3.5.0-beta.0",
|
||||
"version": "3.5.0-beta.2",
|
||||
"main": "./src/index.ts",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||
@@ -14,8 +14,8 @@
|
||||
"schemastery": "^3.16.1",
|
||||
"semver": "^7.7.1",
|
||||
"shell-quote": "^1.8.2",
|
||||
"superagent": "^10.2.0",
|
||||
"ws": "^8.18.1"
|
||||
"superagent": "^10.2.1",
|
||||
"ws": "^8.18.2"
|
||||
},
|
||||
"preferUnplugged": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
|
||||
@@ -18,7 +18,7 @@ export = async function main() {
|
||||
fs.emptyDirSync(CACHE_DIR);
|
||||
logger.info('Cleaned cache.');
|
||||
} else if (argv.args[1] === 'prune') {
|
||||
const duration = +argv.options.duration || 30;
|
||||
const duration = +argv.options.duration || +argv.options.days || 30;
|
||||
const now = Date.now();
|
||||
const hosts = await fs.readdir(CACHE_DIR, { withFileTypes: true });
|
||||
let cnt = 0;
|
||||
@@ -59,5 +59,34 @@ export = async function main() {
|
||||
for (const [domain, s] of top10) {
|
||||
logger.info(' %s: %s', domain, size(s));
|
||||
}
|
||||
} else if (argv.args[1] === 'clearCompileCache') {
|
||||
const hosts = await fs.readdir(CACHE_DIR, { withFileTypes: true });
|
||||
let cnt = 0;
|
||||
for (const host of hosts) {
|
||||
const hostdir = path.resolve(CACHE_DIR, host.name);
|
||||
if (!host.isDirectory()) continue;
|
||||
const domains = await fs.readdir(hostdir, { withFileTypes: true });
|
||||
for (const domain of domains) {
|
||||
if (!domain.isDirectory()) continue;
|
||||
const domaindir = path.resolve(hostdir, domain.name);
|
||||
const problems = await fs.readdir(domaindir, { withFileTypes: true });
|
||||
for (const problem of problems) {
|
||||
const problemdir = path.resolve(domaindir, problem.name);
|
||||
if (!fs.statSync(problemdir).isDirectory()) continue;
|
||||
const etags = path.resolve(problemdir, 'etags');
|
||||
if (fs.existsSync(etags)) {
|
||||
const etag = JSON.parse(await fs.readFile(etags, 'utf-8'));
|
||||
if (etag['*cache']) {
|
||||
const removed = etag['*cache'];
|
||||
delete etag['*cache'];
|
||||
await fs.writeFile(etags, JSON.stringify(etag));
|
||||
logger.info('Removed bianry %s %o', problemdir, removed);
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.info('Done! %d items deleted.', cnt);
|
||||
}
|
||||
};
|
||||
@@ -193,7 +193,14 @@ export class JudgeTask {
|
||||
type: 'interactor' | 'validator' | 'checker' | 'generator' | 'manager' | 'std',
|
||||
source: CompilableSource, checkerType?: string,
|
||||
): Promise<Execute> {
|
||||
if (type === 'checker' && ['default', 'strict'].includes(checkerType)) return { execute: '', copyIn: {}, clean: () => Promise.resolve(null) };
|
||||
if (type === 'checker' && ['default', 'strict'].includes(checkerType)) {
|
||||
return {
|
||||
execute: '',
|
||||
copyIn: {},
|
||||
clean: () => Promise.resolve(null),
|
||||
[Symbol.asyncDispose]: () => Promise.resolve(null),
|
||||
};
|
||||
}
|
||||
if (type === 'checker' && !checkers[checkerType]) throw new FormatError('Unknown checker type {0}.', [checkerType]);
|
||||
if (this.compileCache?.[type]) {
|
||||
return {
|
||||
@@ -238,6 +245,7 @@ export class JudgeTask {
|
||||
this.compileCache[type] = {
|
||||
execute: result.execute,
|
||||
copyIn: newCopyIn,
|
||||
[Symbol.asyncDispose]: () => Promise.resolve(null),
|
||||
};
|
||||
await get((result.copyIn[result._cacheable] as PreparedFile).fileId, loc);
|
||||
const currEtag = await fs.readFile(join(this.folder, 'etags'), 'utf-8');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydrooj",
|
||||
"version": "5.0.0-beta.0",
|
||||
"version": "5.0.0-beta.2",
|
||||
"bin": "bin/hydrooj.js",
|
||||
"main": "src/plugin-api",
|
||||
"module": "src/plugin-api",
|
||||
@@ -16,9 +16,8 @@
|
||||
"@aws-sdk/lib-storage": "3.726.1",
|
||||
"@aws-sdk/s3-presigned-post": "3.726.1",
|
||||
"@aws-sdk/s3-request-presigner": "3.726.1",
|
||||
"@babel/code-frame": "^7.26.2",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@cordisjs/plugin-timer": "1.1.1",
|
||||
"@graphql-tools/schema": "^10.0.23",
|
||||
"@hydrooj/common": "workspace:^",
|
||||
"@hydrooj/framework": "workspace:^",
|
||||
"@hydrooj/utils": "workspace:^",
|
||||
@@ -27,13 +26,11 @@
|
||||
"@zip.js/zip.js": "^2.7.60",
|
||||
"adm-zip": "0.5.16",
|
||||
"cac": "^6.7.14",
|
||||
"cordis": "4.0.0-beta.1",
|
||||
"cordis": "4.0.0-beta.2",
|
||||
"csv-stringify": "^6.5.2",
|
||||
"emoji-regex": "^10.4.0",
|
||||
"emojis-list": "2.1.0",
|
||||
"fs-extra": "^11.3.0",
|
||||
"graphql": "^16.10.0",
|
||||
"graphql-scalars": "^1.24.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"koa-proxies": "^0.12.4",
|
||||
"koa-static-cache": "^5.1.4",
|
||||
@@ -41,10 +38,10 @@
|
||||
"lru-cache": "^10.4.3",
|
||||
"mime-types": "^3.0.1",
|
||||
"moment-timezone": "^0.5.48",
|
||||
"mongodb": "^6.15.0",
|
||||
"mongodb": "^6.16.0",
|
||||
"mongodb-uri": "^0.9.7",
|
||||
"nanoid": "^5.1.5",
|
||||
"nodemailer": "^6.10.1",
|
||||
"nodemailer": "^7.0.3",
|
||||
"notp": "^2.0.3",
|
||||
"p-queue": "^8.1.0",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
@@ -52,7 +49,7 @@
|
||||
"schemastery": "^3.16.1",
|
||||
"semver": "^7.7.1",
|
||||
"shorty.js": "^1.0.1",
|
||||
"superagent": "^10.2.0",
|
||||
"superagent": "^10.2.1",
|
||||
"tar": "6.2.1",
|
||||
"thirty-two": "^1.0.2",
|
||||
"ua-parser-js": "^2.0.3"
|
||||
|
||||
@@ -49,7 +49,6 @@ export async function apply(ctx: Context) {
|
||||
resolve(c);
|
||||
});
|
||||
});
|
||||
await ctx.plugin(require('../service/api'));
|
||||
require('../lib/index');
|
||||
|
||||
ctx.plugin(require('../service/monitor'));
|
||||
|
||||
@@ -216,8 +216,8 @@ export class ContestProblemListHandler extends ContestDetailBaseHandler {
|
||||
: [Object.fromEntries(psdocs.map((i) => [i.rid, { _id: i.rid }])), []];
|
||||
if (!this.user.own(this.tdoc) && !this.user.hasPerm(PERM.PERM_EDIT_CONTEST)) {
|
||||
this.response.body.rdocs = this.response.body.rdocs.map((rdoc) => contest.applyProjection(this.tdoc, rdoc, this.user));
|
||||
for (const psdoc of psdocs) {
|
||||
this.response.body.rdict[psdoc.rid] = contest.applyProjection(this.tdoc, this.response.body.rdict[psdoc.rid], this.user);
|
||||
for (const key in this.response.body.rdict) {
|
||||
this.response.body.rdict[key] = contest.applyProjection(this.tdoc, this.response.body.rdict[key], this.user);
|
||||
}
|
||||
for (const key in this.response.body.psdict) {
|
||||
this.response.body.psdict[key] = contest.applyProjection(this.tdoc, this.response.body.psdict[key], this.user);
|
||||
@@ -742,33 +742,6 @@ export async function apply(ctx: Context) {
|
||||
}
|
||||
await Promise.all(tasks);
|
||||
});
|
||||
ctx.inject(['api'], ({ api }) => {
|
||||
api.value('Contest', [
|
||||
['_id', 'ObjectID!'],
|
||||
['domainId', 'String!'],
|
||||
['docId', 'ObjectID!'],
|
||||
['owner', 'Int!'],
|
||||
['beginAt', 'Date!'],
|
||||
['title', 'String!'],
|
||||
['content', 'String!'],
|
||||
['beginAt', 'Date!'],
|
||||
['endAt', 'Date!'],
|
||||
['attend', 'Int!'],
|
||||
['pids', '[Int]!'],
|
||||
['rated', 'Boolean!'],
|
||||
]);
|
||||
api.resolver(
|
||||
'Query', 'contest(id: ObjectID!)', 'Contest',
|
||||
async (arg, c) => {
|
||||
c.checkPerm(PERM.PERM_VIEW);
|
||||
arg.id = new ObjectId(arg.id);
|
||||
c.tdoc = await contest.get(c.args.domainId, new ObjectId(arg.id));
|
||||
if (!c.tdoc) throw new ContestNotFoundError(c.args.domainId, arg.id);
|
||||
return c.tdoc;
|
||||
},
|
||||
'Get a contest by ID',
|
||||
);
|
||||
});
|
||||
ctx.plugin(ScoreboardService);
|
||||
ctx.inject(['scoreboard'], ({ Route, scoreboard }) => {
|
||||
Route('contest_scoreboard', '/contest/:tid/scoreboard', ContestScoreboardHandler, PERM.PERM_VIEW_CONTEST_SCOREBOARD);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { load } from 'js-yaml';
|
||||
import { Dictionary } from 'lodash';
|
||||
import moment from 'moment-timezone';
|
||||
import Schema from 'schemastery';
|
||||
import type { Context } from '../context';
|
||||
import {
|
||||
CannotDeleteSystemDomainError, DomainJoinAlreadyMemberError, DomainJoinForbiddenError, ForbiddenError,
|
||||
@@ -17,7 +18,7 @@ import { DOMAIN_SETTINGS, DOMAIN_SETTINGS_BY_KEY } from '../model/setting';
|
||||
import * as system from '../model/system';
|
||||
import user from '../model/user';
|
||||
import {
|
||||
Handler, param, post, query, requireSudo, Types,
|
||||
Handler, Mutation, param, post, Query, query, requireSudo, Types,
|
||||
} from '../service/server';
|
||||
import { log2 } from '../utils';
|
||||
|
||||
@@ -412,6 +413,42 @@ class DomainSearchHandler extends Handler {
|
||||
}
|
||||
}
|
||||
|
||||
export const DomainApi = {
|
||||
domain: Query(
|
||||
Schema.object({
|
||||
id: Schema.string(),
|
||||
}),
|
||||
async (ctx, args) => {
|
||||
const ddoc = args.id ? await domain.get(args.id) : ctx.domain;
|
||||
if (!ddoc) return null;
|
||||
const udoc = await user.getById(ddoc._id, ctx.user._id);
|
||||
if (!udoc.hasPerm(PERM.PERM_VIEW) && !udoc.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN)) return null;
|
||||
return ddoc;
|
||||
},
|
||||
),
|
||||
'domain.group': Mutation(
|
||||
Schema.object({
|
||||
name: Schema.string().required(),
|
||||
uids: Schema.array(Schema.number()),
|
||||
}),
|
||||
async (ctx, args) => {
|
||||
if (!ctx.user.hasPerm(PERM.PERM_EDIT_DOMAIN)) throw new PermissionError(PERM.PERM_EDIT_DOMAIN);
|
||||
if (args.uids) {
|
||||
const res = await user.updateGroup(ctx.domain._id, args.name, args.uids);
|
||||
return res.upsertedCount > 0;
|
||||
}
|
||||
const res = await user.delGroup(ctx.domain._id, args.name);
|
||||
return res.deletedCount > 0;
|
||||
},
|
||||
),
|
||||
} as const;
|
||||
|
||||
declare module '@hydrooj/framework' {
|
||||
interface Apis {
|
||||
domain: typeof DomainApi;
|
||||
}
|
||||
}
|
||||
|
||||
export async function apply(ctx: Context) {
|
||||
ctx.Route('ranking', '/ranking', DomainRankHandler, PERM.PERM_VIEW_RANKING);
|
||||
ctx.Route('domain_dashboard', '/domain/dashboard', DomainDashboardHandler);
|
||||
@@ -424,34 +461,6 @@ export async function apply(ctx: Context) {
|
||||
ctx.Route('domain_join', '/domain/join', DomainJoinHandler, PRIV.PRIV_USER_PROFILE);
|
||||
ctx.Route('domain_search', '/domain/search', DomainSearchHandler, PRIV.PRIV_USER_PROFILE);
|
||||
ctx.inject(['api'], ({ api }) => {
|
||||
api.value('GroupInfo', [
|
||||
['name', 'String!'],
|
||||
['uids', '[Int]!'],
|
||||
]);
|
||||
api.resolver('Query', 'domain(id: String)', 'Domain', async (args, c) => {
|
||||
const ddoc = args.id ? await domain.get(args.id) : c.domain;
|
||||
if (!ddoc) return null;
|
||||
const udoc = await user.getById(ddoc._id, c.user._id);
|
||||
if (!udoc.hasPerm(PERM.PERM_VIEW) && !udoc.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN)) return null;
|
||||
c.udoc = udoc;
|
||||
return ddoc;
|
||||
});
|
||||
api.resolver('Domain', 'manage', 'DomainManage', async (args, c) => {
|
||||
if (!c.udoc.hasPerm(PERM.PERM_EDIT_DOMAIN)) throw new PermissionError(PERM.PERM_EDIT_DOMAIN);
|
||||
return c.parent;
|
||||
});
|
||||
api.resolver('DomainManage', 'group', 'DomainGroup', (args, c) => c.parent);
|
||||
api.resolver(
|
||||
'DomainGroup', 'list(uid: Int)', '[GroupInfo]',
|
||||
(args, c) => user.listGroup(c.parent._id, args.uid),
|
||||
);
|
||||
api.resolver(
|
||||
'DomainGroup', 'update(name: String!, uids: [Int]!)', 'Boolean',
|
||||
async (args, c) => !!(await user.updateGroup(c.parent._id, args.name, args.uids)).upsertedCount,
|
||||
);
|
||||
api.resolver(
|
||||
'DomainGroup', 'del(name: String!)', 'Boolean',
|
||||
async (args, c) => !!(await user.delGroup(c.parent._id, args.name)).deletedCount,
|
||||
);
|
||||
api.provide(DomainApi);
|
||||
});
|
||||
}
|
||||
@@ -307,6 +307,8 @@ class HomeSecurityHandler extends Handler {
|
||||
type: 'public-key',
|
||||
})),
|
||||
authenticatorSelection: {
|
||||
residentKey: 'required',
|
||||
userVerification: 'preferred',
|
||||
authenticatorAttachment: type,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -321,9 +321,12 @@ const allPriv = Math.sum(Object.values(Priv));
|
||||
|
||||
class SystemUserPrivHandler extends SystemHandler {
|
||||
@requireSudo
|
||||
async get() {
|
||||
@param('extraIgnore', Types.NumericArray, true)
|
||||
async get({ }, extraIgnore: number[] = []) {
|
||||
const defaultPriv = system.get('default.priv');
|
||||
const udocs = await user.getMulti({ _id: { $gte: -1000, $ne: 1 }, priv: { $nin: [0, defaultPriv] } }).limit(1000).sort({ _id: 1 }).toArray();
|
||||
const udocs = await user.getMulti({
|
||||
_id: { $gte: -1000, $ne: 1 }, priv: { $nin: [0, defaultPriv, ...extraIgnore] },
|
||||
}).limit(1000).sort({ _id: 1 }).toArray();
|
||||
const banudocs = await user.getMulti({ _id: { $gte: -1000, $ne: 1 }, priv: 0 }).limit(1000).sort({ _id: 1 }).toArray();
|
||||
this.response.body = {
|
||||
udocs: [...udocs, ...banudocs],
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { Filter, ObjectId } from 'mongodb';
|
||||
import { nanoid } from 'nanoid';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import Schema from 'schemastery';
|
||||
import parser from '@hydrooj/utils/lib/search';
|
||||
import { sortFiles, streamToBuffer } from '@hydrooj/utils/lib/utils';
|
||||
import type { Context } from '../context';
|
||||
@@ -34,7 +35,7 @@ import storage from '../model/storage';
|
||||
import * as system from '../model/system';
|
||||
import user from '../model/user';
|
||||
import {
|
||||
Handler, param, post, query, route, Types,
|
||||
Handler, param, post, Query, query, route, Types,
|
||||
} from '../service/server';
|
||||
import { ContestDetailBaseHandler } from './contest';
|
||||
|
||||
@@ -1021,6 +1022,38 @@ export class ProblemCreateHandler extends Handler {
|
||||
}
|
||||
}
|
||||
|
||||
export const ProblemApi = {
|
||||
problem: Query(
|
||||
Schema.object({
|
||||
id: Schema.union([Schema.number().step(1), Schema.string()]).required(),
|
||||
domainId: Schema.string().required(),
|
||||
}),
|
||||
async (ctx, args) => {
|
||||
const pdoc = await problem.get(args.domainId, args.id);
|
||||
if (!pdoc) return null;
|
||||
if (pdoc.hidden) ctx.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
|
||||
return pdoc;
|
||||
},
|
||||
),
|
||||
problems: Query(
|
||||
Schema.object({
|
||||
ids: Schema.array(Schema.number().step(1)).required(),
|
||||
domainId: Schema.string().required(),
|
||||
}),
|
||||
async (ctx, args) => {
|
||||
const pdocs = await problem.getList(args.domainId, args.ids, ctx.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN) || ctx.user._id,
|
||||
undefined, undefined, true);
|
||||
return Object.keys(pdocs).map((id) => pdocs[+id]);
|
||||
},
|
||||
),
|
||||
} as const;
|
||||
|
||||
declare module '@hydrooj/framework' {
|
||||
interface Apis {
|
||||
problem: typeof ProblemApi;
|
||||
}
|
||||
}
|
||||
|
||||
export async function apply(ctx: Context) {
|
||||
ctx.Route('problem_main', '/p', ProblemMainHandler, PERM.PERM_VIEW_PROBLEM);
|
||||
ctx.Route('problem_random', '/problem/random', ProblemRandomHandler, PERM.PERM_VIEW_PROBLEM);
|
||||
@@ -1038,64 +1071,6 @@ export async function apply(ctx: Context) {
|
||||
ctx.Route('problem_statistics', '/p/:pid/stat', ProblemStatisticsHandler, PERM.PERM_VIEW_PROBLEM);
|
||||
ctx.Route('problem_create', '/problem/create', ProblemCreateHandler, PERM.PERM_CREATE_PROBLEM);
|
||||
ctx.inject(['api'], ({ api }) => {
|
||||
api.value('FileInfo', [
|
||||
['_id', 'String!'],
|
||||
['name', 'String!'],
|
||||
['size', 'Int'],
|
||||
['lastModified', 'Date'],
|
||||
]);
|
||||
api.value('Problem', [
|
||||
['_id', 'ObjectID!'],
|
||||
['owner', 'Int!'],
|
||||
['domainId', 'String!'],
|
||||
['docId', 'Int!'],
|
||||
['docType', 'Int!'],
|
||||
['pid', 'String'],
|
||||
['title', 'String!'],
|
||||
['content', 'String!'],
|
||||
['data', '[FileInfo]'],
|
||||
['additional_file', '[FileInfo]'],
|
||||
['nSubmit', 'Int'],
|
||||
['nAccept', 'Int'],
|
||||
['difficulty', 'Int'],
|
||||
['tag', '[String]'],
|
||||
['hidden', 'Boolean'],
|
||||
]);
|
||||
api.resolver(
|
||||
'Query', 'problem(id: Int, pid: String)', 'Problem',
|
||||
async (arg, c) => {
|
||||
c.checkPerm(PERM.PERM_VIEW);
|
||||
const pdoc = await problem.get(c.args.domainId, arg.pid || arg.id);
|
||||
if (!pdoc) return null;
|
||||
if (pdoc.hidden) c.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
|
||||
c.pdoc = pdoc;
|
||||
return pdoc;
|
||||
},
|
||||
);
|
||||
api.resolver('Query', 'problems(ids: [Int])', '[Problem]', async (arg, c) => {
|
||||
c.checkPerm(PERM.PERM_VIEW);
|
||||
const res = await problem.getList(c.args.domainId, arg.ids, c.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN) || c.user._id,
|
||||
undefined, undefined, true);
|
||||
return Object.keys(res).map((id) => res[+id]);
|
||||
}, 'Get a list of problem by ids');
|
||||
api.resolver(
|
||||
'Problem', 'manage', 'ProblemManage',
|
||||
(arg, c) => {
|
||||
if (!c.user.own(c.pdoc, PERM.PERM_EDIT_PROBLEM_SELF)) c.checkPerm(PERM.PERM_EDIT_PROBLEM);
|
||||
return {};
|
||||
},
|
||||
);
|
||||
api.resolver(
|
||||
'ProblemManage', 'delete', 'Boolean!',
|
||||
async (arg, c) => {
|
||||
const tdocs = await contest.getRelated(c.args.domainId, c.pdoc.docId);
|
||||
if (tdocs.length) throw new ProblemAlreadyUsedByContestError(c.pdoc.docId, tdocs[0]._id);
|
||||
return problem.del(c.pdoc.domainId, c.pdoc.docId);
|
||||
},
|
||||
);
|
||||
api.resolver(
|
||||
'ProblemManage', 'edit(title: String, content: String, tag: [String], hidden: Boolean)', 'Problem!',
|
||||
(arg, c) => problem.edit(c.args.domainId, c.pdoc.docId, arg),
|
||||
);
|
||||
api.provide(ProblemApi);
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { generateAuthenticationOptions, verifyAuthenticationResponse } from '@simplewebauthn/server';
|
||||
import { isoBase64URL } from '@simplewebauthn/server/helpers';
|
||||
import moment from 'moment-timezone';
|
||||
import type { Binary } from 'mongodb';
|
||||
import { Binary } from 'mongodb';
|
||||
import Schema from 'schemastery';
|
||||
import type { Context } from '../context';
|
||||
import {
|
||||
AuthOperationError, BadRequestError, BlacklistedError, BuiltinLoginError,
|
||||
@@ -25,7 +26,7 @@ import * as system from '../model/system';
|
||||
import token from '../model/token';
|
||||
import user, { deleteUserCache } from '../model/user';
|
||||
import {
|
||||
Handler, param, post, Types,
|
||||
Handler, param, post, Query, Types,
|
||||
} from '../service/server';
|
||||
|
||||
async function successfulAuth(this: Handler, udoc: User) {
|
||||
@@ -147,28 +148,41 @@ class UserWebauthnHandler extends Handler {
|
||||
}
|
||||
|
||||
@param('uname', Types.Username, true)
|
||||
async get(domainId: string, uname: string) {
|
||||
const udoc = this.user._id ? this.user : ((await user.getByEmail(domainId, uname)) || await user.getByUname(domainId, uname));
|
||||
if (!udoc._id) throw new UserNotFoundError(uname || 'user');
|
||||
if (!udoc.authn) throw new AuthOperationError('authn', 'disabled');
|
||||
const options = await generateAuthenticationOptions({
|
||||
allowCredentials: udoc._authenticators.map((authenticator) => ({
|
||||
@param('login', Types.Boolean)
|
||||
async get(domainId: string, uname: string, login: boolean) {
|
||||
let allowCredentials = [];
|
||||
let uid = 0;
|
||||
if (!login) {
|
||||
const udoc = this.user._id ? this.user : ((await user.getByEmail(domainId, uname)) || await user.getByUname(domainId, uname));
|
||||
if (!udoc._id) throw new UserNotFoundError(uname || 'user');
|
||||
if (!udoc.authn) throw new AuthOperationError('authn', 'disabled');
|
||||
allowCredentials = udoc._authenticators.map((authenticator) => ({
|
||||
id: isoBase64URL.fromBuffer(authenticator.credentialID.buffer),
|
||||
})),
|
||||
}));
|
||||
uid = udoc._id;
|
||||
}
|
||||
const options = await generateAuthenticationOptions({
|
||||
allowCredentials,
|
||||
rpID: this.getAuthnHost(),
|
||||
userVerification: 'preferred',
|
||||
});
|
||||
await token.add(token.TYPE_WEBAUTHN, 60, { uid: udoc._id }, options.challenge);
|
||||
await token.add(token.TYPE_WEBAUTHN, 60, { uid: login ? 'login' : uid }, options.challenge);
|
||||
this.session.challenge = options.challenge;
|
||||
this.response.body.authOptions = options;
|
||||
}
|
||||
|
||||
async post({ domainId, result }) {
|
||||
async post({ domainId, result, redirect }) {
|
||||
const challenge = this.session.challenge;
|
||||
if (!challenge) throw new ForbiddenError();
|
||||
const tdoc = await token.get(challenge, token.TYPE_WEBAUTHN);
|
||||
if (!tdoc) throw new InvalidTokenError(token.TYPE_TEXTS[token.TYPE_WEBAUTHN]);
|
||||
const udoc = await user.getById(domainId, tdoc.uid);
|
||||
const udoc = await (tdoc.uid === 'login'
|
||||
? (async () => {
|
||||
const u = await user.coll.findOne({ 'authenticators.credentialID': Binary.createFromBase64(result.id) });
|
||||
return u ? await user.getById(domainId, u._id) : null;
|
||||
})()
|
||||
: user.getById(domainId, tdoc.uid));
|
||||
if (!udoc) throw new NotFoundError();
|
||||
const parseId = (id: Binary) => Buffer.from(id.toString('hex'), 'hex').toString('base64url');
|
||||
const authenticator = udoc._authenticators?.find((c) => parseId(c.credentialID) === result.id);
|
||||
if (!authenticator) throw new ValidationError('authenticator');
|
||||
@@ -186,8 +200,15 @@ class UserWebauthnHandler extends Handler {
|
||||
if (!verification?.verified) throw new ValidationError('authenticator');
|
||||
authenticator.counter = verification.authenticationInfo.newCounter;
|
||||
await user.setById(udoc._id, { authenticators: udoc._authenticators });
|
||||
await token.update(challenge, token.TYPE_WEBAUTHN, 60, { verified: true });
|
||||
this.back();
|
||||
if (tdoc.uid === 'login') {
|
||||
await successfulAuth.call(this, await user.getById(domainId, udoc._id));
|
||||
await token.del(challenge, token.TYPE_WEBAUTHN);
|
||||
this.response.redirect = redirect || ((this.request.referer || '/login').endsWith('/login')
|
||||
? this.url('homepage') : this.request.referer);
|
||||
} else {
|
||||
await token.update(challenge, token.TYPE_WEBAUTHN, 60, { verified: true });
|
||||
this.back();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,6 +536,67 @@ class ContestModeHandler extends Handler {
|
||||
|
||||
export const inject = ['oauth'];
|
||||
|
||||
const UserApi = {
|
||||
user: Query(Schema.object({
|
||||
id: Schema.number().step(1),
|
||||
uname: Schema.string(),
|
||||
mail: Schema.string(),
|
||||
domainId: Schema.string().required(),
|
||||
}), (c, arg) => {
|
||||
if (arg.id) return user.getById(arg.domainId, arg.id);
|
||||
if (arg.mail) return user.getByEmail(arg.domainId, arg.mail);
|
||||
if (arg.uname) return user.getByUname(arg.domainId, arg.uname);
|
||||
return user.getById(arg.domainId, c.user._id);
|
||||
}),
|
||||
users: Query(Schema.object({
|
||||
ids: Schema.array(Schema.number().step(1)),
|
||||
auto: Schema.array(Schema.string()),
|
||||
search: Schema.string(),
|
||||
limit: Schema.number().step(1),
|
||||
exact: Schema.boolean(),
|
||||
}), async (c, arg) => {
|
||||
const auto = (arg.ids?.length && arg.ids) || arg.auto || [];
|
||||
if (auto.length) {
|
||||
const maybeId = auto.filter((i) => !Number.isNaN(+i));
|
||||
const result = [];
|
||||
if (maybeId.length) {
|
||||
const udocs = await user.getList(arg.domainId, maybeId.map((i) => +i));
|
||||
for (const i in udocs) udocs[i].avatarUrl = avatar(udocs[i].avatar);
|
||||
result.push(...Object.values(udocs));
|
||||
}
|
||||
const notFound = auto.filter((i) => !result.find((j) => j._id === +i));
|
||||
if (notFound.length > 50) return result; // reject if too many
|
||||
for (const i of notFound) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const udoc = await user.getByUname(arg.domainId, i.toString()) || await user.getByEmail(arg.domainId, i.toString());
|
||||
if (udoc) result.push(udoc);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (!arg.search) return [];
|
||||
const udoc = await user.getById(arg.domainId, +arg.search)
|
||||
|| await user.getByUname(arg.domainId, arg.search)
|
||||
|| await user.getByEmail(arg.domainId, arg.search);
|
||||
const udocs: User[] = arg.exact
|
||||
? []
|
||||
: await user.getPrefixList(arg.domainId, arg.search, Math.min(arg.limit || 10, 10));
|
||||
if (udoc && !udocs.find((i) => i._id === udoc._id)) {
|
||||
udocs.pop();
|
||||
udocs.unshift(udoc);
|
||||
}
|
||||
for (const i in udocs) {
|
||||
udocs[i].avatarUrl = avatar(udocs[i].avatar);
|
||||
}
|
||||
return udocs;
|
||||
}),
|
||||
} as const;
|
||||
|
||||
declare module '@hydrooj/framework' {
|
||||
interface Apis {
|
||||
user: typeof UserApi;
|
||||
}
|
||||
}
|
||||
|
||||
export async function apply(ctx: Context) {
|
||||
ctx.Route('user_login', '/login', UserLoginHandler);
|
||||
ctx.Route('user_oauth', '/oauth/:type', OauthHandler);
|
||||
@@ -544,60 +626,6 @@ export async function apply(ctx: Context) {
|
||||
},
|
||||
});
|
||||
ctx.inject(['api'], ({ api }) => {
|
||||
api.value('User', [
|
||||
['_id', 'Int!'],
|
||||
['uname', 'String!'],
|
||||
['mail', 'String!'],
|
||||
['perm', 'String'],
|
||||
['role', 'String'],
|
||||
['loginat', 'Date'],
|
||||
['regat', 'Date!'],
|
||||
['priv', 'Int!', 'User Privilege'],
|
||||
['avatarUrl', 'String'],
|
||||
['displayName', 'String @if(perm: "PERM_VIEW_DISPLAYNAME")'],
|
||||
['rpInfo', 'JSONObject'],
|
||||
]);
|
||||
api.resolver('Query', 'user(id: Int, uname: String, mail: String)', 'User', (arg, c) => {
|
||||
if (arg.id) return user.getById(c.args.domainId, arg.id);
|
||||
if (arg.mail) return user.getByEmail(c.args.domainId, arg.mail);
|
||||
if (arg.uname) return user.getByUname(c.args.domainId, arg.uname);
|
||||
return c.user;
|
||||
}, `Get a user by id, uname, or mail.
|
||||
Returns current user if no argument is provided.`);
|
||||
api.resolver('Query', 'users(ids: [Int], auto: [String], search: String, limit: Int, exact: Boolean)', '[User]', async (arg, c) => {
|
||||
const auto = arg.ids || arg.auto || [];
|
||||
if (auto.length) {
|
||||
const maybeId = auto.filter((i) => !Number.isNaN(+i));
|
||||
const result = [];
|
||||
if (maybeId.length) {
|
||||
const udocs = await user.getList(c.args.domainId, maybeId.map((i) => +i));
|
||||
for (const i in udocs) udocs[i].avatarUrl = avatar(udocs[i].avatar);
|
||||
result.push(...Object.values(udocs));
|
||||
}
|
||||
const notFound = auto.filter((i) => !result.find((j) => j._id === +i));
|
||||
if (notFound.length > 50) return result; // reject if too many
|
||||
for (const i of notFound) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const udoc = await user.getByUname(c.args.domainId, i) || await user.getByEmail(c.args.domainId, i);
|
||||
if (udoc) result.push(udoc);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (!arg.search) return [];
|
||||
const udoc = await user.getById(c.args.domainId, +arg.search)
|
||||
|| await user.getByUname(c.args.domainId, arg.search)
|
||||
|| await user.getByEmail(c.args.domainId, arg.search);
|
||||
const udocs: User[] = arg.exact
|
||||
? []
|
||||
: await user.getPrefixList(c.args.domainId, arg.search, Math.min(arg.limit || 10, 10));
|
||||
if (udoc && !udocs.find((i) => i._id === udoc._id)) {
|
||||
udocs.pop();
|
||||
udocs.unshift(udoc);
|
||||
}
|
||||
for (const i in udocs) {
|
||||
udocs[i].avatarUrl = avatar(udocs[i].avatar);
|
||||
}
|
||||
return udocs;
|
||||
}, 'Get a list of user by ids, or search users with the prefix.');
|
||||
api.provide(UserApi);
|
||||
});
|
||||
}
|
||||
@@ -784,7 +784,7 @@ export async function add(
|
||||
RULES[rule].check(data);
|
||||
await bus.parallel('contest/before-add', data);
|
||||
const docId = await document.add(domainId, content, owner, document.TYPE_CONTEST, null, null, null, {
|
||||
...data, title, rule, beginAt, endAt, pids, attend: 0, rated,
|
||||
assign: [], ...data, title, rule, beginAt, endAt, pids, attend: 0, rated,
|
||||
});
|
||||
await bus.parallel('contest/add', data, docId);
|
||||
return docId;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ObjectId } from 'mongodb';
|
||||
import { OplogDoc } from '../interface';
|
||||
import bus from '../service/bus';
|
||||
import db from '../service/db';
|
||||
import type { Handler } from '../service/server';
|
||||
import type { ConnectionHandler, Handler } from '../service/server';
|
||||
|
||||
export const coll = db.collection('oplog');
|
||||
|
||||
@@ -37,7 +37,7 @@ function safeKeys(data: any) {
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function log<T extends Handler>(handler: T, type: string, data: any) {
|
||||
export async function log<T extends Handler | ConnectionHandler>(handler: T, type: string, data: any) {
|
||||
const args = safeKeys(cloneDeep(handler.args));
|
||||
await bus.parallel('oplog/log', type, handler, args, data);
|
||||
const res = await coll.insertOne({
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
/* eslint-disable max-len */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import fs from 'fs';
|
||||
import * as cordis from 'cordis';
|
||||
import yaml from 'js-yaml';
|
||||
import { Dictionary } from 'lodash';
|
||||
import moment from 'moment-timezone';
|
||||
import Schema from 'schemastery';
|
||||
import { LangConfig, parseLang } from '@hydrooj/common';
|
||||
import { retry } from '@hydrooj/utils';
|
||||
import { findFileSync, retry } from '@hydrooj/utils';
|
||||
import { Context, Service } from '../context';
|
||||
import { Setting as _Setting } from '../interface';
|
||||
import { Logger } from '../logger';
|
||||
@@ -14,6 +15,8 @@ import * as builtin from './builtin';
|
||||
|
||||
type SettingDict = Dictionary<_Setting>;
|
||||
|
||||
const settingFile = yaml.load(fs.readFileSync(findFileSync('hydrooj/setting.yaml'), 'utf-8')) as any;
|
||||
|
||||
const logger = new Logger('model/setting');
|
||||
const countries = moment.tz.countries();
|
||||
const tzs = new Set();
|
||||
@@ -87,7 +90,8 @@ function schemaToSettings(schema: Schema<any>) {
|
||||
| (s.meta?.disabled ? FLAG_DISABLED : 0);
|
||||
const type = s.type === 'number' ? 'number'
|
||||
: s.type === 'boolean' ? 'checkbox'
|
||||
: s.meta?.role === 'textarea' ? 'textarea' : 'text';
|
||||
: s.meta?.role === 'markdown' ? 'markdown'
|
||||
: s.meta?.role === 'textarea' ? 'textarea' : 'text';
|
||||
if (s.meta?.role === 'password') flag |= FLAG_SECRET;
|
||||
const options = {};
|
||||
for (const item of s.list || []) {
|
||||
@@ -344,6 +348,8 @@ SystemSetting(
|
||||
Setting('setting_basic', 'pagination.solution', 20, 'number', 'pagination.solution', 'Solutions per page'),
|
||||
Setting('setting_basic', 'pagination.training', 10, 'number', 'pagination.training', 'Trainings per page'),
|
||||
Setting('setting_basic', 'pagination.reply', 50, 'number', 'pagination.reply', 'Replies per page'),
|
||||
Setting('setting_basic', 'hydrooj.homepage', settingFile.homepage.default, 'yaml', 'hydrooj.homepage', 'Homepage config'),
|
||||
Setting('setting_basic', 'hydrooj.langs', settingFile.langs.default, 'yaml', 'hydrooj.langs', 'Language config'),
|
||||
Setting('setting_session', 'session.keys', [String.random(32)], 'text', 'session.keys', 'session.keys', FLAG_HIDDEN),
|
||||
Setting('setting_session', 'session.domain', '', 'text', 'session.domain', 'session.domain', FLAG_HIDDEN),
|
||||
Setting('setting_session', 'session.saved_expire_seconds', 3600 * 24 * 30,
|
||||
|
||||
@@ -55,4 +55,3 @@ export { db, pwsh };
|
||||
|
||||
// to load services into to context
|
||||
export { } from './handler/contest';
|
||||
export { } from './service/api';
|
||||
@@ -1,202 +0,0 @@
|
||||
import { makeExecutableSchema } from '@graphql-tools/schema';
|
||||
import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils';
|
||||
import { defaultFieldResolver, graphql, GraphQLSchema } from 'graphql';
|
||||
import { resolvers, typeDefs } from 'graphql-scalars';
|
||||
import { debounce } from 'lodash';
|
||||
import { Context, Service } from '../context';
|
||||
import { PERM, PRIV } from '../model/builtin';
|
||||
import { Handler } from './server';
|
||||
|
||||
const types: Record<string, Record<string, string>> = Object.create(null);
|
||||
const unions: Record<string, string> = Object.create(null);
|
||||
const descriptions: Record<string, Record<string, string>> = Object.create(null);
|
||||
const handlers: Record<string, Record<string, any>> = {
|
||||
Query: Object.create(null),
|
||||
};
|
||||
const root: Record<string, any> = handlers.Query;
|
||||
|
||||
interface ApiContext extends ApiHandler {
|
||||
[key: string]: any;
|
||||
}
|
||||
declare module '../context' {
|
||||
interface Context {
|
||||
api: ApiService;
|
||||
}
|
||||
}
|
||||
|
||||
function setDescription(desc: string) {
|
||||
if (desc.includes('\n')) return ['"""', desc, '"""'].join('\n');
|
||||
return JSON.stringify(desc);
|
||||
}
|
||||
|
||||
let schema: GraphQLSchema;
|
||||
let schemaStr = '';
|
||||
|
||||
const applyAuthDirective = (s) => mapSchema(s, {
|
||||
// eslint-disable-next-line consistent-return
|
||||
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
|
||||
const authDirective = getDirective(s, fieldConfig, 'auth')?.[0];
|
||||
if (authDirective) {
|
||||
const { resolve = defaultFieldResolver } = fieldConfig;
|
||||
return {
|
||||
...fieldConfig,
|
||||
async resolve(source, args, context, info) {
|
||||
if (authDirective.perm) {
|
||||
const perm = PERM[authDirective.perm];
|
||||
if (!context.user.hasPerm(perm)) throw new Error(`Permission denied: ${authDirective.perm}`);
|
||||
}
|
||||
if (authDirective.priv) {
|
||||
const priv = PRIV[authDirective.priv];
|
||||
if (!context.user.hasPriv(priv)) throw new Error(`Permission denied: ${authDirective.priv}`);
|
||||
}
|
||||
return await resolve(source, args, context, info);
|
||||
},
|
||||
};
|
||||
}
|
||||
const ifDirective = getDirective(s, fieldConfig, 'if')?.[0];
|
||||
if (ifDirective) {
|
||||
const { resolve = defaultFieldResolver } = fieldConfig;
|
||||
return {
|
||||
...fieldConfig,
|
||||
async resolve(source, args, context, info) {
|
||||
if (ifDirective.perm && !context.user.hasPerm(PERM[ifDirective.perm])) return null;
|
||||
if (ifDirective.priv && !context.user.hasPriv(PRIV[ifDirective.priv])) return null;
|
||||
return await resolve(source, args, context, info);
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
class ApiHandler extends Handler {
|
||||
category = '#api';
|
||||
// FIXME: adding PERM_VIEW check will break omnibar search
|
||||
|
||||
query(q: string, variables: any) {
|
||||
return graphql({
|
||||
schema,
|
||||
source: q,
|
||||
rootValue: root,
|
||||
contextValue: this,
|
||||
variableValues: variables,
|
||||
});
|
||||
}
|
||||
|
||||
async get() {
|
||||
const q = decodeURIComponent(this.request.querystring);
|
||||
if (q === 'schema') {
|
||||
this.response.type = 'application/json';
|
||||
this.response.body = { schema: schemaStr };
|
||||
} else if (q) {
|
||||
this.response.type = 'application/json';
|
||||
this.response.body = await this.query(q, {});
|
||||
} else this.response.template = 'api.html';
|
||||
}
|
||||
|
||||
async post() {
|
||||
this.response.type = 'application/json';
|
||||
this.response.body = await this.query(this.args.query, this.args.variables);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO support dispose
|
||||
class ApiService extends Service {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'api');
|
||||
this.rebuild = debounce(this.rebuild.bind(this), 500, { trailing: true });
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
private rebuild() {
|
||||
try {
|
||||
const defs = [
|
||||
...Object.keys(unions).map((i) => `union ${i} = ${unions[i]}`),
|
||||
'directive @auth(perm: String, priv: String) on FIELD_DEFINITION',
|
||||
'directive @if(perm: String, priv: String) on FIELD_DEFINITION',
|
||||
...typeDefs,
|
||||
...Object.keys(types).map((key) => {
|
||||
let def = '';
|
||||
if (descriptions[key]?._description) def += `${setDescription(descriptions[key]._description)}\n`;
|
||||
def += `type ${key} {\n`;
|
||||
for (const k in types[key]) {
|
||||
if (descriptions[key]?.[k]) def += ` ${setDescription(descriptions[key][k])}\n`;
|
||||
def += ` ${k}: ${types[key][k]}\n`;
|
||||
}
|
||||
def += '}\n';
|
||||
return def;
|
||||
}),
|
||||
];
|
||||
schema = makeExecutableSchema({
|
||||
typeDefs: defs,
|
||||
resolvers,
|
||||
});
|
||||
schema = applyAuthDirective(schema);
|
||||
schemaStr = defs.join('\n');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
this.ctx.emit('api/update');
|
||||
}
|
||||
|
||||
value(typeName: string, key: string, value: string, description?: string): void;
|
||||
value(typeName: string, vals: [string, string, string?][]): void;
|
||||
value(typeName: string, arg1: [string, string, string?][] | string, value?: string, description?: string) {
|
||||
if (typeof arg1 === 'string') arg1 = [[arg1, value!, description]];
|
||||
for (const [k, v, d] of arg1) {
|
||||
if (!types[typeName]) types[typeName] = { [k]: v };
|
||||
else types[typeName][k] = v;
|
||||
if (d) {
|
||||
if (!descriptions[typeName]) descriptions[typeName] = { [k]: d };
|
||||
else descriptions[typeName][k] = d;
|
||||
}
|
||||
}
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
resolver(
|
||||
typeName: string, key: string, value: string,
|
||||
func: (args: any, ctx: ApiContext, info: any) => any,
|
||||
description?: string,
|
||||
) {
|
||||
this.value(typeName, key, value, description);
|
||||
const wrappedFunc = async (arg, ctx, info) => {
|
||||
const res = await func(arg, ctx, info);
|
||||
if (typeof res !== 'object' || res === null) return res;
|
||||
let node = value.includes('!') ? value.split('!')[0] : value;
|
||||
const isArray = node.includes('[');
|
||||
node = node.replace('[', '').replace(']', '');
|
||||
if (handlers[node]) {
|
||||
if (!isArray) Object.assign(res, handlers[node]);
|
||||
else if (res instanceof Array) for (const i of res) { Object.assign(i, handlers[node]); }
|
||||
}
|
||||
ctx.parent = res;
|
||||
return res;
|
||||
};
|
||||
handlers[typeName] ||= {};
|
||||
handlers[typeName][key.split('(')[0].trim()] = wrappedFunc;
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
union(typeName: string, ...unionTypes: string[]) {
|
||||
unions[typeName] = unionTypes.join(' | ');
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
query(query: string, args: Record<string, any>) {
|
||||
return graphql({
|
||||
schema,
|
||||
source: query,
|
||||
rootValue: root,
|
||||
contextValue: this,
|
||||
variableValues: args,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const sideEffect = true;
|
||||
export const inject = ['server', 'setting'];
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.plugin(ApiService);
|
||||
ctx.Route('api', '/api', ApiHandler);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
BSON, Db, Filter, ObjectId, OnlyFieldsOfType,
|
||||
} from 'mongodb';
|
||||
import type { Handler } from '@hydrooj/framework';
|
||||
import type { ConnectionHandler, Handler } from '@hydrooj/framework';
|
||||
import pm2 from '@hydrooj/utils/lib/locate-pm2';
|
||||
import { Context } from '../context';
|
||||
import type {
|
||||
@@ -84,7 +84,7 @@ export interface EventMap {
|
||||
'contest/balloon': (domainId: string, tid: ObjectId, bdoc: ContestBalloonDoc) => VoidReturn
|
||||
'contest/del': (domainId: string, tid: ObjectId) => VoidReturn
|
||||
|
||||
'oplog/log': (type: string, handler: Handler<Context>, args: any, data: any) => VoidReturn;
|
||||
'oplog/log': (type: string, handler: Handler<Context> | ConnectionHandler<Context>, args: any, data: any) => VoidReturn;
|
||||
|
||||
'training/list': (query: Filter<TrainingDoc>, handler: any) => VoidReturn
|
||||
'training/get': (tdoc: TrainingDoc, handler: any) => VoidReturn
|
||||
|
||||
@@ -4,7 +4,7 @@ import fs from 'fs-extra';
|
||||
import cache from 'koa-static-cache';
|
||||
import { type FindCursor, ObjectId } from 'mongodb';
|
||||
import {
|
||||
ConnectionHandler as ConnectionHandlerOriginal,
|
||||
applyApiHandler, ConnectionHandler as ConnectionHandlerOriginal,
|
||||
Handler as HandlerOriginal, HydroError, NotFoundError, UserFacingError,
|
||||
WebService,
|
||||
} from '@hydrooj/framework';
|
||||
@@ -41,6 +41,7 @@ declare module '@hydrooj/framework' {
|
||||
|
||||
export * from '@hydrooj/framework/decorators';
|
||||
export * from '@hydrooj/framework/validator';
|
||||
export { Query, Mutation } from '@hydrooj/framework/api';
|
||||
|
||||
/*
|
||||
* For security concern, some API requires sudo privilege to access.
|
||||
@@ -90,7 +91,8 @@ export async function apply(ctx: Context) {
|
||||
xhost: system.get('server.xhost'),
|
||||
});
|
||||
if (process.env.HYDRO_CLI) return;
|
||||
ctx.inject(['server', 'oauth'], ({ server, on, oauth }) => {
|
||||
ctx.inject(['server', 'oauth', 'setting'], (childContext) => {
|
||||
const { server, on, oauth } = childContext;
|
||||
server.addHandlerLayer('init', async (c, next) => {
|
||||
const init = Date.now();
|
||||
try {
|
||||
@@ -106,6 +108,9 @@ export async function apply(ctx: Context) {
|
||||
}
|
||||
});
|
||||
|
||||
applyApiHandler(childContext, 'api', '/api/:op');
|
||||
server.setDefaultContext(childContext);
|
||||
|
||||
for (const addon of [...Object.values(global.addons)].reverse()) {
|
||||
const dir = resolve(addon, 'public');
|
||||
if (!fs.existsSync(dir)) continue;
|
||||
@@ -194,7 +199,7 @@ export async function apply(ctx: Context) {
|
||||
await opcount.inc(op, id, periodSecs, maxOperations);
|
||||
},
|
||||
renderTitle(str: string) {
|
||||
const name = this.ctx.setting.get('server.name');
|
||||
const name = this.ctx.get('setting')?.get('server.name') || system.get('server.name');
|
||||
if (this.UiContext.extraTitleContent) return `${this.UiContext.extraTitleContent} - ${this.translate(str)} - ${name}`;
|
||||
return `${this.translate(str)} - ${name}`;
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import cac from 'cac';
|
||||
import moment from 'moment-timezone';
|
||||
import { sleep } from '@hydrooj/utils';
|
||||
import { Context, Service } from '../context';
|
||||
@@ -9,6 +10,8 @@ declare module '../context' {
|
||||
}
|
||||
}
|
||||
|
||||
const argv = cac().parse();
|
||||
|
||||
export default class WorkerService extends Service {
|
||||
private handlers: Record<string, Function> = {};
|
||||
consuming = true;
|
||||
@@ -20,7 +23,8 @@ export default class WorkerService extends Service {
|
||||
super(ctx, 'worker');
|
||||
}
|
||||
|
||||
async *[Context.init]() {
|
||||
*[Service.init]() {
|
||||
if (argv.options.disableWorker) return;
|
||||
this.consume();
|
||||
yield () => {
|
||||
this.consuming = false;
|
||||
|
||||
@@ -603,4 +603,16 @@ export const coreScripts: MigrationScript[] = [
|
||||
});
|
||||
return true;
|
||||
},
|
||||
async function _93_94(ctx) {
|
||||
await ctx.inject(['oauth'], async (c) => {
|
||||
await c.oauth.coll.deleteMany({ _id: { $type: 'string' } });
|
||||
const docs = await c.oauth.coll.find({ id: { $type: 'number' as any } }).toArray();
|
||||
const op = c.oauth.coll.initializeUnorderedBulkOp();
|
||||
for (const doc of docs) {
|
||||
op.find({ _id: doc._id }).updateOne({ $set: { id: doc.id.toString() } });
|
||||
}
|
||||
if (op.length) await op.execute();
|
||||
});
|
||||
return true;
|
||||
},
|
||||
];
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/import-hoj",
|
||||
"version": "0.0.6-beta.0",
|
||||
"version": "0.0.6-beta.1",
|
||||
"description": "Import HOJ problem export",
|
||||
"main": "index.ts",
|
||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/import-qduoj",
|
||||
"version": "1.6.0-beta.0",
|
||||
"version": "1.6.0-beta.1",
|
||||
"description": "Import QDUOJ problem export",
|
||||
"main": "index.ts",
|
||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||
|
||||
@@ -47,7 +47,7 @@ export default class LoginWithGithubService extends Service {
|
||||
.set('Accept', 'application/vnd.github.v3+json')
|
||||
.set('Authorization', `token ${t}`);
|
||||
const ret = {
|
||||
_id: userInfo.body.id,
|
||||
_id: userInfo.body.id.toString(),
|
||||
email: userInfo.body.email,
|
||||
bio: userInfo.body.bio,
|
||||
uname: [userInfo.body.name, userInfo.body.login].filter((i) => i),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/login-with-github",
|
||||
"version": "0.3.0-beta.0",
|
||||
"version": "0.3.0-beta.1",
|
||||
"main": "index.ts",
|
||||
"repository": "git@github.com:hydro-dev/Hydro.git",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
|
||||
@@ -60,7 +60,7 @@ export default class GoogleOAuthService extends Service {
|
||||
await TokenModel.del(state, TokenModel.TYPE_OAUTH);
|
||||
this.response.redirect = s.redirect;
|
||||
return {
|
||||
_id: payload.sub,
|
||||
_id: payload.sub.toString(),
|
||||
email: payload.email,
|
||||
uname: [`${payload.given_name} ${payload.family_name}`, payload.name],
|
||||
viewLang: payload.locale.replace('-', '_'),
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@hydrooj/migrate",
|
||||
"version": "0.5.0-beta.0",
|
||||
"version": "0.5.0-beta.1",
|
||||
"main": "index.ts",
|
||||
"repository": "git@github.com:hydro-dev/Hydro.git",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"mariadb": "^3.4.1",
|
||||
"mongodb": "^6.15.0",
|
||||
"mariadb": "^3.4.2",
|
||||
"mongodb": "^6.16.0",
|
||||
"turndown": "^7.2.0",
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
|
||||
@@ -276,10 +276,16 @@ export async function run({
|
||||
const pdocs = tdoc.problems.split('|').map((i) => i.trim());
|
||||
const pids = pdocs.map((i) => pidMap[i]).filter((i) => i);
|
||||
const admin = uidMap[tdoc.holder_id] || uidMap[tdoc.admins.split('|')[0]];
|
||||
const startAt = new Date(tdoc.start_time * 1000);
|
||||
let endAt = new Date(tdoc.end_time * 1000);
|
||||
if (startAt >= endAt) {
|
||||
report({ message: `Invalid contest time: ${tdoc.title} ${tdoc.start_time} ${tdoc.end_time}` });
|
||||
endAt = new Date(startAt.getTime() + 5000);
|
||||
}
|
||||
const tid = await ContestModel.add(
|
||||
domainId, tdoc.title, `${tdoc.subtitle ? `#### ${tdoc.subtitle}\n` : ''}${tdoc.information || 'No Description'}`,
|
||||
admin, contentTypeMap[tdoc.type], new Date(tdoc.start_time * 1000), new Date(tdoc.end_time * 1000),
|
||||
pids, ratedTids.includes(tdoc.id), { maintainer: tdoc.admins.split('|').map((i) => uidMap[i]) },
|
||||
admin, contentTypeMap[tdoc.type], startAt, endAt,
|
||||
pids, ratedTids.includes(tdoc.id), { maintainer: tdoc.admins.split('|').map((i) => uidMap[i]), assign: [] },
|
||||
);
|
||||
tidMap[tdoc.id] = tid.toHexString();
|
||||
}
|
||||
@@ -461,7 +467,7 @@ export async function run({
|
||||
`spj.${langMap[syzojConfig.specialJudge.language]}`, `${dataDir}/testdata/${file.name}/${syzojConfig.specialJudge.fileName}`);
|
||||
config.checker = `spj.${langMap[syzojConfig.specialJudge.language]}`;
|
||||
}
|
||||
if (syzojConfig.subtasks) {
|
||||
if (syzojConfig.subtasks && syzojConfig.inputFile && syzojConfig.outputFile) {
|
||||
config.subtasks = syzojConfig.subtasks.map((subtask, index) => ({
|
||||
score: subtask.score,
|
||||
id: index + 1,
|
||||
|
||||
@@ -113,7 +113,7 @@ export function apply(ctx: Context) {
|
||||
const groupId = {};
|
||||
let gid = 1;
|
||||
for (const i of relatedGroups) groupId[i] = `group-${gid++}`;
|
||||
const organizations = teams.flatMap((i) => i.organization);
|
||||
const organizations = Array.from(new Set(teams.flatMap((i) => i.organization)));
|
||||
const orgId = {};
|
||||
let oid = 1;
|
||||
for (const i of organizations) orgId[i] = `org-${oid++}`;
|
||||
@@ -256,9 +256,11 @@ export function apply(ctx: Context) {
|
||||
zip.add('organizations/', null, { directory: true }),
|
||||
]);
|
||||
await Promise.all(teams.map(async (i) => {
|
||||
await zip.add(`teams/${i.team_id}/`, null, { directory: true });
|
||||
await zip.add(`teams/${i.team_id}/photo.download.txt`, new Zip.TextReader(i.avatar));
|
||||
}));
|
||||
await Promise.all(organizations.map(async (i) => {
|
||||
await zip.add(`organizations/${orgId[i]}/`, null, { directory: true });
|
||||
await zip.add(`organizations/${orgId[i]}/photo.download.txt`, new Zip.TextReader(avatar(teams.find((j) => j.organization === i)?.avatar || 'no photo, find and download it yourself')));
|
||||
}));
|
||||
this.binary(await zip.close(), `contest-${tdoc._id}-cdp.zip`);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/onsite-toolkit",
|
||||
"version": "0.1.0-beta.0",
|
||||
"version": "0.1.0-beta.1",
|
||||
"dependencies": {
|
||||
"@react-spring/web": "^9.7.5",
|
||||
"react-use": "^17.6.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/prom-client",
|
||||
"version": "0.2.0-beta.0",
|
||||
"version": "0.2.0-beta.1",
|
||||
"main": "index.ts",
|
||||
"dependencies": {
|
||||
"prom-client": "^15.1.3"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@hydrooj/scoreboard-xcpcio",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"version": "0.0.5-beta.0",
|
||||
"version": "0.0.5-beta.1",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"postinstall": "node -r @hydrooj/register install.ts"
|
||||
|
||||
@@ -40,7 +40,7 @@ export default class TelegramService extends Service {
|
||||
id, first_name, last_name, username, photo_url,
|
||||
} = parsed;
|
||||
const ret = {
|
||||
_id: id,
|
||||
_id: id.toString(),
|
||||
email: `${id}@telegram.local`,
|
||||
bio: '',
|
||||
uname: [`${first_name} ${last_name}`, username, `telegram${id.toString()}`].filter((i) => i),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"name": "@hydrooj/telegram",
|
||||
"version": "0.0.2-beta.0"
|
||||
"version": "0.0.2-beta.1"
|
||||
}
|
||||
@@ -134,7 +134,8 @@ export default async function (env: { watch?: boolean, production?: boolean, mea
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx', '.cjs'],
|
||||
alias: {
|
||||
vj: root(),
|
||||
'graphql-ws': root('../../modules/nop.ts'),
|
||||
'react/jsx-runtime': require.resolve('react/jsx-runtime'),
|
||||
react: require.resolve('react'),
|
||||
},
|
||||
},
|
||||
module: {
|
||||
@@ -242,7 +243,7 @@ export default async function (env: { watch?: boolean, production?: boolean, mea
|
||||
name(module) {
|
||||
const packageName = module.context.replace(/\\/g, '/').split('node_modules/').pop().split('/')[0];
|
||||
if (packageName === 'monaco-editor-nls') {
|
||||
return `i.monaco.${module.userRequest.replace(/\\/g, '/').split('/').pop().split('.')[0]}`;
|
||||
return `i.monaco.${(module as any).userRequest.replace(/\\/g, '/').split('/').pop().split('.')[0]}`;
|
||||
}
|
||||
return `n.${packageName.replace('@', '')}`;
|
||||
},
|
||||
@@ -291,7 +292,6 @@ export default async function (env: { watch?: boolean, production?: boolean, mea
|
||||
{ from: root('components/navigation/nav-logo-small_dark.png'), to: 'components/navigation/nav-logo-small_dark.png' },
|
||||
{ from: root(`${dirname(require.resolve('streamsaver/package.json'))}/mitm.html`), to: 'streamsaver/mitm.html' },
|
||||
{ from: root(`${dirname(require.resolve('streamsaver/package.json'))}/sw.js`), to: 'streamsaver/sw.js' },
|
||||
{ from: root(`${dirname(require.resolve('graphiql/package.json'))}/graphiql.min.css`), to: 'graphiql.min.css' },
|
||||
{ from: `${dirname(require.resolve('monaco-themes/package.json'))}/themes`, to: 'monaco/themes/' },
|
||||
],
|
||||
}),
|
||||
|
||||
+11
-12
@@ -22,7 +22,17 @@ const DomainSelectAutoComplete = forwardRef<AutoCompleteHandle<DomainDoc>, AutoC
|
||||
queryItems={(query) => request.get('/domain/search', { q: query })}
|
||||
itemText={(domain) => domain.name}
|
||||
itemKey={(domain) => domain._id}
|
||||
{...props}
|
||||
{...{
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
listStyle: {},
|
||||
multi: false,
|
||||
selectedKeys: [],
|
||||
allowEmptyQuery: true,
|
||||
freeSolo: false,
|
||||
freeSoloConverter: (input) => input,
|
||||
...props,
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -38,17 +48,6 @@ DomainSelectAutoComplete.propTypes = {
|
||||
freeSoloConverter: PropTypes.func,
|
||||
};
|
||||
|
||||
DomainSelectAutoComplete.defaultProps = {
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
listStyle: {},
|
||||
multi: false,
|
||||
selectedKeys: [],
|
||||
allowEmptyQuery: true,
|
||||
freeSolo: false,
|
||||
freeSoloConverter: (input) => input,
|
||||
};
|
||||
|
||||
DomainSelectAutoComplete.displayName = 'DomainSelectAutoComplete';
|
||||
|
||||
export default DomainSelectAutoComplete;
|
||||
@@ -19,7 +19,16 @@ const FileSelectAutoComplete = React.forwardRef<any, any>((props, ref) => (
|
||||
itemKey={(item) => `${item._id?.toString() || item.name || item}`}
|
||||
renderItem={(item) => item.name || item._id || item} // TODO icon
|
||||
allowEmptyQuery
|
||||
{...props}
|
||||
{...{
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
listStyle: {},
|
||||
multi: false,
|
||||
selectedKeys: [],
|
||||
freeSolo: false,
|
||||
freeSoloConverter: (input) => input,
|
||||
...props,
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -38,14 +47,4 @@ FileSelectAutoComplete.propTypes = {
|
||||
|
||||
FileSelectAutoComplete.displayName = 'FileSelectAutoComplete';
|
||||
|
||||
FileSelectAutoComplete.defaultProps = {
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
listStyle: {},
|
||||
multi: false,
|
||||
selectedKeys: [],
|
||||
freeSolo: false,
|
||||
freeSoloConverter: (input) => input,
|
||||
};
|
||||
|
||||
export default FileSelectAutoComplete;
|
||||
+13
-20
@@ -2,7 +2,7 @@ import { AutoComplete, AutoCompleteHandle, AutoCompleteProps } from '@hydrooj/co
|
||||
import type { ProblemDoc } from 'hydrooj/src/interface';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { forwardRef } from 'react';
|
||||
import { api, gql, request } from 'vj/utils';
|
||||
import { api, request } from 'vj/utils';
|
||||
|
||||
const ProblemSelectAutoComplete = forwardRef<AutoCompleteHandle<ProblemDoc>, AutoCompleteProps<ProblemDoc>>((props, ref) => (
|
||||
<AutoComplete<ProblemDoc>
|
||||
@@ -12,13 +12,7 @@ const ProblemSelectAutoComplete = forwardRef<AutoCompleteHandle<ProblemDoc>, Aut
|
||||
const { pdocs } = await request.get(`/d/${UiContext.domainId}/p`, { q: query, quick: true });
|
||||
return pdocs;
|
||||
}}
|
||||
fetchItems={(ids) => api(gql`
|
||||
problems(ids: ${ids.map((i) => +i)}) {
|
||||
docId
|
||||
pid
|
||||
title
|
||||
}
|
||||
`, ['data', 'problems'])}
|
||||
fetchItems={(ids) => api('problems', { ids: ids.map((i) => +i) }, ['docId', 'pid', 'title'])}
|
||||
itemText={(pdoc) => `${`${pdoc.docId} ${pdoc.title}`}`}
|
||||
itemKey={(pdoc) => `${pdoc.docId || pdoc}`}
|
||||
renderItem={(pdoc) => (
|
||||
@@ -29,7 +23,17 @@ const ProblemSelectAutoComplete = forwardRef<AutoCompleteHandle<ProblemDoc>, Aut
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{...props}
|
||||
{...{
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
listStyle: {},
|
||||
multi: false,
|
||||
selectedKeys: [],
|
||||
allowEmptyQuery: false,
|
||||
freeSolo: false,
|
||||
freeSoloConverter: (input) => input,
|
||||
...props,
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -45,17 +49,6 @@ ProblemSelectAutoComplete.propTypes = {
|
||||
freeSoloConverter: PropTypes.func,
|
||||
};
|
||||
|
||||
ProblemSelectAutoComplete.defaultProps = {
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
listStyle: {},
|
||||
multi: false,
|
||||
selectedKeys: [],
|
||||
allowEmptyQuery: false,
|
||||
freeSolo: false,
|
||||
freeSoloConverter: (input) => input,
|
||||
};
|
||||
|
||||
ProblemSelectAutoComplete.displayName = 'ProblemSelectAutoComplete';
|
||||
|
||||
export default ProblemSelectAutoComplete;
|
||||
@@ -2,28 +2,14 @@ import { AutoComplete, AutoCompleteHandle, AutoCompleteProps } from '@hydrooj/co
|
||||
import type { Udoc } from 'hydrooj/src/interface';
|
||||
import PropTypes from 'prop-types';
|
||||
import { forwardRef } from 'react';
|
||||
import { api, gql } from 'vj/utils';
|
||||
import { api } from 'vj/utils';
|
||||
|
||||
const UserSelectAutoComplete = forwardRef<AutoCompleteHandle<Udoc>, AutoCompleteProps<Udoc>>((props, ref) => (
|
||||
<AutoComplete<Udoc>
|
||||
ref={ref as any}
|
||||
cacheKey="user"
|
||||
{...props}
|
||||
queryItems={(query) => api(gql`
|
||||
users(search: ${query}) {
|
||||
_id
|
||||
uname
|
||||
displayName
|
||||
avatarUrl
|
||||
}
|
||||
`, ['data', 'users'])}
|
||||
fetchItems={(ids) => api(gql`
|
||||
users(auto: ${ids}) {
|
||||
_id
|
||||
uname
|
||||
displayName
|
||||
}
|
||||
`, ['data', 'users'])}
|
||||
queryItems={(query) => api('users', { search: query }, ['_id', 'uname', 'displayName', 'avatarUrl'])}
|
||||
fetchItems={(ids) => api('users', { auto: ids }, ['_id', 'uname', 'displayName'])}
|
||||
itemText={(user) => user.uname + (user.displayName ? ` (${user.displayName})` : '')}
|
||||
itemKey={(user) => ((props.multi || /^[+-]?\d+$/.test(user.uname.trim())) ? user._id.toString() : user.uname)}
|
||||
renderItem={(user) => (
|
||||
@@ -37,6 +23,17 @@ const UserSelectAutoComplete = forwardRef<AutoCompleteHandle<Udoc>, AutoComplete
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{...{
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
listStyle: {},
|
||||
multi: false,
|
||||
selectedKeys: [],
|
||||
allowEmptyQuery: false,
|
||||
freeSolo: false,
|
||||
freeSoloConverter: (input) => input,
|
||||
...props,
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -52,17 +49,6 @@ UserSelectAutoComplete.propTypes = {
|
||||
freeSoloConverter: PropTypes.func,
|
||||
};
|
||||
|
||||
UserSelectAutoComplete.defaultProps = {
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
listStyle: {},
|
||||
multi: false,
|
||||
selectedKeys: [],
|
||||
allowEmptyQuery: false,
|
||||
freeSolo: false,
|
||||
freeSoloConverter: (input) => input,
|
||||
};
|
||||
|
||||
UserSelectAutoComplete.displayName = 'UserSelectAutoComplete';
|
||||
|
||||
export default UserSelectAutoComplete;
|
||||
@@ -0,0 +1,88 @@
|
||||
import { CustomSelectAutoComplete } from '@hydrooj/components';
|
||||
import $ from 'jquery';
|
||||
import { AutoloadPage } from 'vj/misc/Page';
|
||||
import { i18n, tpl } from 'vj/utils';
|
||||
import { ActionDialog } from './dialog';
|
||||
|
||||
async function getAvailableFonts() {
|
||||
const fonts = await (window as any).queryLocalFonts();
|
||||
const result = {};
|
||||
for (const font of fonts) result[font.family] ||= font.fullName;
|
||||
return Object.entries(result).map(([family, fullName]) => ({
|
||||
id: family,
|
||||
name: family === fullName ? family : `${fullName} (${family})`,
|
||||
fullName,
|
||||
}));
|
||||
}
|
||||
|
||||
const customFont = localStorage.getItem('customFont');
|
||||
const customCodeFont = localStorage.getItem('customCodeFont');
|
||||
|
||||
export default new AutoloadPage('customFont', (pageName) => {
|
||||
let dialog;
|
||||
let selection = '';
|
||||
let handle = null;
|
||||
|
||||
async function showDialog(saveKey: string) {
|
||||
if (!dialog) {
|
||||
const fonts = await getAvailableFonts();
|
||||
dialog = new ActionDialog({
|
||||
$body: $(tpl(<CustomSelectAutoComplete
|
||||
ref={(el) => { handle = el; }}
|
||||
allowEmptyQuery={false}
|
||||
data={fonts}
|
||||
renderItem={(item) => <div style={{ fontFamily: item.id }}>{item.name}</div>}
|
||||
/>, true)),
|
||||
onDispatch: (action) => {
|
||||
selection = handle.getSelectedItems()?.[0]?.id;
|
||||
return action !== 'ok' || selection;
|
||||
},
|
||||
cancelByEsc: false,
|
||||
cancelByClickingBack: false,
|
||||
});
|
||||
}
|
||||
dialog.open().then((action) => {
|
||||
console.log(action, saveKey, selection);
|
||||
if (action === 'ok') {
|
||||
localStorage.setItem(saveKey, selection);
|
||||
window.location.reload();
|
||||
} else {
|
||||
localStorage.removeItem(saveKey);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function FontSelectButton(props: { saveKey: string, label: string, value: string }) {
|
||||
return <div className="medium-6 columns"><label>
|
||||
{i18n(props.label)}
|
||||
<div><button className="button inline" data-show-font-dialog={props.saveKey}>
|
||||
{props.value ? i18n('Using custom font: {0}', props.value) : i18n('Find custom font')}
|
||||
</button></div>
|
||||
</label></div>;
|
||||
}
|
||||
|
||||
if (pageName === 'home_preference' && 'queryLocalFonts' in window) {
|
||||
$('[name="form_item_fontFamily"]').parent().parent().parent().append($(tpl(
|
||||
<FontSelectButton saveKey="customFont" label="Select Custom Font" value={customFont} />,
|
||||
)));
|
||||
$('[name="form_item_codeFontFamily"]').parent().parent().parent().append($(tpl(
|
||||
<FontSelectButton saveKey="customCodeFont" label="Select Custom Code Font" value={customCodeFont} />,
|
||||
)));
|
||||
$(document).on('click', '[data-show-font-dialog]', (ev) => {
|
||||
ev.preventDefault();
|
||||
showDialog(ev.target.dataset.showFontDialog);
|
||||
});
|
||||
}
|
||||
}, () => {
|
||||
const fontBlock = Array.from(document.head.children).find((el) => el.tagName === 'STYLE' && el.textContent?.includes('--code-font-family'));
|
||||
if (fontBlock) {
|
||||
if (customFont) {
|
||||
fontBlock.textContent = fontBlock.textContent?.replace(/--font-family:/, `--font-family: "${customFont}", `);
|
||||
}
|
||||
if (customCodeFont) {
|
||||
fontBlock.textContent = fontBlock.textContent?.replace(/--code-font-family:/, `--code-font-family: "${customCodeFont}", `);
|
||||
}
|
||||
} else {
|
||||
console.warn('No font style block found');
|
||||
}
|
||||
});
|
||||
@@ -16,6 +16,7 @@ const mapStateToProps = (state) => ({
|
||||
: null,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export default connect(mapStateToProps)(class MessagePadDialogueContentContainer extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
$(this.refs.list).scrollLock({ strict: true });
|
||||
|
||||
@@ -21,6 +21,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(class MessagePadDialogueListContainer extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
$(this.refs.list).scrollLock({ strict: true });
|
||||
|
||||
@@ -42,6 +42,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(class MessagePadInputContainer extends React.PureComponent {
|
||||
static contextTypes = {
|
||||
store: PropTypes.object,
|
||||
|
||||
@@ -16,6 +16,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export default connect(null, mapDispatchToProps)(class MessagePadContainer extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onAdd: PropTypes.func.isRequired,
|
||||
|
||||
@@ -2,7 +2,7 @@ import keyword from 'emojis-keywords';
|
||||
import list from 'emojis-list';
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import qface from 'qface';
|
||||
import { api, gql } from 'vj/utils';
|
||||
import { api } from 'vj/utils';
|
||||
|
||||
function emoji(range) {
|
||||
return keyword.map((i, index) => ({
|
||||
@@ -40,12 +40,7 @@ monaco.languages.registerCodeLensProvider('markdown', {
|
||||
dispose: () => { },
|
||||
};
|
||||
}
|
||||
const { data } = await api(gql`
|
||||
users(ids: ${users.map((i) => +i.matches[1])}) {
|
||||
_id
|
||||
uname
|
||||
}
|
||||
`);
|
||||
const data = await api('users', { ids: users.map((i) => +i.matches[1]) }, ['_id', 'uname']);
|
||||
return {
|
||||
lenses: users.map((i, index) => ({
|
||||
range: i.range,
|
||||
@@ -53,7 +48,7 @@ monaco.languages.registerCodeLensProvider('markdown', {
|
||||
command: {
|
||||
id: 'hydro.openUserPage',
|
||||
arguments: [i.matches[1]],
|
||||
title: `@${data.users.find((doc) => doc._id.toString() === i.matches[1])?.uname || i.matches[1]}`,
|
||||
title: `@${data.find((doc) => doc._id.toString() === i.matches[1])?.uname || i.matches[1]}`,
|
||||
},
|
||||
})),
|
||||
dispose: () => { },
|
||||
@@ -82,14 +77,7 @@ monaco.languages.registerCompletionItemProvider('markdown', {
|
||||
endColumn: word.endColumn,
|
||||
};
|
||||
if (prefix === '@') {
|
||||
const users = await api(gql`
|
||||
users(search: ${word.word}) {
|
||||
_id
|
||||
uname
|
||||
avatarUrl
|
||||
priv
|
||||
}
|
||||
`, ['data', 'users']);
|
||||
const users = await api('users', { search: word.word }, ['_id', 'uname']);
|
||||
return {
|
||||
suggestions: users.map((i) => ({
|
||||
label: { label: `@${i.uname}`, description: `UID=${i._id}` },
|
||||
|
||||
@@ -4,7 +4,7 @@ import ReactDOM from 'react-dom/client';
|
||||
import { STATUS_CODES } from 'vj/constant/record';
|
||||
import { AutoloadPage } from 'vj/misc/Page';
|
||||
import {
|
||||
api, gql, i18n, request,
|
||||
api, i18n, request,
|
||||
} from 'vj/utils';
|
||||
|
||||
export default new AutoloadPage('omnibar', () => {
|
||||
@@ -92,14 +92,7 @@ export default new AutoloadPage('omnibar', () => {
|
||||
setSearching?.(true);
|
||||
[{ pdocs, psdict }, udocs] = await Promise.all([
|
||||
request.get(`/d/${UiContext.domainId}/p`, { q: query, limit: 10 }),
|
||||
api(gql`
|
||||
users(search: ${query}) {
|
||||
_id
|
||||
uname
|
||||
displayName
|
||||
avatarUrl
|
||||
}
|
||||
`, ['data', 'users']),
|
||||
api('users', { search: query }, ['_id', 'uname', 'displayName', 'avatarUrl']),
|
||||
]);
|
||||
setSearching?.(false);
|
||||
}
|
||||
|
||||
@@ -39,15 +39,12 @@ function ExtraFilesConfig() {
|
||||
const userExtraFiles = useSelector((state: RootState) => state.config.user_extra_files || [], isEqual);
|
||||
const judgeExtraFiles = useSelector((state: RootState) => state.config.judge_extra_files || [], isEqual);
|
||||
const dispatch = useDispatch();
|
||||
const userRef = React.useRef<any>();
|
||||
const judgeRef = React.useRef<any>();
|
||||
return (
|
||||
<FormItem columns={12} label="ExtraFilesConfig" disableLabel>
|
||||
<Card style={{ padding: 10 }}>
|
||||
<div className="row">
|
||||
<FormItem columns={12} label={i18n('user_extra_files')}>
|
||||
<FileSelectAutoComplete
|
||||
ref={userRef}
|
||||
data={Files}
|
||||
selectedKeys={userExtraFiles}
|
||||
onChange={(val) => dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'user_extra_files', value: val.split(',') })}
|
||||
@@ -56,7 +53,6 @@ function ExtraFilesConfig() {
|
||||
</FormItem>
|
||||
<FormItem columns={12} label={i18n('judge_extra_files')}>
|
||||
<FileSelectAutoComplete
|
||||
ref={judgeRef}
|
||||
data={Files}
|
||||
selectedKeys={judgeExtraFiles}
|
||||
onChange={(val) => dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'judge_extra_files', value: val.split(',') })}
|
||||
@@ -75,7 +71,6 @@ function LangConfig() {
|
||||
const data = Object.keys(window.LANGS).filter((i) => !prefixes.has(i))
|
||||
.map((i) => ({ name: `${i.includes('.') ? `${window.LANGS[i.split('.')[0]].display || ''}/` : ''}${window.LANGS[i].display}`, _id: i }));
|
||||
const dispatch = useDispatch();
|
||||
const ref = React.useRef<any>();
|
||||
const selectedKeys = langs.filter((i) => !prefixes.has(i));
|
||||
return (
|
||||
<FormItem columns={12} label="langs" disableLabel>
|
||||
@@ -83,7 +78,6 @@ function LangConfig() {
|
||||
<div className="row">
|
||||
<FormItem columns={12} label="langs">
|
||||
<CustomSelectAutoComplete
|
||||
ref={ref}
|
||||
data={data}
|
||||
placeholder={!selectedKeys.length ? i18n('Unlimited') : i18n('Code language')}
|
||||
selectedKeys={selectedKeys}
|
||||
|
||||
@@ -17,8 +17,8 @@ export function AddTestcase() {
|
||||
const [autoOutput, setAutoOutput] = React.useState(true);
|
||||
const testdata = useSelector((state: RootState) => state.testdata);
|
||||
const store = useStore<RootState>();
|
||||
const refInput = useRef();
|
||||
const refOutput = useRef();
|
||||
const refInput = useRef(null);
|
||||
const refOutput = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const inputValid = testdata.find((i) => i.name === input);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { omit } from 'lodash';
|
||||
import React from 'react';
|
||||
|
||||
export default function DomComponent(props: React.HTMLAttributes<HTMLDivElement> & { childDom: HTMLElement }) {
|
||||
const ref = React.useRef<HTMLDivElement>();
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
React.useEffect(() => {
|
||||
ref.current.appendChild(props.childDom);
|
||||
return () => {
|
||||
|
||||
@@ -13,6 +13,7 @@ interface ScratchpadOptions {
|
||||
commandDone?: () => void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export default connect((state: any) => ({
|
||||
value: state.editor.code,
|
||||
language: window.LANGS[state.editor.lang]?.monaco,
|
||||
|
||||
@@ -27,6 +27,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(class ScratchpadPretestContainer extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
|
||||
@@ -11,6 +11,7 @@ const mapStateToProps = (state) => ({
|
||||
isLoading: state.ui.records.isLoading,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export default connect(mapStateToProps)(class ScratchpadRecordsContainer extends React.PureComponent {
|
||||
render() {
|
||||
const cn = classNames('data-table is--full-row scratchpad__records__table', {
|
||||
|
||||
@@ -46,6 +46,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => ({
|
||||
data: stateProps.items[ownProps.id],
|
||||
});
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export default connect(mapStateToProps, null, mergeProps)(class ScratchpadRecordsRowContainer extends React.PureComponent {
|
||||
handleRowClick(ev, id) {
|
||||
const url = substitute(
|
||||
|
||||
@@ -73,6 +73,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
const availableLangs = getAvailableLangs(UiContext.pdoc.config.langs);
|
||||
const keys = Object.keys(availableLangs);
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(class ScratchpadToolbarContainer extends React.PureComponent {
|
||||
static contextTypes = {
|
||||
store: PropTypes.object,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { browserSupportsWebAuthnAutofill, startAuthentication } from '@simplewebauthn/browser';
|
||||
import $ from 'jquery';
|
||||
import responsiveCutoff from 'vj/breakpoints.json';
|
||||
import DomDialog from 'vj/components/dialog/DomDialog';
|
||||
import { InfoDialog } from 'vj/components/dialog/index';
|
||||
import { AutoloadPage } from 'vj/misc/Page';
|
||||
import { i18n, mediaQuery, tpl } from 'vj/utils';
|
||||
import {
|
||||
i18n, mediaQuery, request, tpl,
|
||||
} from 'vj/utils';
|
||||
|
||||
const signinDialogPage = new AutoloadPage('signinDialogPage', null, () => {
|
||||
const signInDialog = DomDialog.getOrConstruct($('.dialog--signin'), {
|
||||
@@ -11,13 +14,47 @@ const signinDialogPage = new AutoloadPage('signinDialogPage', null, () => {
|
||||
cancelByEsc: true,
|
||||
});
|
||||
|
||||
let authnInitialized = false;
|
||||
|
||||
async function initPasskey() {
|
||||
if (authnInitialized) return;
|
||||
authnInitialized = true;
|
||||
const support = await browserSupportsWebAuthnAutofill();
|
||||
if (!support) return;
|
||||
const authnInfo = await request.get('/user/webauthn', { login: true });
|
||||
if (!authnInfo.authOptions) return;
|
||||
const result = await startAuthentication({ optionsJSON: authnInfo.authOptions, useBrowserAutofill: true })
|
||||
.catch((e) => {
|
||||
Notification.error(i18n('Failed to get credential: {0}', e));
|
||||
return null;
|
||||
});
|
||||
if (!result) return;
|
||||
try {
|
||||
const authn = await request.post('/user/webauthn', {
|
||||
result,
|
||||
});
|
||||
if (authn.url) window.location.href = authn.url;
|
||||
else if (authn.error) throw new Error(authn.error);
|
||||
} catch (err) {
|
||||
Notification.error(err.message);
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (window.location.href.endsWith('/login')) initPasskey();
|
||||
|
||||
function show() {
|
||||
signInDialog.show();
|
||||
initPasskey();
|
||||
}
|
||||
|
||||
// don't show quick login dialog if in mobile
|
||||
if ($('[name="nav_login"]').length > 0) {
|
||||
// nav
|
||||
$('[name="nav_login"]').on('click', (ev) => {
|
||||
if (mediaQuery.isBelow(responsiveCutoff.mobile)) return;
|
||||
if (ev.shiftKey || ev.metaKey || ev.ctrlKey) return;
|
||||
signInDialog.show();
|
||||
show();
|
||||
ev.preventDefault();
|
||||
});
|
||||
}
|
||||
@@ -36,7 +73,7 @@ const signinDialogPage = new AutoloadPage('signinDialogPage', null, () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
signInDialog.show();
|
||||
show();
|
||||
};
|
||||
|
||||
$('[data-lostpass]').on('click', (e) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import PQueue from 'p-queue';
|
||||
import streamsaver from 'streamsaver';
|
||||
import Notification from 'vj/components/notification';
|
||||
import {
|
||||
api, createZipStream, gql, i18n, pipeStream, request,
|
||||
api, createZipStream, i18n, pipeStream, request,
|
||||
} from 'vj/utils';
|
||||
import { ctx } from '../../context';
|
||||
|
||||
@@ -94,23 +94,21 @@ export async function downloadProblemSet(pids, name = 'Export') {
|
||||
try {
|
||||
await ctx.serial('problemset/download', pids, name, targets);
|
||||
for (const pid of pids) {
|
||||
const pdoc = await api(gql`
|
||||
problem(id: ${+pid}) {
|
||||
pid
|
||||
owner
|
||||
title
|
||||
content
|
||||
tag
|
||||
nSubmit
|
||||
nAccept
|
||||
data {
|
||||
name
|
||||
}
|
||||
additional_file {
|
||||
name
|
||||
}
|
||||
}
|
||||
`, ['data', 'problem']);
|
||||
const pdoc = await api('problem', { id: +pid }, {
|
||||
pid: 1,
|
||||
owner: 1,
|
||||
title: 1,
|
||||
content: 1,
|
||||
tag: 1,
|
||||
nSubmit: 1,
|
||||
nAccept: 1,
|
||||
data: {
|
||||
name: 1,
|
||||
},
|
||||
additional_file: {
|
||||
name: 1,
|
||||
},
|
||||
});
|
||||
targets.push({
|
||||
filename: `${pid}/problem.yaml`,
|
||||
content: dump({
|
||||
|
||||
@@ -176,7 +176,7 @@ export function apply(ctx: Context) {
|
||||
);
|
||||
c.setting.SystemSetting(Schema.object({
|
||||
'ui-default': Schema.object({
|
||||
footer_extra_html: Schema.string().default(''),
|
||||
footer_extra_html: Schema.string().role('textarea').default(''),
|
||||
nav_logo_dark: Schema.string().default('/components/navigation/nav-logo-small_dark.png'),
|
||||
preload: Schema.string().default(''),
|
||||
domainNavigation: Schema.boolean().default(true).description('Show Domain Navigation'),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/ui-default",
|
||||
"version": "4.58.0-beta.0",
|
||||
"version": "4.58.0-beta.2",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "index.ts",
|
||||
@@ -11,9 +11,9 @@
|
||||
"cli": false
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blueprintjs/core": "^5.18.0",
|
||||
"@blueprintjs/core": "^5.19.0",
|
||||
"@fontsource/dm-mono": "^5.2.5",
|
||||
"@fontsource/fira-code": "^5.2.5",
|
||||
"@fontsource/fira-code": "^5.2.6",
|
||||
"@fontsource/inconsolata": "^5.2.5",
|
||||
"@fontsource/jetbrains-mono": "^5.2.5",
|
||||
"@fontsource/pt-mono": "^5.2.5",
|
||||
@@ -21,8 +21,8 @@
|
||||
"@fontsource/source-code-pro": "^5.2.5",
|
||||
"@fontsource/ubuntu-mono": "^5.2.5",
|
||||
"@hydrooj/utils": "workspace:^",
|
||||
"@sentry/browser": "^9.12.0",
|
||||
"@sentry/webpack-plugin": "^3.3.1",
|
||||
"@sentry/browser": "^9.17.0",
|
||||
"@sentry/webpack-plugin": "^3.4.0",
|
||||
"@simplewebauthn/browser": "13.1.0",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@types/html-to-text": "^9.0.4",
|
||||
@@ -34,8 +34,8 @@
|
||||
"@types/nunjucks": "^3.2.6",
|
||||
"@types/pickadate": "^3.5.35",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@types/react": "^19.1.3",
|
||||
"@types/react-dom": "^19.1.3",
|
||||
"@types/redux-logger": "^3.0.13",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/webpack-env": "^1.18.8",
|
||||
@@ -49,9 +49,9 @@
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"clipboard": "^2.0.11",
|
||||
"copy-webpack-plugin": "^13.0.0",
|
||||
"cordis": "4.0.0-beta.1",
|
||||
"core-js": "^3.41.0",
|
||||
"core-js-compat": "^3.41.0",
|
||||
"cordis": "4.0.0-beta.2",
|
||||
"core-js": "^3.42.0",
|
||||
"core-js-compat": "^3.42.0",
|
||||
"css-loader": "^7.1.2",
|
||||
"diff": "^7.0.0",
|
||||
"diff-dom": "^5.1.4",
|
||||
@@ -60,20 +60,19 @@
|
||||
"emojis-list": "2.1.0",
|
||||
"esbuild-loader": "^4.3.0",
|
||||
"flatpickr": "^4.6.13",
|
||||
"graphiql": "^3.8.3",
|
||||
"idb": "^8.0.2",
|
||||
"idb": "^8.0.3",
|
||||
"jquery": "^3.7.1",
|
||||
"jquery-scroll-lock": "^3.1.3",
|
||||
"jquery.easing": "^1.4.1",
|
||||
"jquery.transit": "^0.9.12",
|
||||
"matchmedia-polyfill": "^0.3.2",
|
||||
"md-editor-rt": "^5.4.5",
|
||||
"md-editor-rt": "^5.5.1",
|
||||
"mini-css-extract-plugin": "^2.9.2",
|
||||
"moment": "^2.30.1",
|
||||
"monaco-editor": "0.52.2",
|
||||
"monaco-editor-nls": "^3.1.0",
|
||||
"monaco-editor-webpack-plugin": "^7.1.0",
|
||||
"monaco-themes": "^0.4.4",
|
||||
"monaco-themes": "^0.4.5",
|
||||
"monaco-yaml": "5.1.1",
|
||||
"nanoid": "^5.1.5",
|
||||
"normalize.css": "^8.0.1",
|
||||
@@ -99,7 +98,7 @@
|
||||
"redux-promise-middleware": "^6.2.0",
|
||||
"redux-thunk": "^3.1.0",
|
||||
"rupture": "^0.7.1",
|
||||
"sass": "^1.86.3",
|
||||
"sass": "^1.87.0",
|
||||
"sass-loader": "^16.0.5",
|
||||
"schemastery": "^3.16.1",
|
||||
"schemastery-react": "^0.1.2",
|
||||
@@ -116,7 +115,7 @@
|
||||
"timeago.js": "^4.0.2",
|
||||
"ts-loader": "^9.5.2",
|
||||
"web-streams-polyfill": "^4.1.0",
|
||||
"webpack": "^5.99.5",
|
||||
"webpack": "^5.99.8",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"webpack-dev-server": "^5.2.1",
|
||||
"webpack-manifest-plugin": "^5.0.1",
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
.graphiql-container .doc-explorer-title, .graphiql-container .history-title
|
||||
overflow: hidden;
|
||||
padding: 5px 40px 0px 0px !important
|
||||
|
||||
.graphiql-container .docExplorerWrap
|
||||
font-family: var(--code-font-family) !important
|
||||
|
||||
.graphiql-container .doc-explorer-back
|
||||
overflow: hidden;
|
||||
padding: 8px 20px 0px 20px !important
|
||||
@@ -1,29 +0,0 @@
|
||||
import { pick } from 'lodash';
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
import { request } from 'vj/utils';
|
||||
|
||||
const defaultQuery = `\
|
||||
query Example(
|
||||
$name: String!
|
||||
) {
|
||||
user(uname: $name) {
|
||||
_id
|
||||
uname
|
||||
}
|
||||
}`;
|
||||
|
||||
export default new NamedPage('api', async () => {
|
||||
const { GraphiQL } = await import('graphiql');
|
||||
GraphiQL.Logo = function () {
|
||||
return <p></p>;
|
||||
} as any;
|
||||
createRoot(document.getElementById('main')!).render(
|
||||
<GraphiQL
|
||||
fetcher={(body, opts = {}) => request.post(`/d/${UiContext.domain._id}/api`, body, pick(opts, 'headers'))}
|
||||
defaultQuery={defaultQuery}
|
||||
variables='{"name": "Hydro"}'
|
||||
/>,
|
||||
);
|
||||
});
|
||||
@@ -5,22 +5,19 @@ import { ActionDialog, ConfirmDialog } from 'vj/components/dialog';
|
||||
import Notification from 'vj/components/notification';
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
import {
|
||||
api, delay, gql, i18n, tpl,
|
||||
api, delay, i18n, tpl,
|
||||
} from 'vj/utils';
|
||||
|
||||
function update(name: string, uids: number[]) {
|
||||
return api(gql`
|
||||
domain { manage { group {
|
||||
update(name: ${name}, uids: ${uids})
|
||||
} } }
|
||||
`);
|
||||
function update(name: string, uids: number[] = []) {
|
||||
return api('domain.group', {
|
||||
name,
|
||||
uids,
|
||||
}, []);
|
||||
}
|
||||
function del(name: string) {
|
||||
return api(gql`
|
||||
domain { manage { group {
|
||||
del(name: ${name})
|
||||
} } }
|
||||
`);
|
||||
return api('domain.group', {
|
||||
name,
|
||||
}, []);
|
||||
}
|
||||
|
||||
const page = new NamedPage('domain_group', () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import VjNotification from 'vj/components/notification';
|
||||
import selectUser from 'vj/components/selectUser';
|
||||
import { ctx, Service } from 'vj/context';
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
import { api, gql, loadReactRedux } from 'vj/utils';
|
||||
import { api, loadReactRedux } from 'vj/utils';
|
||||
|
||||
class MessagePadService extends Service {
|
||||
WebSocket: typeof import('../components/socket').default;
|
||||
@@ -91,14 +91,10 @@ const page = new NamedPage('home_messages', () => {
|
||||
async function loadSendTarget() {
|
||||
const target = new URL(window.location.href).searchParams.get('target');
|
||||
if (!target) return;
|
||||
const user = await api(gql`
|
||||
users(search: ${target}, exact: true) {
|
||||
_id
|
||||
uname
|
||||
avatarUrl
|
||||
mail
|
||||
}
|
||||
`, ['data', 'users']);
|
||||
const user = await api(
|
||||
'users', { search: target, exact: true },
|
||||
['_id', 'uname', 'avatarUrl', 'mail'],
|
||||
);
|
||||
if (!user?.length) return;
|
||||
createDialog(user[0]);
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{% extends "layout/basic.html" %}
|
||||
{% block content %}
|
||||
<div style="height: 90vh" id="main"></div>
|
||||
<style>
|
||||
#main {
|
||||
font-family: var(--code-font-family)
|
||||
}
|
||||
</style>
|
||||
<link href="/graphiql.min.css" rel="stylesheet" />
|
||||
{% endblock %}
|
||||
@@ -23,7 +23,6 @@
|
||||
</h1>
|
||||
<div class="footer__category__expander"><ul class="footer__category__list">
|
||||
<li class="footer__category__item"><a href="https://github.com/hydro-dev/Hydro" target="_blank">{{ _('Open Source') }}</a></li>
|
||||
<li class="footer__category__item"><a href="{{ url('api') }}">{{ _('API') }}</a></li>
|
||||
</ul></div>
|
||||
</div>
|
||||
<div class="medium-3 large-2 columns footer__category expandable end">
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<div class="row"><div class="columns">
|
||||
<label class="material textbox">
|
||||
{{ _('Username') }}
|
||||
<input name="uname" type="text" data-autofocus>
|
||||
<input name="uname" type="text" data-autofocus autocomplete="username webauthn">
|
||||
</label>
|
||||
</div></div>
|
||||
<div class="row"><div class="columns">
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
<div class="section__body no-padding">
|
||||
<div data-fragment-id="ranking">
|
||||
{% if not udocs.length %}
|
||||
<blockquote class="note">{{ _('The ranking page is not updated realtime.') }}</blockquote>
|
||||
{% if page == 1 %}
|
||||
<blockquote class="note">{{ _('The ranking page is not updated realtime.') }}</blockquote>
|
||||
{% endif %}
|
||||
{{ nothing.render('Sorry, there are no users in this domain.') }}
|
||||
{% else %}
|
||||
<table class="data-table">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="row"><div class="columns">
|
||||
<label class="inverse material textbox">
|
||||
{{ _('Username') }}
|
||||
<input name="uname" type="text" autofocus>
|
||||
<input name="uname" type="text" autofocus autocomplete="username webauthn">
|
||||
</label>
|
||||
</div></div>
|
||||
<div class="row"><div class="columns">
|
||||
|
||||
@@ -2,30 +2,11 @@ import 'streamsaver/examples/zip-stream';
|
||||
|
||||
import { request } from './base';
|
||||
|
||||
export async function api(q: string, path: string[] = []) {
|
||||
let query = q.trim();
|
||||
if (!query.startsWith('query')) query = `query{${query}}`;
|
||||
const res = await request.post(`/d/${UiContext.domainId}/api`, { query });
|
||||
if (res.errors) throw new Error(res.errors[0].message);
|
||||
let cursor = res;
|
||||
for (const p of path) {
|
||||
cursor = cursor[p];
|
||||
if (!cursor) return undefined;
|
||||
}
|
||||
return cursor;
|
||||
}
|
||||
|
||||
export const gql = (
|
||||
pieces: TemplateStringsArray,
|
||||
...templates: (string | number | string[] | number[])[]
|
||||
) => {
|
||||
let res = '';
|
||||
for (let i = 0; i < pieces.length; i++) {
|
||||
res += pieces[i];
|
||||
if (templates[i]) res += JSON.stringify(templates[i]);
|
||||
}
|
||||
export async function api(method: string, args: Record<string, any>, projection: any) {
|
||||
const res = await request.post(`/d/${UiContext.domainId}/api/${encodeURIComponent(method)}`, { args, projection });
|
||||
if (res.error) throw new Error(res.error);
|
||||
return res;
|
||||
};
|
||||
}
|
||||
|
||||
export function getAvailableLangs(langsList?: string[]) {
|
||||
const prefixes = new Set(Object.keys(window.LANGS).filter((i) => i.includes('.')).map((i) => i.split('.')[0]));
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-react": "^4.4.0",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@types/react": "^19.1.3",
|
||||
"@types/react-dom": "^19.1.3",
|
||||
"@vitejs/plugin-react-swc": "^3.9.0",
|
||||
"vite": "^6.2.6"
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/utils",
|
||||
"version": "1.4.34-beta.0",
|
||||
"version": "1.4.34-beta.2",
|
||||
"description": "hydrooj utils",
|
||||
"main": "lib/utils.ts",
|
||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||
@@ -13,7 +13,7 @@
|
||||
"fs-extra": "^11.3.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"moment-timezone": "^0.5.48",
|
||||
"mongodb": "^6.15.0",
|
||||
"mongodb": "^6.16.0",
|
||||
"reggol": "^2.1.0",
|
||||
"search-query-parser": "^1.6.0",
|
||||
"systeminformation": "^5.25.11"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/vjudge",
|
||||
"version": "1.10.0-beta.0",
|
||||
"version": "1.10.0-beta.1",
|
||||
"description": "Submit problems to remote oj",
|
||||
"main": "./src/index.ts",
|
||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||
|
||||
Reference in new issue
Block a user