fix ci issues

This commit is contained in:
undefined committed 2025-04-02 00:35:45 +08:00
1 parent 466bc984bd
commit 8af621222c
26 files changed
+108 -85

No files matched your search

+5
View File
@@ -135,6 +135,11 @@ Hydro 用户群:1085853538
- core: 支持存储并显示提交记录重测历史
- core: 新的加域逻辑
- ui: UserSelect.Multi: 支持批量粘贴用户名
- core: oauth: 支持绑定/解绑三方平台账户 (#971)
- ui: 修复暗色模式表格边框 (#968)
- core: 优化 icpc 题目包导入 (#966)
- judge: 弃用 diff (#965)
- core: 支持设置加域时自动加入小组
### Breaking API Changes
+1 -1
View File
@@ -71,7 +71,7 @@ for (const name of ['plugins', 'modules']) {
// Write an empty file to make eslint happy
fs.writeFileSync(path.resolve(process.cwd(), name, 'nop.ts'), 'export default {};\n');
fs.writeFileSync(
path.resolve(process.cwd(), name, 'vue.d.ts'),
path.resolve(process.cwd(), name, 'vue-types.d.ts'),
'declare module "*.vue" {\n const content: any;\n export default content;\n}\n',
);
}
+3 -3
View File
@@ -4,12 +4,12 @@
"license": "MIT",
"main": "base.mjs",
"dependencies": {
"@eslint-react/eslint-plugin": "^1.38.4",
"@eslint-react/eslint-plugin": "^1.40.0",
"@eslint/compat": "^1.2.7",
"@eslint/eslintrc": "^3.3.1",
"@stylistic/eslint-plugin": "^4.2.0",
"@typescript-eslint/eslint-plugin": "^8.28.0",
"@typescript-eslint/parser": "^8.28.0",
"@typescript-eslint/eslint-plugin": "^8.29.0",
"@typescript-eslint/parser": "^8.29.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-plugin-de-morgan": "^1.2.1",
"eslint-plugin-github": "^6.0.0",
+1 -1
View File
@@ -29,7 +29,7 @@ function _buildParam(name: string, source: 'get' | 'post' | 'all' | 'route', ...
if (type[2]) v.isOptional = type[2];
} else if (typeof current === 'boolean') v.isOptional = current;
else if (isValidate) {
if (current !== null) v.validate = mergeValidator(v.validate, current);
if (current !== null) v.validate = v.validate ? mergeValidator(v.validate, current) : current;
isValidate = false;
} else v.convert = current;
cursor++;
+1 -1
View File
@@ -29,6 +29,6 @@
"@types/koa-router": "^7.4.8",
"@types/lodash": "^4.17.16",
"@types/saslprep": "^1.0.3",
"@types/ws": "^8.18.0"
"@types/ws": "^8.18.1"
}
}
+1 -1
View File
@@ -461,7 +461,7 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`);
} catch (e) { }
});
socket.pause();
const c: any = koa.createContext(request, {} as any);
const c: any = koa.createContext(request, {} as any); // eslint-disable-line
await executeMiddlewareStack(c, this.wsLayers);
for (const manager of router.wsStack) {
if (manager.accept(socket, request, c)) return;
+1 -1
View File
@@ -53,7 +53,7 @@
"mongodb-memory-server": "10.1.4",
"nyc": "^17.1.0",
"ora": "^8.2.0",
"oxlint": "^0.16.3",
"oxlint": "^0.16.4",
"package-json": "^10.0.1",
"semver": "^7.7.1",
"simple-git": "^3.27.0",
@@ -99,7 +99,7 @@ const AutoComplete = forwardRef(function Impl<T>(props: AutoCompleteProps<T>, re
const [itemList, setItemList] = useState([]); // items list
const [currentItem, setCurrentItem] = useState(null); // index of current item (in item list)
const [rerender, setRerender] = useState(false);
const [draggableId] = useState(uniqueId());
const [draggableId] = useState(() => uniqueId());
const inputRef = useRef<HTMLInputElement>(null);
const listRef = useRef<HTMLUListElement>(null);
+1 -1
View File
@@ -15,6 +15,6 @@
"@types/diff": "^7.0.2",
"@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.17.16",
"sass": "^1.86.0"
"sass": "^1.86.1"
}
}
+1 -1
View File
@@ -22,6 +22,6 @@
"devDependencies": {
"@types/adm-zip": "^0.5.7",
"@types/shell-quote": "^1.7.5",
"@types/ws": "^8.18.0"
"@types/ws": "^8.18.1"
}
}
+3 -6
View File
@@ -123,9 +123,7 @@ export async function load(ctx: Context) {
ctx.loader.reloadPlugin(require.resolve('../service/server'), 'server'),
]);
require('../lib/index');
await Promise.all([
service(pending, fail, ctx),
]);
await service(pending, fail, ctx);
await builtinModel(ctx);
await model(pending, fail, ctx);
ctx = await new Promise((resolve) => {
@@ -135,8 +133,7 @@ export async function load(ctx: Context) {
});
await addon(pending, fail, ctx);
const scriptDir = path.resolve(__dirname, '..', 'script');
for (const h of await fs.readdir(scriptDir)) {
ctx.loader.reloadPlugin(path.resolve(scriptDir, h), '');
}
await Promise.all((await fs.readdir(scriptDir))
.map((h) => ctx.loader.reloadPlugin(path.resolve(scriptDir, h), '')));
await cli();
}
+6 -11
View File
@@ -49,7 +49,7 @@ export async function apply(ctx: Context) {
resolve(c);
});
});
await require('../service/api').apply(ctx);
await ctx.plugin(require('../service/api'));
require('../lib/index');
ctx.plugin(require('../service/monitor'));
@@ -62,17 +62,12 @@ export async function apply(ctx: Context) {
resolve(c);
});
});
const handlerDir = path.resolve(__dirname, '..', 'handler');
const handlers = await fs.readdir(handlerDir);
for (const h of handlers.filter((i) => i.endsWith('.ts'))) {
ctx.loader.reloadPlugin(path.resolve(handlerDir, h), '');
}
ctx.plugin(require('../service/migration').default);
const loadDir = async (dir: string) => Promise.all((await fs.readdir(dir)).filter((i) => i.endsWith('.ts'))
.map((h) => ctx.loader.reloadPlugin(path.resolve(dir, h), '')));
await loadDir(path.resolve(__dirname, '..', 'handler'));
await ctx.plugin(require('../service/migration').default);
await addon(pending, fail, ctx);
const scriptDir = path.resolve(__dirname, '..', 'script');
for (const h of await fs.readdir(scriptDir)) {
ctx.loader.reloadPlugin(path.resolve(scriptDir, h), '');
}
await loadDir(path.resolve(__dirname, '..', 'script'));
await ctx.parallel('app/started');
if (process.env.NODE_APP_INSTANCE === '0') {
await new Promise((resolve, reject) => {
+2 -2
View File
@@ -254,9 +254,9 @@ class HomeSecurityHandler extends Handler {
@param('platform', Types.String)
async postLinkAccount({ }, platform: string) {
if (!global.Hydro.module.oauth[platform]) throw new ValidationError('platform');
if (!this.ctx.oauth.providers[platform]) throw new ValidationError('platform');
this.session.oauthBind = platform;
await global.Hydro.module.oauth[platform].get.call(this);
await this.ctx.oauth.providers[platform].get.call(this);
}
@param('tokenDigest', Types.String)
+4 -4
View File
@@ -115,7 +115,7 @@ export class JudgeResultCallbackContext {
return this.operationPromise;
}
static async postJudge(rdoc: RecordDoc) {
static async postJudge(rdoc: RecordDoc, context?: JudgeResultCallbackContext) {
if (rdoc.contest?.toString().startsWith('0'.repeat(23))) return;
const accept = rdoc.status === builtin.STATUS.STATUS_ACCEPTED;
const updated = await problem.updateStatus(rdoc.domainId, rdoc.pid, rdoc.uid, rdoc._id, rdoc.status, rdoc.score);
@@ -134,7 +134,7 @@ export class JudgeResultCallbackContext {
problem.inc(pdoc.domainId, pdoc.docId, `stats.s${Math.floor(rdoc.score)}`, 1),
]);
}
await app.parallel('record/judge', rdoc, updated, pdoc, this);
await app.parallel('record/judge', rdoc, updated, pdoc, context);
}
async _end(body: Partial<JudgeResultBody>) {
@@ -155,7 +155,7 @@ export class JudgeResultCallbackContext {
const rdoc = await record.update(this.task.domainId, new ObjectId(this.task.rid as string), $set, $push, $unset);
if (rdoc) bus.broadcast('record/change', rdoc, null, null, body); // trigger a full update
await JudgeResultCallbackContext.postJudge(rdoc);
await JudgeResultCallbackContext.postJudge(rdoc, this);
this.resolve(rdoc);
}
@@ -357,7 +357,7 @@ export async function apply(ctx: Context) {
const priority = await record.submissionPriority(rdoc.uid, -5000 - rdocs.length * 5 - 50);
await record.judge(rdoc.domainId, rdocs.map((r) => r._id), priority, {}, { hackRejudge: input });
} catch (e) {
t.next({
t?.next?.({
rid: rdoc._id.toString(),
domainId: rdoc.domainId,
key: 'next',
+19 -7
View File
@@ -4,7 +4,8 @@ import moment from 'moment-timezone';
import type { Binary } from 'mongodb';
import type { Context } from '../context';
import {
AuthOperationError, BadRequestError, BlacklistedError, BuiltinLoginError, ForbiddenError, InvalidTokenError,
AuthOperationError, BadRequestError, BlacklistedError, BuiltinLoginError,
ForbiddenError, InvalidTokenError, NotFoundError,
SystemError, UserAlreadyExistError, UserFacingError,
UserNotFoundError, ValidationError, VerifyPasswordError,
} from '../error';
@@ -279,9 +280,9 @@ class UserRegisterWithCodeHandler extends Handler {
domainId: string, password: string, verify: string,
uname = '', code: string,
) {
if (global.Hydro.module.oauth[this.tdoc.identity.provider].lockUsername) {
uname = this.tdoc.identity.username;
}
const provider = this.ctx.oauth.providers[this.tdoc.identity.provider];
if (!provider) throw new SystemError(`OAuth provider ${this.tdoc.identity.provider} not found`);
if (provider.lockUsername) uname = this.tdoc.identity.username;
if (!Types.Username[1](uname)) throw new ValidationError('uname');
if (password !== verify) throw new VerifyPasswordError();
const randomEmail = `${String.random(12)}@invalid.local`; // some random email to remove in the future
@@ -428,7 +429,7 @@ class OauthHandler extends Handler {
@param('type', Types.Key)
async get(domainId: string, type: string) {
await global.Hydro.module.oauth[type]?.get.call(this);
await this.ctx.oauth.providers[type]?.get.call(this);
}
}
@@ -436,8 +437,8 @@ class OauthCallbackHandler extends Handler {
noCheckPermView = true;
async get(args: any) {
if (!global.Hydro.module.oauth[args.type]) throw new UserFacingError('Oauth type');
const r = await global.Hydro.module.oauth[args.type].callback.call(this, args);
if (!this.ctx.oauth.providers[args.type]) throw new UserFacingError('Oauth type');
const r = await this.ctx.oauth.providers[args.type].callback.call(this, args);
if (this.session.oauthBind === args.type) {
delete this.session.oauthBind;
const existing = await this.ctx.oauth.get(args.type, r._id);
@@ -529,6 +530,17 @@ export async function apply(ctx: Context) {
if (system.get('server.contestmode')) {
ctx.Route('contest_mode', '/contestmode', ContestModeHandler, PRIV.PRIV_EDIT_SYSTEM);
}
ctx.oauth.provide('mail', {
text: 'Mail',
name: 'mail',
hidden: true,
async get() {
throw new NotFoundError();
},
async callback() {
throw new NotFoundError();
},
});
ctx.inject(['api'], ({ api }) => {
api.value('User', [
['_id', 'Int!'],
+1 -21
View File
@@ -10,7 +10,6 @@ import type { Context } from './context';
import type { DocStatusType } from './model/document';
import type { OauthMap } from './model/oauth';
import type { ProblemDoc } from './model/problem';
import type { Handler } from './service/server';
export * from '@hydrooj/common/types';
@@ -56,17 +55,6 @@ export interface Setting {
flag: number,
}
export interface OAuthUserResponse {
_id: string;
email: string;
avatar?: string;
bio?: string;
uname?: string[];
viewLang?: string;
set?: Record<string, any>;
setInDomain?: Record<string, any>;
}
export interface Authenticator {
name: string;
regat: number;
@@ -506,7 +494,7 @@ export interface OpCountDoc {
opcount: number;
}
export type { OauthMap } from './model/oauth';
export type { OauthMap, OAuthProvider, OAuthUserResponse } from './model/oauth';
export interface DiscussionHistoryDoc {
title?: string;
@@ -622,14 +610,6 @@ export interface UI {
}
export interface ModuleInterfaces {
oauth: {
text: string;
name: string;
icon?: string;
get: (this: Handler) => Promise<void>;
callback: (this: Handler, args: Record<string, any>) => Promise<OAuthUserResponse>;
lockUsername?: boolean;
};
hash: (password: string, salt: string, user: User) => boolean | string | Promise<string>;
problemSearch: ProblemSearch;
}
+33 -1
View File
@@ -1,5 +1,6 @@
import { Collection } from 'mongodb';
import { Context, Service } from '../context';
import type { Handler } from '../service/server';
export interface OauthMap {
platform: string;
@@ -15,9 +16,31 @@ declare module '../context' {
}
}
export interface OAuthUserResponse {
_id: string;
email: string;
avatar?: string;
bio?: string;
uname?: string[];
viewLang?: string;
set?: Record<string, any>;
setInDomain?: Record<string, any>;
}
export interface OAuthProvider {
text: string;
name: string;
icon?: string;
hidden?: boolean;
get: (this: Handler) => Promise<void>;
callback: (this: Handler, args: Record<string, any>) => Promise<OAuthUserResponse>;
lockUsername?: boolean;
}
export default class OauthModel extends Service {
static inject = ['db'];
coll: Collection<OauthMap>;
providers: Record<string, OAuthProvider> = Object.create(null);
constructor(ctx: Context) {
super(ctx, 'oauth');
@@ -29,7 +52,6 @@ export default class OauthModel extends Service {
// { key: { platform: 1, id: 1 }, name: 'platform_id', unique: true },
{ key: { uid: 1, platform: 1 }, name: 'uid_platform' },
);
console.log('oauth init');
}
async get(platform: string, id: string) {
@@ -54,4 +76,14 @@ export default class OauthModel extends Service {
async list(uid: number) {
return this.coll.find({ uid }).toArray();
}
async provide(name: string, provider: OAuthProvider) {
if (this.providers[name]) throw new Error(`OAuth provider ${name} already exists`);
this.ctx.effect(() => {
this.providers[name] = provider;
return () => {
delete this.providers[name];
};
});
}
}
+1 -1
View File
@@ -194,7 +194,7 @@ class ApiService extends Service {
}
export const sideEffect = true;
export const inject = ['server'];
export const inject = ['server', 'setting'];
export function apply(ctx: Context) {
ctx.plugin(ApiService);
+1 -1
View File
@@ -90,7 +90,7 @@ export interface EventMap {
'training/get': (tdoc: TrainingDoc, handler: any) => VoidReturn
'record/change': (rdoc: RecordDoc, $set?: any, $push?: any, body?: any) => void
'record/judge': (rdoc: RecordDoc, updated: boolean, pdoc: ProblemDoc, updater: any) => VoidReturn
'record/judge': (rdoc: RecordDoc, updated: boolean, pdoc?: ProblemDoc, updater?: any) => VoidReturn
}
export function apply(ctx: Context) {
+9 -9
View File
@@ -90,7 +90,7 @@ export async function apply(ctx: Context) {
xhost: system.get('server.xhost'),
});
if (process.env.HYDRO_CLI) return;
ctx.inject(['server'], ({ server, on }) => {
ctx.inject(['server', 'oauth'], ({ server, on, oauth }) => {
server.addHandlerLayer('init', async (c, next) => {
const init = Date.now();
try {
@@ -119,7 +119,7 @@ export async function apply(ctx: Context) {
server.addLayer('base', baseLayer);
server.addLayer('user', userLayer);
let cachedTranslate = null;
const cachedTranslate = ctx.i18n.translate;
server.handlerMixin({
url(name: string, ...kwargsList: Record<string, any>[]) {
@@ -164,7 +164,6 @@ export async function apply(ctx: Context) {
const langs = lang
? [lang, ...this.context.acceptsLanguages()]
: [...this.context.acceptsLanguages(), system.get('server.language')];
cachedTranslate ||= ctx.i18n.translate;
return cachedTranslate(str.toString(), langs);
},
paginate<T>(cursor: FindCursor<T>, page: number, key: string | number) {
@@ -259,12 +258,13 @@ export async function apply(ctx: Context) {
});
on('handler/create/http', async (h) => {
if (!argv.options.benchmark && !h.notUsage) await h.limitRate('global', 5, 100);
h.loginMethods = Object.keys(global.Hydro.module.oauth)
.map((key) => ({
id: key,
icon: global.Hydro.module.oauth[key].icon,
text: global.Hydro.module.oauth[key].text,
name: global.Hydro.module.oauth[key].name,
h.loginMethods = Object.entries(oauth.providers)
.filter(([_, v]) => !v.hidden)
.map(([k, v]) => ({
id: k,
icon: v.icon,
text: v.text,
name: v.name,
}));
if (!h.noCheckPermView && !h.user.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN)) h.checkPerm(PERM.PERM_VIEW);
if (h.context.pendingError) throw h.context.pendingError;
+2 -1
View File
@@ -8,6 +8,7 @@ import {
const icon = '<svg viewBox="0 0 98 96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>';
export default class LoginWithGithubService extends Service {
static inject = ['oauth'];
static Config = Schema.object({
id: Schema.string().description('Github OAuth AppID').required(),
secret: Schema.string().description('Github OAuth Secret').role('secret').required(),
@@ -16,7 +17,7 @@ export default class LoginWithGithubService extends Service {
constructor(ctx: Context, config: ReturnType<typeof LoginWithGithubService.Config>) {
super(ctx, 'oauth.github');
ctx.provideModule('oauth', 'github', {
ctx.oauth.provide('github', {
text: 'Login with Github',
name: 'Github',
icon,
+4 -4
View File
@@ -26,6 +26,7 @@ function decodeJWT(idToken: string) {
}
export default class GoogleOAuthService extends Service {
static inject = ['oauth'];
static Config = Schema.object({
id: Schema.string().description('Google OAuth AppID').required(),
secret: Schema.string().description('Google OAuth Secret').role('secret').required(),
@@ -34,7 +35,7 @@ export default class GoogleOAuthService extends Service {
constructor(ctx: Context, config: ReturnType<typeof GoogleOAuthService.Config>) {
super(ctx, 'oauth.google');
ctx.provideModule('oauth', 'google', {
ctx.oauth.provide('google', {
text: 'Login with Google',
name: 'Google',
callback: async function callback(this: Handler, {
@@ -57,10 +58,9 @@ export default class GoogleOAuthService extends Service {
await TokenModel.del(state, TokenModel.TYPE_OAUTH);
this.response.redirect = s.redirect;
return {
// TODO use openid
_id: payload.email,
_id: payload.sub,
email: payload.email,
uname: [payload.given_name, payload.name, payload.family_name],
uname: [`${payload.given_name} ${payload.family_name}`, payload.name],
viewLang: payload.locale.replace('-', '_'),
};
},
+2 -1
View File
@@ -8,6 +8,7 @@ import {
const icon = '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m20.665 3.717-17.73 6.837c-1.21.486-1.203 1.161-.222 1.462l4.552 1.42 10.532-6.645c.498-.303.953-.14.579.192l-8.533 7.701h-.002l.002.001-.314 4.692c.46 0 .663-.211.921-.46l2.211-2.15 4.599 3.397c.848.467 1.457.227 1.668-.785l3.019-14.228c.309-1.239-.473-1.8-1.282-1.434z"/></svg>';
export default class TelegramService extends Service {
static inject = ['oauth'];
static Config = Schema.object({
token: Schema.string().role('secret').required(),
botLogin: Schema.string().required(),
@@ -16,7 +17,7 @@ export default class TelegramService extends Service {
constructor(ctx: Context, config: ReturnType<typeof TelegramService.Config>) {
super(ctx, 'oauth.telegram');
ctx.provideModule('oauth', 'telegram', {
ctx.oauth.provide('telegram', {
text: 'Login with Telegram',
name: 'Telegram',
icon,
+1 -1
View File
@@ -99,7 +99,7 @@
"redux-promise-middleware": "^6.2.0",
"redux-thunk": "^3.1.0",
"rupture": "^0.7.1",
"sass": "^1.86.0",
"sass": "^1.86.1",
"sass-loader": "^16.0.5",
"schemastery": "^3.16.1",
"schemastery-react": "^0.1.2",
@@ -103,10 +103,9 @@
</div>
</div>
</li>
{%- endif -%}
{%- endfor -%}
</form>
</ul>
{%- endif -%}
{%- endfor -%}
</form>
</ul>
</div>
</div>
@@ -40,6 +40,7 @@
</style>
{% if process.env.DEV %}
<script src="/default.theme.js?version={{ global.Hydro.version['ui-default'] }}"></script>
<link rel="stylesheet" media="all" href="/theme.css?version={{ global.Hydro.version['ui-default'] }}">
{% else %}
<link rel="stylesheet" media="all" href="{{ UiContext.cdn_prefix }}theme-{{ global.Hydro.version['ui-default'] }}.css">
{% endif %}