mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
core&ui: drop graphql apis
This commit is contained in:
1 parent
0f7961d675
commit
6e16d03f67
52 files changed
+485
-583
No files matched your search
@@ -86,6 +86,9 @@ export default defineConfig([globalIgnores([
|
|||||||
},
|
},
|
||||||
|
|
||||||
settings: {
|
settings: {
|
||||||
|
'react-x': {
|
||||||
|
version: '18.3.1',
|
||||||
|
},
|
||||||
'import/resolver': {
|
'import/resolver': {
|
||||||
webpack: {
|
webpack: {
|
||||||
config: {
|
config: {
|
||||||
|
|||||||
@@ -4,19 +4,19 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "base.mjs",
|
"main": "base.mjs",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-react/eslint-plugin": "^1.48.4",
|
"@eslint-react/eslint-plugin": "^1.48.5",
|
||||||
"@eslint/compat": "^1.2.8",
|
"@eslint/compat": "^1.2.8",
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@stylistic/eslint-plugin": "^4.2.0",
|
"@stylistic/eslint-plugin": "^4.2.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.30.1",
|
"@typescript-eslint/eslint-plugin": "^8.31.0",
|
||||||
"@typescript-eslint/parser": "^8.30.1",
|
"@typescript-eslint/parser": "^8.31.0",
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
"eslint-plugin-de-morgan": "^1.2.1",
|
"eslint-plugin-de-morgan": "^1.2.1",
|
||||||
"eslint-plugin-github": "^6.0.0",
|
"eslint-plugin-github": "^6.0.0",
|
||||||
"eslint-plugin-import": "2.31.0",
|
"eslint-plugin-import": "2.31.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"globals": "^16.0.0"
|
"globals": "^16.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import { Context, Service } from 'cordis';
|
||||||
|
import Schema from 'schemastery';
|
||||||
|
import { param } from './decorators';
|
||||||
|
import { BadRequestError, MethodNotAllowedError, NotFoundError } from './error';
|
||||||
|
import { } from './interface';
|
||||||
|
import { Handler } from './server';
|
||||||
|
import { Types } from './validator';
|
||||||
|
|
||||||
|
const BINARY = Symbol.for('hydro.api.response.binary');
|
||||||
|
const REDIRECT = Symbol.for('hydro.api.response.redirect');
|
||||||
|
|
||||||
|
export type ApiCall<Arg, Res> = {
|
||||||
|
readonly type: 'Query' | 'Mutation';
|
||||||
|
readonly input: Schema<Arg>;
|
||||||
|
readonly func: (context: any, args: Arg) => Promise<Res> | Res;
|
||||||
|
readonly hooks: ApiCall<Arg, void>[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const _get = (type: 'Query' | 'Mutation') => <Arg, Res>(
|
||||||
|
schema: Schema<Arg>,
|
||||||
|
func: (context: Handler, args: Arg) => Promise<Res> | Res,
|
||||||
|
hooks: ApiCall<any, void>[] = [],
|
||||||
|
): ApiCall<Arg, Res> => ({ input: schema, func, hooks, type } as const); // eslint-disable-line
|
||||||
|
|
||||||
|
export const Query = _get('Query');
|
||||||
|
export const Mutation = _get('Mutation');
|
||||||
|
|
||||||
|
export class BinaryResponse {
|
||||||
|
[BINARY]: true;
|
||||||
|
constructor(public readonly data: Buffer, public filename: string) { }
|
||||||
|
static check(value: any): value is BinaryResponse {
|
||||||
|
return value && typeof value === 'object' && BINARY in value && value[BINARY] === true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RedirectResponse {
|
||||||
|
[REDIRECT]: true;
|
||||||
|
constructor(public readonly url: string) { }
|
||||||
|
static check(value: any): value is RedirectResponse {
|
||||||
|
return value && typeof value === 'object' && REDIRECT in value && value[REDIRECT] === true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated TODO */
|
||||||
|
export const NOP = Query(Schema.any(), () => { });
|
||||||
|
|
||||||
|
export const APIS = {
|
||||||
|
'query.batch': Query(Schema.array(Schema.object({ op: Schema.string(), args: Schema.any() })), () => ({})),
|
||||||
|
'mutation.batch': Mutation(Schema.array(Schema.object({ op: Schema.string(), args: Schema.any() })), () => ({})),
|
||||||
|
} as const;
|
||||||
|
export interface Apis {
|
||||||
|
builtin: {
|
||||||
|
'query.batch': ApiCall<{ op: string, args: any }[], { [key: string]: any }>;
|
||||||
|
'mutation.batch': ApiCall<{ op: string, args: any }[], { [key: string]: any }>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export type FlattenedApis = Apis[keyof Apis];
|
||||||
|
|
||||||
|
// Thanks to @ForkKILLET for the projection function
|
||||||
|
type ProjectionSchemaId = 1;
|
||||||
|
type MKeyOf<T> = T extends any ? keyof T : never;
|
||||||
|
type MId<T> = { [K in MKeyOf<T>]: T[K] } & {};
|
||||||
|
type ProjectionSchema<T> = T extends Array<infer U>
|
||||||
|
? ProjectionSchema<U>
|
||||||
|
: | { [K in keyof T]?: ProjectionSchemaId | ProjectionSchema<T[K]> }
|
||||||
|
| Record<keyof any, ProjectionSchemaId | object>;
|
||||||
|
type AsKeys<T> = T extends Array<infer U extends string> ? Record<U, 1> : T;
|
||||||
|
type Projection<T, S> = S extends ProjectionSchemaId
|
||||||
|
? T : T extends Array<infer U> ? Array<MId<Projection<U, S>>> : {
|
||||||
|
[K in keyof T & keyof S]: K extends keyof AsKeys<S> ? Projection<T[K], AsKeys<S>[K]> : never
|
||||||
|
};
|
||||||
|
|
||||||
|
export const projection = <T, S extends ProjectionSchema<T>>(input: T, schema: S): Projection<T, S> => {
|
||||||
|
if (typeof input !== 'object' || input === null) throw new Error('Input must be an object.');
|
||||||
|
type R = Projection<T, S>;
|
||||||
|
if (Array.isArray(schema)) schema = Object.fromEntries(schema.map((s) => [s, 1])) as S;
|
||||||
|
if (Array.isArray(input)) {
|
||||||
|
return input.map((item) => projection(item, schema)) as R;
|
||||||
|
}
|
||||||
|
const result = {} as R;
|
||||||
|
for (const key of Reflect.ownKeys(input)) {
|
||||||
|
const schemaIt = schema[key];
|
||||||
|
if (!schemaIt) continue;
|
||||||
|
if (schemaIt === 1) result[key] = input[key];
|
||||||
|
else result[key] = projection(input[key], schemaIt);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ApiExecutionContext {
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApiService extends Service {
|
||||||
|
constructor(ctx: Context) {
|
||||||
|
super(ctx, 'api');
|
||||||
|
}
|
||||||
|
|
||||||
|
provide(calls: Partial<FlattenedApis>) {
|
||||||
|
this.ctx.effect(() => {
|
||||||
|
for (const key in calls) {
|
||||||
|
APIS[key] = calls[key];
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
for (const key in calls) {
|
||||||
|
delete APIS[key];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(context: ApiExecutionContext, callOrName: ApiCall<any, any> | string, rawArgs: any, emitHook?: any, project?: any) {
|
||||||
|
const call = typeof callOrName === 'string' ? APIS[callOrName] : callOrName;
|
||||||
|
if (!call) throw new NotFoundError(callOrName);
|
||||||
|
const { input, func, hooks } = call;
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
for (const hook of hooks) await this.execute(context, hook, rawArgs);
|
||||||
|
|
||||||
|
let args: any;
|
||||||
|
try {
|
||||||
|
args = input ? input(rawArgs as any) : rawArgs;
|
||||||
|
} catch (e) {
|
||||||
|
throw new BadRequestError(e.message);
|
||||||
|
}
|
||||||
|
if (typeof callOrName === 'string') {
|
||||||
|
await emitHook?.('api/before', args);
|
||||||
|
await emitHook?.(`api/before/${callOrName}`, args);
|
||||||
|
}
|
||||||
|
const result = await func(context, args as any);
|
||||||
|
return project ? projection(result, project) : result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'cordis' {
|
||||||
|
interface Context {
|
||||||
|
api: ApiService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApiHandler<C extends Context> extends Handler<C> {
|
||||||
|
@param('op', Types.String)
|
||||||
|
async all({ }, op: string) {
|
||||||
|
if (!['get', 'post'].includes(this.request.method.toLowerCase())) {
|
||||||
|
throw new MethodNotAllowedError(this.request.method);
|
||||||
|
}
|
||||||
|
if (!APIS[op]) throw new BadRequestError('Invalid operation');
|
||||||
|
if (APIS[op].type === 'Mutation' && this.request.method.toLowerCase() === 'get') {
|
||||||
|
throw new BadRequestError('Mutation operation cannot be called with GET method');
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
await this.ctx.parallel('handler/api/before', this);
|
||||||
|
// @ts-ignore
|
||||||
|
await this.ctx.parallel(`handler/api/before/${op}`, this);
|
||||||
|
const result = await this.ctx.api.execute(
|
||||||
|
this, op, { domainId: this.args.domainId, ...(this.args.args || {}) },
|
||||||
|
(m, args) => (this.ctx.parallel as any)(m, args), this.args.projection,
|
||||||
|
);
|
||||||
|
if (BinaryResponse.check(result)) {
|
||||||
|
this.binary(result.data, result.filename);
|
||||||
|
} else if (RedirectResponse.check(result)) {
|
||||||
|
this.response.redirect = result.url;
|
||||||
|
} else {
|
||||||
|
this.response.body = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyApiHandler(ctx: Context, name: string, path: string) {
|
||||||
|
ctx.plugin(ApiService);
|
||||||
|
ctx.inject(['server', 'api'], ({ Route }) => {
|
||||||
|
Route(name, path, ApiHandler);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -5,3 +5,4 @@ export * from './validator';
|
|||||||
export * from './error';
|
export * from './error';
|
||||||
export * from './router';
|
export * from './router';
|
||||||
export * from './interface';
|
export * from './interface';
|
||||||
|
export * from './api';
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { Context } from 'cordis';
|
||||||
|
import type { ApiHandler } from './api';
|
||||||
import type { ConnectionHandler, Handler, NotFoundHandler } from './server';
|
import type { ConnectionHandler, Handler, NotFoundHandler } from './server';
|
||||||
|
|
||||||
export interface KnownHandlers {
|
export interface KnownHandlers {
|
||||||
@@ -21,16 +23,20 @@ export type HookType = 'before-prepare' | 'before' | 'before-operation' | 'after
|
|||||||
export type Methods = 'get' | 'post' | 'put' | 'delete' | 'patch';
|
export type Methods = 'get' | 'post' | 'put' | 'delete' | 'patch';
|
||||||
export type HookWithMethod = Exclude<HookType, 'before-operation'>;
|
export type HookWithMethod = Exclude<HookType, 'before-operation'>;
|
||||||
|
|
||||||
export interface ServerEvents<C> extends KnownHandlerEvents, HandlerEvents<C> {
|
export interface ServerEvents<C extends Context> extends KnownHandlerEvents, HandlerEvents<C> {
|
||||||
'handler/create': (thisArg: Handler<C> | ConnectionHandler<C>, type: 'ws' | 'http') => VoidReturn
|
'handler/create': (thisArg: Handler<C> | ConnectionHandler<C>, type: 'ws' | 'http') => VoidReturn
|
||||||
'handler/create/http': (thisArg: Handler<C>) => VoidReturn
|
'handler/create/http': (thisArg: Handler<C>) => VoidReturn
|
||||||
'handler/create/ws': (thisArg: ConnectionHandler<C>) => VoidReturn
|
'handler/create/ws': (thisArg: ConnectionHandler<C>) => VoidReturn
|
||||||
'handler/init': (thisArg: Handler<C>) => VoidReturn
|
'handler/init': (thisArg: Handler<C>) => VoidReturn
|
||||||
'handler/error': (thisArg: Handler<C> | ConnectionHandler<C>, e: Error) => VoidReturn
|
'handler/error': (thisArg: Handler<C> | ConnectionHandler<C>, e: Error) => VoidReturn
|
||||||
|
'handler/api/before': (thisArg: ApiHandler<C>) => VoidReturn
|
||||||
|
'api/before': (args: any) => VoidReturn
|
||||||
[k: `handler/${HookType}/${string}`]: (thisArg: any) => VoidReturn
|
[k: `handler/${HookType}/${string}`]: (thisArg: any) => VoidReturn
|
||||||
[k: `handler/${HookWithMethod}/${string}/${Methods}`]: (thisArg: any) => VoidReturn
|
[k: `handler/${HookWithMethod}/${string}/${Methods}`]: (thisArg: any) => VoidReturn
|
||||||
[k: `handler/register/${string}`]: (HandlerClass: new (...args: any[]) => any) => VoidReturn
|
[k: `handler/register/${string}`]: (HandlerClass: new (...args: any[]) => any) => VoidReturn
|
||||||
[k: `handler/error/${string}`]: (thisArg: Handler<C>, e: Error) => VoidReturn
|
[k: `handler/error/${string}`]: (thisArg: Handler<C>, e: Error) => VoidReturn
|
||||||
|
[k: `handler/api/before/${string}`]: (thisArg: ApiHandler<C>) => VoidReturn
|
||||||
|
[k: `api/before/${string}`]: (args: any) => VoidReturn
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'cordis' {
|
declare module 'cordis' {
|
||||||
|
|||||||
@@ -502,16 +502,18 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`);
|
|||||||
await (this.ctx.parallel as any)('handler/create/http', h);
|
await (this.ctx.parallel as any)('handler/create/http', h);
|
||||||
|
|
||||||
if (checker) checker.call(h);
|
if (checker) checker.call(h);
|
||||||
if (method === 'post') {
|
if (typeof h.all !== 'function') {
|
||||||
if (operation) {
|
if (method === 'post') {
|
||||||
if (typeof h[`post${operation}`] !== 'function') {
|
if (operation) {
|
||||||
throw new InvalidOperationError(operation);
|
if (typeof h[`post${operation}`] !== 'function') {
|
||||||
|
throw new InvalidOperationError(operation);
|
||||||
|
}
|
||||||
|
} else if (typeof h.post !== 'function') {
|
||||||
|
throw new MethodNotAllowedError(method);
|
||||||
}
|
}
|
||||||
} else if (typeof h.post !== 'function') {
|
} else if (typeof h[method] !== 'function') {
|
||||||
throw new MethodNotAllowedError(method);
|
throw new MethodNotAllowedError(method);
|
||||||
}
|
}
|
||||||
} else if (typeof h[method] !== 'function' && typeof h.all !== 'function') {
|
|
||||||
throw new MethodNotAllowedError(method);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { expect } from 'chai';
|
||||||
|
import { describe, it } from 'node:test';
|
||||||
|
import { projection } from '../api';
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
foo: 1,
|
||||||
|
bar: 'string',
|
||||||
|
arr: [
|
||||||
|
{ sub: 1, another: 'key' },
|
||||||
|
{ sub: 2 },
|
||||||
|
],
|
||||||
|
obj: {
|
||||||
|
sub: 3,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('projection', () => {
|
||||||
|
it('pick', () => {
|
||||||
|
expect(projection(input, { foo: 1, bar: 1 })).to.deep.equal({ foo: 1, bar: 'string' });
|
||||||
|
});
|
||||||
|
it('array', () => {
|
||||||
|
expect(projection(input, { arr: { sub: 1 } })).to.deep.equal({ arr: [{ sub: 1 }, { sub: 2 }] });
|
||||||
|
});
|
||||||
|
it('non-exist', () => {
|
||||||
|
expect(projection(input, { 'non-exist': 1 })).to.deep.equal({});
|
||||||
|
});
|
||||||
|
it('partial', () => {
|
||||||
|
expect(projection(input, { arr: { sub: 1, another: 1 } })).to.deep.equal({ arr: [{ sub: 1, another: 'key' }, { sub: 2 }] });
|
||||||
|
});
|
||||||
|
it('object', () => {
|
||||||
|
expect(projection(input, { obj: { sub: 1 } })).to.deep.equal({ obj: { sub: 3 } });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('safety', () => {
|
||||||
|
it('prototype', () => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
expect(projection({}, { __proto__: 1 })).to.deep.equal({});
|
||||||
|
expect(projection({}, { prototype: 1 })).to.deep.equal({});
|
||||||
|
});
|
||||||
|
it('circular', () => {
|
||||||
|
const circular = {};
|
||||||
|
// @ts-ignore
|
||||||
|
circular.circular = circular;
|
||||||
|
expect(projection(circular, { circular: 1 })).to.deep.equal({ circular });
|
||||||
|
});
|
||||||
|
});
|
||||||
+4
-4
@@ -43,8 +43,8 @@
|
|||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cross-spawn": "^7.0.6",
|
"cross-spawn": "^7.0.6",
|
||||||
"esbuild": "0.25.2",
|
"esbuild": "0.25.2",
|
||||||
"eslint": "^9.25.0",
|
"eslint": "^9.25.1",
|
||||||
"eslint-import-resolver-typescript": "^4.3.3",
|
"eslint-import-resolver-typescript": "^4.3.4",
|
||||||
"eslint-import-resolver-webpack": "^0.13.10",
|
"eslint-import-resolver-webpack": "^0.13.10",
|
||||||
"fs-extra": "^11.3.0",
|
"fs-extra": "^11.3.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
"mongodb-memory-server": "10.1.4",
|
"mongodb-memory-server": "10.1.4",
|
||||||
"nyc": "^17.1.0",
|
"nyc": "^17.1.0",
|
||||||
"ora": "^8.2.0",
|
"ora": "^8.2.0",
|
||||||
"oxlint": "^0.16.6",
|
"oxlint": "^0.16.8",
|
||||||
"package-json": "^10.0.1",
|
"package-json": "^10.0.1",
|
||||||
"semver": "^7.7.1",
|
"semver": "^7.7.1",
|
||||||
"simple-git": "^3.27.0",
|
"simple-git": "^3.27.0",
|
||||||
@@ -62,5 +62,5 @@
|
|||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"wtfnode": "^0.10.0"
|
"wtfnode": "^0.10.0"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
"packageManager": "yarn@4.9.1"
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,6 @@
|
|||||||
height: 30px;
|
height: 30px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
width: 100%;
|
|
||||||
min-width: 30px;
|
min-width: 30px;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"version": "1.0.0-beta.1",
|
"version": "1.0.0-beta.1",
|
||||||
"main": "./frontend/index.ts",
|
"main": "./frontend/index.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"allotment": "^1.20.3",
|
||||||
"diff": "^7.0.0",
|
"diff": "^7.0.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
@@ -15,6 +16,9 @@
|
|||||||
"@types/diff": "^7.0.2",
|
"@types/diff": "^7.0.2",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/lodash": "^4.17.16",
|
"@types/lodash": "^4.17.16",
|
||||||
"sass": "^1.86.3"
|
"sass": "^1.87.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"schemastery": "*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
"author": "undefined <i@undefined.moe>",
|
"author": "undefined <i@undefined.moe>",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elastic/elasticsearch": "^8.18.0"
|
"@elastic/elasticsearch": "^8.18.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"hydrooj": ">=5.0.0-alpha.1"
|
"hydrooj": ">=5.0.0-alpha.1"
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
"@aws-sdk/s3-request-presigner": "3.726.1",
|
"@aws-sdk/s3-request-presigner": "3.726.1",
|
||||||
"@babel/code-frame": "^7.26.2",
|
"@babel/code-frame": "^7.26.2",
|
||||||
"@cordisjs/plugin-timer": "1.1.1",
|
"@cordisjs/plugin-timer": "1.1.1",
|
||||||
"@graphql-tools/schema": "^10.0.23",
|
|
||||||
"@hydrooj/common": "workspace:^",
|
"@hydrooj/common": "workspace:^",
|
||||||
"@hydrooj/framework": "workspace:^",
|
"@hydrooj/framework": "workspace:^",
|
||||||
"@hydrooj/utils": "workspace:^",
|
"@hydrooj/utils": "workspace:^",
|
||||||
@@ -32,8 +31,6 @@
|
|||||||
"emoji-regex": "^10.4.0",
|
"emoji-regex": "^10.4.0",
|
||||||
"emojis-list": "2.1.0",
|
"emojis-list": "2.1.0",
|
||||||
"fs-extra": "^11.3.0",
|
"fs-extra": "^11.3.0",
|
||||||
"graphql": "^16.10.0",
|
|
||||||
"graphql-scalars": "^1.24.2",
|
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"koa-proxies": "^0.12.4",
|
"koa-proxies": "^0.12.4",
|
||||||
"koa-static-cache": "^5.1.4",
|
"koa-static-cache": "^5.1.4",
|
||||||
@@ -41,7 +38,7 @@
|
|||||||
"lru-cache": "^10.4.3",
|
"lru-cache": "^10.4.3",
|
||||||
"mime-types": "^3.0.1",
|
"mime-types": "^3.0.1",
|
||||||
"moment-timezone": "^0.5.48",
|
"moment-timezone": "^0.5.48",
|
||||||
"mongodb": "^6.15.0",
|
"mongodb": "^6.16.0",
|
||||||
"mongodb-uri": "^0.9.7",
|
"mongodb-uri": "^0.9.7",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"nodemailer": "^6.10.1",
|
"nodemailer": "^6.10.1",
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ export async function apply(ctx: Context) {
|
|||||||
resolve(c);
|
resolve(c);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await ctx.plugin(require('../service/api'));
|
|
||||||
require('../lib/index');
|
require('../lib/index');
|
||||||
|
|
||||||
ctx.plugin(require('../service/monitor'));
|
ctx.plugin(require('../service/monitor'));
|
||||||
|
|||||||
@@ -742,33 +742,6 @@ export async function apply(ctx: Context) {
|
|||||||
}
|
}
|
||||||
await Promise.all(tasks);
|
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.plugin(ScoreboardService);
|
||||||
ctx.inject(['scoreboard'], ({ Route, scoreboard }) => {
|
ctx.inject(['scoreboard'], ({ Route, scoreboard }) => {
|
||||||
Route('contest_scoreboard', '/contest/:tid/scoreboard', ContestScoreboardHandler, PERM.PERM_VIEW_CONTEST_SCOREBOARD);
|
Route('contest_scoreboard', '/contest/:tid/scoreboard', ContestScoreboardHandler, PERM.PERM_VIEW_CONTEST_SCOREBOARD);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { load } from 'js-yaml';
|
import { load } from 'js-yaml';
|
||||||
import { Dictionary } from 'lodash';
|
import { Dictionary } from 'lodash';
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
|
import Schema from 'schemastery';
|
||||||
import type { Context } from '../context';
|
import type { Context } from '../context';
|
||||||
import {
|
import {
|
||||||
CannotDeleteSystemDomainError, DomainJoinAlreadyMemberError, DomainJoinForbiddenError, ForbiddenError,
|
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 * as system from '../model/system';
|
||||||
import user from '../model/user';
|
import user from '../model/user';
|
||||||
import {
|
import {
|
||||||
Handler, param, post, query, requireSudo, Types,
|
Handler, Mutation, param, post, Query, query, requireSudo, Types,
|
||||||
} from '../service/server';
|
} from '../service/server';
|
||||||
import { log2 } from '../utils';
|
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) {
|
export async function apply(ctx: Context) {
|
||||||
ctx.Route('ranking', '/ranking', DomainRankHandler, PERM.PERM_VIEW_RANKING);
|
ctx.Route('ranking', '/ranking', DomainRankHandler, PERM.PERM_VIEW_RANKING);
|
||||||
ctx.Route('domain_dashboard', '/domain/dashboard', DomainDashboardHandler);
|
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_join', '/domain/join', DomainJoinHandler, PRIV.PRIV_USER_PROFILE);
|
||||||
ctx.Route('domain_search', '/domain/search', DomainSearchHandler, PRIV.PRIV_USER_PROFILE);
|
ctx.Route('domain_search', '/domain/search', DomainSearchHandler, PRIV.PRIV_USER_PROFILE);
|
||||||
ctx.inject(['api'], ({ api }) => {
|
ctx.inject(['api'], ({ api }) => {
|
||||||
api.value('GroupInfo', [
|
api.provide(DomainApi);
|
||||||
['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,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -321,9 +321,12 @@ const allPriv = Math.sum(Object.values(Priv));
|
|||||||
|
|
||||||
class SystemUserPrivHandler extends SystemHandler {
|
class SystemUserPrivHandler extends SystemHandler {
|
||||||
@requireSudo
|
@requireSudo
|
||||||
async get() {
|
@param('extraIgnore', Types.NumericArray, true)
|
||||||
|
async get({ }, extraIgnore: number[] = []) {
|
||||||
const defaultPriv = system.get('default.priv');
|
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();
|
const banudocs = await user.getMulti({ _id: { $gte: -1000, $ne: 1 }, priv: 0 }).limit(1000).sort({ _id: 1 }).toArray();
|
||||||
this.response.body = {
|
this.response.body = {
|
||||||
udocs: [...udocs, ...banudocs],
|
udocs: [...udocs, ...banudocs],
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
import { Filter, ObjectId } from 'mongodb';
|
import { Filter, ObjectId } from 'mongodb';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import sanitize from 'sanitize-filename';
|
import sanitize from 'sanitize-filename';
|
||||||
|
import Schema from 'schemastery';
|
||||||
import parser from '@hydrooj/utils/lib/search';
|
import parser from '@hydrooj/utils/lib/search';
|
||||||
import { sortFiles, streamToBuffer } from '@hydrooj/utils/lib/utils';
|
import { sortFiles, streamToBuffer } from '@hydrooj/utils/lib/utils';
|
||||||
import type { Context } from '../context';
|
import type { Context } from '../context';
|
||||||
@@ -34,7 +35,7 @@ import storage from '../model/storage';
|
|||||||
import * as system from '../model/system';
|
import * as system from '../model/system';
|
||||||
import user from '../model/user';
|
import user from '../model/user';
|
||||||
import {
|
import {
|
||||||
Handler, param, post, query, route, Types,
|
Handler, param, post, Query, query, route, Types,
|
||||||
} from '../service/server';
|
} from '../service/server';
|
||||||
import { ContestDetailBaseHandler } from './contest';
|
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) {
|
export async function apply(ctx: Context) {
|
||||||
ctx.Route('problem_main', '/p', ProblemMainHandler, PERM.PERM_VIEW_PROBLEM);
|
ctx.Route('problem_main', '/p', ProblemMainHandler, PERM.PERM_VIEW_PROBLEM);
|
||||||
ctx.Route('problem_random', '/problem/random', ProblemRandomHandler, 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_statistics', '/p/:pid/stat', ProblemStatisticsHandler, PERM.PERM_VIEW_PROBLEM);
|
||||||
ctx.Route('problem_create', '/problem/create', ProblemCreateHandler, PERM.PERM_CREATE_PROBLEM);
|
ctx.Route('problem_create', '/problem/create', ProblemCreateHandler, PERM.PERM_CREATE_PROBLEM);
|
||||||
ctx.inject(['api'], ({ api }) => {
|
ctx.inject(['api'], ({ api }) => {
|
||||||
api.value('FileInfo', [
|
api.provide(ProblemApi);
|
||||||
['_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),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ import { generateAuthenticationOptions, verifyAuthenticationResponse } from '@si
|
|||||||
import { isoBase64URL } from '@simplewebauthn/server/helpers';
|
import { isoBase64URL } from '@simplewebauthn/server/helpers';
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
import type { Binary } from 'mongodb';
|
import type { Binary } from 'mongodb';
|
||||||
|
import Schema from 'schemastery';
|
||||||
import type { Context } from '../context';
|
import type { Context } from '../context';
|
||||||
import {
|
import {
|
||||||
AuthOperationError, BadRequestError, BlacklistedError, BuiltinLoginError,
|
AuthOperationError, BadRequestError, BlacklistedError, BuiltinLoginError,
|
||||||
@@ -25,7 +26,7 @@ import * as system from '../model/system';
|
|||||||
import token from '../model/token';
|
import token from '../model/token';
|
||||||
import user, { deleteUserCache } from '../model/user';
|
import user, { deleteUserCache } from '../model/user';
|
||||||
import {
|
import {
|
||||||
Handler, param, post, Types,
|
Handler, param, post, Query, Types,
|
||||||
} from '../service/server';
|
} from '../service/server';
|
||||||
|
|
||||||
async function successfulAuth(this: Handler, udoc: User) {
|
async function successfulAuth(this: Handler, udoc: User) {
|
||||||
@@ -515,6 +516,67 @@ class ContestModeHandler extends Handler {
|
|||||||
|
|
||||||
export const inject = ['oauth'];
|
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 c.user;
|
||||||
|
}),
|
||||||
|
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) {
|
export async function apply(ctx: Context) {
|
||||||
ctx.Route('user_login', '/login', UserLoginHandler);
|
ctx.Route('user_login', '/login', UserLoginHandler);
|
||||||
ctx.Route('user_oauth', '/oauth/:type', OauthHandler);
|
ctx.Route('user_oauth', '/oauth/:type', OauthHandler);
|
||||||
@@ -544,60 +606,6 @@ export async function apply(ctx: Context) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
ctx.inject(['api'], ({ api }) => {
|
ctx.inject(['api'], ({ api }) => {
|
||||||
api.value('User', [
|
api.provide(UserApi);
|
||||||
['_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.');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -784,7 +784,7 @@ export async function add(
|
|||||||
RULES[rule].check(data);
|
RULES[rule].check(data);
|
||||||
await bus.parallel('contest/before-add', data);
|
await bus.parallel('contest/before-add', data);
|
||||||
const docId = await document.add(domainId, content, owner, document.TYPE_CONTEST, null, null, null, {
|
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);
|
await bus.parallel('contest/add', data, docId);
|
||||||
return docId;
|
return docId;
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
/* eslint-disable max-len */
|
/* eslint-disable max-len */
|
||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
|
import fs from 'fs';
|
||||||
import * as cordis from 'cordis';
|
import * as cordis from 'cordis';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import { Dictionary } from 'lodash';
|
import { Dictionary } from 'lodash';
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
import Schema from 'schemastery';
|
import Schema from 'schemastery';
|
||||||
import { LangConfig, parseLang } from '@hydrooj/common';
|
import { LangConfig, parseLang } from '@hydrooj/common';
|
||||||
import { retry } from '@hydrooj/utils';
|
import { findFileSync, retry } from '@hydrooj/utils';
|
||||||
import { Context, Service } from '../context';
|
import { Context, Service } from '../context';
|
||||||
import { Setting as _Setting } from '../interface';
|
import { Setting as _Setting } from '../interface';
|
||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
@@ -14,6 +15,8 @@ import * as builtin from './builtin';
|
|||||||
|
|
||||||
type SettingDict = Dictionary<_Setting>;
|
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 logger = new Logger('model/setting');
|
||||||
const countries = moment.tz.countries();
|
const countries = moment.tz.countries();
|
||||||
const tzs = new Set();
|
const tzs = new Set();
|
||||||
@@ -344,6 +347,8 @@ SystemSetting(
|
|||||||
Setting('setting_basic', 'pagination.solution', 20, 'number', 'pagination.solution', 'Solutions per page'),
|
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.training', 10, 'number', 'pagination.training', 'Trainings per page'),
|
||||||
Setting('setting_basic', 'pagination.reply', 50, 'number', 'pagination.reply', 'Replies 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.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.domain', '', 'text', 'session.domain', 'session.domain', FLAG_HIDDEN),
|
||||||
Setting('setting_session', 'session.saved_expire_seconds', 3600 * 24 * 30,
|
Setting('setting_session', 'session.saved_expire_seconds', 3600 * 24 * 30,
|
||||||
|
|||||||
@@ -55,4 +55,3 @@ export { db, pwsh };
|
|||||||
|
|
||||||
// to load services into to context
|
// to load services into to context
|
||||||
export { } from './handler/contest';
|
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);
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@ import fs from 'fs-extra';
|
|||||||
import cache from 'koa-static-cache';
|
import cache from 'koa-static-cache';
|
||||||
import { type FindCursor, ObjectId } from 'mongodb';
|
import { type FindCursor, ObjectId } from 'mongodb';
|
||||||
import {
|
import {
|
||||||
ConnectionHandler as ConnectionHandlerOriginal,
|
applyApiHandler, ConnectionHandler as ConnectionHandlerOriginal,
|
||||||
Handler as HandlerOriginal, HydroError, NotFoundError, UserFacingError,
|
Handler as HandlerOriginal, HydroError, NotFoundError, UserFacingError,
|
||||||
WebService,
|
WebService,
|
||||||
} from '@hydrooj/framework';
|
} from '@hydrooj/framework';
|
||||||
@@ -41,6 +41,7 @@ declare module '@hydrooj/framework' {
|
|||||||
|
|
||||||
export * from '@hydrooj/framework/decorators';
|
export * from '@hydrooj/framework/decorators';
|
||||||
export * from '@hydrooj/framework/validator';
|
export * from '@hydrooj/framework/validator';
|
||||||
|
export { Query, Mutation } from '@hydrooj/framework/api';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* For security concern, some API requires sudo privilege to access.
|
* For security concern, some API requires sudo privilege to access.
|
||||||
@@ -106,6 +107,8 @@ export async function apply(ctx: Context) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
applyApiHandler(ctx, 'api', '/api/:op');
|
||||||
|
|
||||||
for (const addon of [...Object.values(global.addons)].reverse()) {
|
for (const addon of [...Object.values(global.addons)].reverse()) {
|
||||||
const dir = resolve(addon, 'public');
|
const dir = resolve(addon, 'public');
|
||||||
if (!fs.existsSync(dir)) continue;
|
if (!fs.existsSync(dir)) continue;
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
"author": "undefined <i@undefined.moe>",
|
"author": "undefined <i@undefined.moe>",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mariadb": "^3.4.1",
|
"mariadb": "^3.4.2",
|
||||||
"mongodb": "^6.15.0",
|
"mongodb": "^6.16.0",
|
||||||
"turndown": "^7.2.0",
|
"turndown": "^7.2.0",
|
||||||
"xml2js": "^0.6.2"
|
"xml2js": "^0.6.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -276,10 +276,16 @@ export async function run({
|
|||||||
const pdocs = tdoc.problems.split('|').map((i) => i.trim());
|
const pdocs = tdoc.problems.split('|').map((i) => i.trim());
|
||||||
const pids = pdocs.map((i) => pidMap[i]).filter((i) => i);
|
const pids = pdocs.map((i) => pidMap[i]).filter((i) => i);
|
||||||
const admin = uidMap[tdoc.holder_id] || uidMap[tdoc.admins.split('|')[0]];
|
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(
|
const tid = await ContestModel.add(
|
||||||
domainId, tdoc.title, `${tdoc.subtitle ? `#### ${tdoc.subtitle}\n` : ''}${tdoc.information || 'No Description'}`,
|
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),
|
admin, contentTypeMap[tdoc.type], startAt, endAt,
|
||||||
pids, ratedTids.includes(tdoc.id), { maintainer: tdoc.admins.split('|').map((i) => uidMap[i]) },
|
pids, ratedTids.includes(tdoc.id), { maintainer: tdoc.admins.split('|').map((i) => uidMap[i]), assign: [] },
|
||||||
);
|
);
|
||||||
tidMap[tdoc.id] = tid.toHexString();
|
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}`);
|
`spj.${langMap[syzojConfig.specialJudge.language]}`, `${dataDir}/testdata/${file.name}/${syzojConfig.specialJudge.fileName}`);
|
||||||
config.checker = `spj.${langMap[syzojConfig.specialJudge.language]}`;
|
config.checker = `spj.${langMap[syzojConfig.specialJudge.language]}`;
|
||||||
}
|
}
|
||||||
if (syzojConfig.subtasks) {
|
if (syzojConfig.subtasks && syzojConfig.inputFile && syzojConfig.outputFile) {
|
||||||
config.subtasks = syzojConfig.subtasks.map((subtask, index) => ({
|
config.subtasks = syzojConfig.subtasks.map((subtask, index) => ({
|
||||||
score: subtask.score,
|
score: subtask.score,
|
||||||
id: index + 1,
|
id: index + 1,
|
||||||
|
|||||||
@@ -134,7 +134,8 @@ export default async function (env: { watch?: boolean, production?: boolean, mea
|
|||||||
extensions: ['.js', '.jsx', '.ts', '.tsx', '.cjs'],
|
extensions: ['.js', '.jsx', '.ts', '.tsx', '.cjs'],
|
||||||
alias: {
|
alias: {
|
||||||
vj: root(),
|
vj: root(),
|
||||||
'graphql-ws': root('../../modules/nop.ts'),
|
'react/jsx-runtime': require.resolve('react/jsx-runtime'),
|
||||||
|
react: require.resolve('react'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
@@ -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('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'))}/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('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/' },
|
{ from: `${dirname(require.resolve('monaco-themes/package.json'))}/themes`, to: 'monaco/themes/' },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
|||||||
+2
-8
@@ -2,7 +2,7 @@ import { AutoComplete, AutoCompleteHandle, AutoCompleteProps } from '@hydrooj/co
|
|||||||
import type { ProblemDoc } from 'hydrooj/src/interface';
|
import type { ProblemDoc } from 'hydrooj/src/interface';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { forwardRef } from 'react';
|
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) => (
|
const ProblemSelectAutoComplete = forwardRef<AutoCompleteHandle<ProblemDoc>, AutoCompleteProps<ProblemDoc>>((props, ref) => (
|
||||||
<AutoComplete<ProblemDoc>
|
<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 });
|
const { pdocs } = await request.get(`/d/${UiContext.domainId}/p`, { q: query, quick: true });
|
||||||
return pdocs;
|
return pdocs;
|
||||||
}}
|
}}
|
||||||
fetchItems={(ids) => api(gql`
|
fetchItems={(ids) => api('problems', { ids: ids.map((i) => +i) }, ['docId', 'pid', 'title'])}
|
||||||
problems(ids: ${ids.map((i) => +i)}) {
|
|
||||||
docId
|
|
||||||
pid
|
|
||||||
title
|
|
||||||
}
|
|
||||||
`, ['data', 'problems'])}
|
|
||||||
itemText={(pdoc) => `${`${pdoc.docId} ${pdoc.title}`}`}
|
itemText={(pdoc) => `${`${pdoc.docId} ${pdoc.title}`}`}
|
||||||
itemKey={(pdoc) => `${pdoc.docId || pdoc}`}
|
itemKey={(pdoc) => `${pdoc.docId || pdoc}`}
|
||||||
renderItem={(pdoc) => (
|
renderItem={(pdoc) => (
|
||||||
|
|||||||
@@ -2,27 +2,14 @@ import { AutoComplete, AutoCompleteHandle, AutoCompleteProps } from '@hydrooj/co
|
|||||||
import type { Udoc } from 'hydrooj/src/interface';
|
import type { Udoc } from 'hydrooj/src/interface';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import { api, gql } from 'vj/utils';
|
import { api } from 'vj/utils';
|
||||||
|
|
||||||
const UserSelectAutoComplete = forwardRef<AutoCompleteHandle<Udoc>, AutoCompleteProps<Udoc>>((props, ref) => (
|
const UserSelectAutoComplete = forwardRef<AutoCompleteHandle<Udoc>, AutoCompleteProps<Udoc>>((props, ref) => (
|
||||||
<AutoComplete<Udoc>
|
<AutoComplete<Udoc>
|
||||||
ref={ref as any}
|
ref={ref as any}
|
||||||
cacheKey="user"
|
cacheKey="user"
|
||||||
queryItems={(query) => api(gql`
|
queryItems={(query) => api('users', { search: query }, ['_id', 'uname', 'displayName', 'avatarUrl'])}
|
||||||
users(search: ${query}) {
|
fetchItems={(ids) => api('users', { auto: ids }, ['_id', 'uname', 'displayName'])}
|
||||||
_id
|
|
||||||
uname
|
|
||||||
displayName
|
|
||||||
avatarUrl
|
|
||||||
}
|
|
||||||
`, ['data', 'users'])}
|
|
||||||
fetchItems={(ids) => api(gql`
|
|
||||||
users(auto: ${ids}) {
|
|
||||||
_id
|
|
||||||
uname
|
|
||||||
displayName
|
|
||||||
}
|
|
||||||
`, ['data', 'users'])}
|
|
||||||
itemText={(user) => user.uname + (user.displayName ? ` (${user.displayName})` : '')}
|
itemText={(user) => user.uname + (user.displayName ? ` (${user.displayName})` : '')}
|
||||||
itemKey={(user) => ((props.multi || /^[+-]?\d+$/.test(user.uname.trim())) ? user._id.toString() : user.uname)}
|
itemKey={(user) => ((props.multi || /^[+-]?\d+$/.test(user.uname.trim())) ? user._id.toString() : user.uname)}
|
||||||
renderItem={(user) => (
|
renderItem={(user) => (
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const mapStateToProps = (state) => ({
|
|||||||
: null,
|
: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export default connect(mapStateToProps)(class MessagePadDialogueContentContainer extends React.PureComponent {
|
export default connect(mapStateToProps)(class MessagePadDialogueContentContainer extends React.PureComponent {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
$(this.refs.list).scrollLock({ strict: true });
|
$(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 {
|
export default connect(mapStateToProps, mapDispatchToProps)(class MessagePadDialogueListContainer extends React.PureComponent {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
$(this.refs.list).scrollLock({ strict: true });
|
$(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 {
|
export default connect(mapStateToProps, mapDispatchToProps)(class MessagePadInputContainer extends React.PureComponent {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
store: PropTypes.object,
|
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 {
|
export default connect(null, mapDispatchToProps)(class MessagePadContainer extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onAdd: PropTypes.func.isRequired,
|
onAdd: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import keyword from 'emojis-keywords';
|
|||||||
import list from 'emojis-list';
|
import list from 'emojis-list';
|
||||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||||
import qface from 'qface';
|
import qface from 'qface';
|
||||||
import { api, gql } from 'vj/utils';
|
import { api } from 'vj/utils';
|
||||||
|
|
||||||
function emoji(range) {
|
function emoji(range) {
|
||||||
return keyword.map((i, index) => ({
|
return keyword.map((i, index) => ({
|
||||||
@@ -40,12 +40,7 @@ monaco.languages.registerCodeLensProvider('markdown', {
|
|||||||
dispose: () => { },
|
dispose: () => { },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { data } = await api(gql`
|
const data = await api('users', { ids: users.map((i) => +i.matches[1]) }, ['_id', 'uname']);
|
||||||
users(ids: ${users.map((i) => +i.matches[1])}) {
|
|
||||||
_id
|
|
||||||
uname
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
return {
|
return {
|
||||||
lenses: users.map((i, index) => ({
|
lenses: users.map((i, index) => ({
|
||||||
range: i.range,
|
range: i.range,
|
||||||
@@ -53,7 +48,7 @@ monaco.languages.registerCodeLensProvider('markdown', {
|
|||||||
command: {
|
command: {
|
||||||
id: 'hydro.openUserPage',
|
id: 'hydro.openUserPage',
|
||||||
arguments: [i.matches[1]],
|
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: () => { },
|
dispose: () => { },
|
||||||
@@ -82,14 +77,7 @@ monaco.languages.registerCompletionItemProvider('markdown', {
|
|||||||
endColumn: word.endColumn,
|
endColumn: word.endColumn,
|
||||||
};
|
};
|
||||||
if (prefix === '@') {
|
if (prefix === '@') {
|
||||||
const users = await api(gql`
|
const users = await api('users', { search: word.word }, ['_id', 'uname']);
|
||||||
users(search: ${word.word}) {
|
|
||||||
_id
|
|
||||||
uname
|
|
||||||
avatarUrl
|
|
||||||
priv
|
|
||||||
}
|
|
||||||
`, ['data', 'users']);
|
|
||||||
return {
|
return {
|
||||||
suggestions: users.map((i) => ({
|
suggestions: users.map((i) => ({
|
||||||
label: { label: `@${i.uname}`, description: `UID=${i._id}` },
|
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 { STATUS_CODES } from 'vj/constant/record';
|
||||||
import { AutoloadPage } from 'vj/misc/Page';
|
import { AutoloadPage } from 'vj/misc/Page';
|
||||||
import {
|
import {
|
||||||
api, gql, i18n, request,
|
api, i18n, request,
|
||||||
} from 'vj/utils';
|
} from 'vj/utils';
|
||||||
|
|
||||||
export default new AutoloadPage('omnibar', () => {
|
export default new AutoloadPage('omnibar', () => {
|
||||||
@@ -92,14 +92,7 @@ export default new AutoloadPage('omnibar', () => {
|
|||||||
setSearching?.(true);
|
setSearching?.(true);
|
||||||
[{ pdocs, psdict }, udocs] = await Promise.all([
|
[{ pdocs, psdict }, udocs] = await Promise.all([
|
||||||
request.get(`/d/${UiContext.domainId}/p`, { q: query, limit: 10 }),
|
request.get(`/d/${UiContext.domainId}/p`, { q: query, limit: 10 }),
|
||||||
api(gql`
|
api('users', { search: query }, ['_id', 'uname', 'displayName', 'avatarUrl']),
|
||||||
users(search: ${query}) {
|
|
||||||
_id
|
|
||||||
uname
|
|
||||||
displayName
|
|
||||||
avatarUrl
|
|
||||||
}
|
|
||||||
`, ['data', 'users']),
|
|
||||||
]);
|
]);
|
||||||
setSearching?.(false);
|
setSearching?.(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ export function AddTestcase() {
|
|||||||
const [autoOutput, setAutoOutput] = React.useState(true);
|
const [autoOutput, setAutoOutput] = React.useState(true);
|
||||||
const testdata = useSelector((state: RootState) => state.testdata);
|
const testdata = useSelector((state: RootState) => state.testdata);
|
||||||
const store = useStore<RootState>();
|
const store = useStore<RootState>();
|
||||||
const refInput = useRef();
|
const refInput = useRef(null);
|
||||||
const refOutput = useRef();
|
const refOutput = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const inputValid = testdata.find((i) => i.name === input);
|
const inputValid = testdata.find((i) => i.name === input);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ interface ScratchpadOptions {
|
|||||||
commandDone?: () => void;
|
commandDone?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export default connect((state: any) => ({
|
export default connect((state: any) => ({
|
||||||
value: state.editor.code,
|
value: state.editor.code,
|
||||||
language: window.LANGS[state.editor.lang]?.monaco,
|
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 {
|
export default connect(mapStateToProps, mapDispatchToProps)(class ScratchpadPretestContainer extends React.PureComponent {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const mapStateToProps = (state) => ({
|
|||||||
isLoading: state.ui.records.isLoading,
|
isLoading: state.ui.records.isLoading,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export default connect(mapStateToProps)(class ScratchpadRecordsContainer extends React.PureComponent {
|
export default connect(mapStateToProps)(class ScratchpadRecordsContainer extends React.PureComponent {
|
||||||
render() {
|
render() {
|
||||||
const cn = classNames('data-table is--full-row scratchpad__records__table', {
|
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],
|
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 {
|
export default connect(mapStateToProps, null, mergeProps)(class ScratchpadRecordsRowContainer extends React.PureComponent {
|
||||||
handleRowClick(ev, id) {
|
handleRowClick(ev, id) {
|
||||||
const url = substitute(
|
const url = substitute(
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
const availableLangs = getAvailableLangs(UiContext.pdoc.config.langs);
|
const availableLangs = getAvailableLangs(UiContext.pdoc.config.langs);
|
||||||
const keys = Object.keys(availableLangs);
|
const keys = Object.keys(availableLangs);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(class ScratchpadToolbarContainer extends React.PureComponent {
|
export default connect(mapStateToProps, mapDispatchToProps)(class ScratchpadToolbarContainer extends React.PureComponent {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
store: PropTypes.object,
|
store: PropTypes.object,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import PQueue from 'p-queue';
|
|||||||
import streamsaver from 'streamsaver';
|
import streamsaver from 'streamsaver';
|
||||||
import Notification from 'vj/components/notification';
|
import Notification from 'vj/components/notification';
|
||||||
import {
|
import {
|
||||||
api, createZipStream, gql, i18n, pipeStream, request,
|
api, createZipStream, i18n, pipeStream, request,
|
||||||
} from 'vj/utils';
|
} from 'vj/utils';
|
||||||
import { ctx } from '../../context';
|
import { ctx } from '../../context';
|
||||||
|
|
||||||
@@ -94,23 +94,21 @@ export async function downloadProblemSet(pids, name = 'Export') {
|
|||||||
try {
|
try {
|
||||||
await ctx.serial('problemset/download', pids, name, targets);
|
await ctx.serial('problemset/download', pids, name, targets);
|
||||||
for (const pid of pids) {
|
for (const pid of pids) {
|
||||||
const pdoc = await api(gql`
|
const pdoc = await api('problem', { id: +pid }, {
|
||||||
problem(id: ${+pid}) {
|
pid: 1,
|
||||||
pid
|
owner: 1,
|
||||||
owner
|
title: 1,
|
||||||
title
|
content: 1,
|
||||||
content
|
tag: 1,
|
||||||
tag
|
nSubmit: 1,
|
||||||
nSubmit
|
nAccept: 1,
|
||||||
nAccept
|
data: {
|
||||||
data {
|
name: 1,
|
||||||
name
|
},
|
||||||
}
|
additional_file: {
|
||||||
additional_file {
|
name: 1,
|
||||||
name
|
},
|
||||||
}
|
});
|
||||||
}
|
|
||||||
`, ['data', 'problem']);
|
|
||||||
targets.push({
|
targets.push({
|
||||||
filename: `${pid}/problem.yaml`,
|
filename: `${pid}/problem.yaml`,
|
||||||
content: dump({
|
content: dump({
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"cli": false
|
"cli": false
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@blueprintjs/core": "^5.18.0",
|
"@blueprintjs/core": "^5.19.0",
|
||||||
"@fontsource/dm-mono": "^5.2.5",
|
"@fontsource/dm-mono": "^5.2.5",
|
||||||
"@fontsource/fira-code": "^5.2.5",
|
"@fontsource/fira-code": "^5.2.5",
|
||||||
"@fontsource/inconsolata": "^5.2.5",
|
"@fontsource/inconsolata": "^5.2.5",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"@fontsource/source-code-pro": "^5.2.5",
|
"@fontsource/source-code-pro": "^5.2.5",
|
||||||
"@fontsource/ubuntu-mono": "^5.2.5",
|
"@fontsource/ubuntu-mono": "^5.2.5",
|
||||||
"@hydrooj/utils": "workspace:^",
|
"@hydrooj/utils": "workspace:^",
|
||||||
"@sentry/browser": "^9.13.0",
|
"@sentry/browser": "^9.14.0",
|
||||||
"@sentry/webpack-plugin": "^3.3.1",
|
"@sentry/webpack-plugin": "^3.3.1",
|
||||||
"@simplewebauthn/browser": "13.1.0",
|
"@simplewebauthn/browser": "13.1.0",
|
||||||
"@svgr/webpack": "^8.1.0",
|
"@svgr/webpack": "^8.1.0",
|
||||||
@@ -60,7 +60,6 @@
|
|||||||
"emojis-list": "2.1.0",
|
"emojis-list": "2.1.0",
|
||||||
"esbuild-loader": "^4.3.0",
|
"esbuild-loader": "^4.3.0",
|
||||||
"flatpickr": "^4.6.13",
|
"flatpickr": "^4.6.13",
|
||||||
"graphiql": "^3.8.3",
|
|
||||||
"idb": "^8.0.2",
|
"idb": "^8.0.2",
|
||||||
"jquery": "^3.7.1",
|
"jquery": "^3.7.1",
|
||||||
"jquery-scroll-lock": "^3.1.3",
|
"jquery-scroll-lock": "^3.1.3",
|
||||||
@@ -99,7 +98,7 @@
|
|||||||
"redux-promise-middleware": "^6.2.0",
|
"redux-promise-middleware": "^6.2.0",
|
||||||
"redux-thunk": "^3.1.0",
|
"redux-thunk": "^3.1.0",
|
||||||
"rupture": "^0.7.1",
|
"rupture": "^0.7.1",
|
||||||
"sass": "^1.86.3",
|
"sass": "^1.87.0",
|
||||||
"sass-loader": "^16.0.5",
|
"sass-loader": "^16.0.5",
|
||||||
"schemastery": "^3.16.1",
|
"schemastery": "^3.16.1",
|
||||||
"schemastery-react": "^0.1.2",
|
"schemastery-react": "^0.1.2",
|
||||||
@@ -116,7 +115,7 @@
|
|||||||
"timeago.js": "^4.0.2",
|
"timeago.js": "^4.0.2",
|
||||||
"ts-loader": "^9.5.2",
|
"ts-loader": "^9.5.2",
|
||||||
"web-streams-polyfill": "^4.1.0",
|
"web-streams-polyfill": "^4.1.0",
|
||||||
"webpack": "^5.99.6",
|
"webpack": "^5.99.7",
|
||||||
"webpack-bundle-analyzer": "^4.10.2",
|
"webpack-bundle-analyzer": "^4.10.2",
|
||||||
"webpack-dev-server": "^5.2.1",
|
"webpack-dev-server": "^5.2.1",
|
||||||
"webpack-manifest-plugin": "^5.0.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 Notification from 'vj/components/notification';
|
||||||
import { NamedPage } from 'vj/misc/Page';
|
import { NamedPage } from 'vj/misc/Page';
|
||||||
import {
|
import {
|
||||||
api, delay, gql, i18n, tpl,
|
api, delay, i18n, tpl,
|
||||||
} from 'vj/utils';
|
} from 'vj/utils';
|
||||||
|
|
||||||
function update(name: string, uids: number[]) {
|
function update(name: string, uids: number[] = []) {
|
||||||
return api(gql`
|
return api('domain.group', {
|
||||||
domain { manage { group {
|
name,
|
||||||
update(name: ${name}, uids: ${uids})
|
uids,
|
||||||
} } }
|
}, []);
|
||||||
`);
|
|
||||||
}
|
}
|
||||||
function del(name: string) {
|
function del(name: string) {
|
||||||
return api(gql`
|
return api('domain.group', {
|
||||||
domain { manage { group {
|
name,
|
||||||
del(name: ${name})
|
}, []);
|
||||||
} } }
|
|
||||||
`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const page = new NamedPage('domain_group', () => {
|
const page = new NamedPage('domain_group', () => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import VjNotification from 'vj/components/notification';
|
|||||||
import selectUser from 'vj/components/selectUser';
|
import selectUser from 'vj/components/selectUser';
|
||||||
import { ctx, Service } from 'vj/context';
|
import { ctx, Service } from 'vj/context';
|
||||||
import { NamedPage } from 'vj/misc/Page';
|
import { NamedPage } from 'vj/misc/Page';
|
||||||
import { api, gql, loadReactRedux } from 'vj/utils';
|
import { api, loadReactRedux } from 'vj/utils';
|
||||||
|
|
||||||
class MessagePadService extends Service {
|
class MessagePadService extends Service {
|
||||||
WebSocket: typeof import('../components/socket').default;
|
WebSocket: typeof import('../components/socket').default;
|
||||||
@@ -91,14 +91,10 @@ const page = new NamedPage('home_messages', () => {
|
|||||||
async function loadSendTarget() {
|
async function loadSendTarget() {
|
||||||
const target = new URL(window.location.href).searchParams.get('target');
|
const target = new URL(window.location.href).searchParams.get('target');
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
const user = await api(gql`
|
const user = await api(
|
||||||
users(search: ${target}, exact: true) {
|
'users', { search: target, exact: true },
|
||||||
_id
|
['_id', 'uname', 'avatarUrl', 'mail'],
|
||||||
uname
|
);
|
||||||
avatarUrl
|
|
||||||
mail
|
|
||||||
}
|
|
||||||
`, ['data', 'users']);
|
|
||||||
if (!user?.length) return;
|
if (!user?.length) return;
|
||||||
createDialog(user[0]);
|
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>
|
</h1>
|
||||||
<div class="footer__category__expander"><ul class="footer__category__list">
|
<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="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>
|
</ul></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="medium-3 large-2 columns footer__category expandable end">
|
<div class="medium-3 large-2 columns footer__category expandable end">
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
<div class="section__body no-padding">
|
<div class="section__body no-padding">
|
||||||
<div data-fragment-id="ranking">
|
<div data-fragment-id="ranking">
|
||||||
{% if not udocs.length %}
|
{% 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.') }}
|
{{ nothing.render('Sorry, there are no users in this domain.') }}
|
||||||
{% else %}
|
{% else %}
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
|
|||||||
@@ -2,30 +2,11 @@ import 'streamsaver/examples/zip-stream';
|
|||||||
|
|
||||||
import { request } from './base';
|
import { request } from './base';
|
||||||
|
|
||||||
export async function api(q: string, path: string[] = []) {
|
export async function api(method: string, args: Record<string, any>, projection: any) {
|
||||||
let query = q.trim();
|
const res = await request.post(`/d/${UiContext.domainId}/api/${encodeURIComponent(method)}`, { args, projection });
|
||||||
if (!query.startsWith('query')) query = `query{${query}}`;
|
if (res.error) throw new Error(res.error);
|
||||||
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]);
|
|
||||||
}
|
|
||||||
return res;
|
return res;
|
||||||
};
|
}
|
||||||
|
|
||||||
export function getAvailableLangs(langsList?: string[]) {
|
export function getAvailableLangs(langsList?: string[]) {
|
||||||
const prefixes = new Set(Object.keys(window.LANGS).filter((i) => i.includes('.')).map((i) => i.split('.')[0]));
|
const prefixes = new Set(Object.keys(window.LANGS).filter((i) => i.includes('.')).map((i) => i.split('.')[0]));
|
||||||
|
|||||||
@@ -16,6 +16,6 @@
|
|||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19.1.2",
|
||||||
"@types/react-dom": "^19.1.2",
|
"@types/react-dom": "^19.1.2",
|
||||||
"@vitejs/plugin-react-swc": "^3.9.0",
|
"@vitejs/plugin-react-swc": "^3.9.0",
|
||||||
"vite": "^6.3.2"
|
"vite": "^6.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"fs-extra": "^11.3.0",
|
"fs-extra": "^11.3.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"moment-timezone": "^0.5.48",
|
"moment-timezone": "^0.5.48",
|
||||||
"mongodb": "^6.15.0",
|
"mongodb": "^6.16.0",
|
||||||
"reggol": "^2.1.0",
|
"reggol": "^2.1.0",
|
||||||
"search-query-parser": "^1.6.0",
|
"search-query-parser": "^1.6.0",
|
||||||
"systeminformation": "^5.25.11"
|
"systeminformation": "^5.25.11"
|
||||||
|
|||||||
Reference in new issue
Block a user