加域申请管理

This commit is contained in:
undefined committed 2020-08-09 11:07:07 +08:00
1 parent 16108993cf
commit c3ff5856e4
16 files changed
+164 -32

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@
"lint": "eslint packages --ext ts --fix", "lint": "eslint packages --ext ts --fix",
"pub": "node build/publish", "pub": "node build/publish",
"jest": "node build/jest", "jest": "node build/jest",
"start": "node_modules/.bin/hydrooj" "start": "node --async-stack-traces --trace-deprecation --enable-source-maps node_modules/.bin/hydrooj"
}, },
"version": "1.0.0", "version": "1.0.0",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
-1
View File
@@ -4,4 +4,3 @@ SETTINGS_PRIVACY.allowRegisteredUsers
model.setting.ui.name model.setting.ui.name
pdoc.secretConfig? pdoc.secretConfig?
message.markAsRead message.markAsRead
domain_join_application
+1 -1
View File
@@ -237,6 +237,7 @@ Domain ID: 域 ID
domain_dashboard: 管理域 domain_dashboard: 管理域
domain_discussion: 讨论节点 domain_discussion: 讨论节点
domain_edit: 编辑域资料 domain_edit: 编辑域资料
domain_join_applications: 加域申请
domain_join: 加入域 domain_join: 加入域
domain_main: 首页 domain_main: 首页
domain_permission: 管理权限 domain_permission: 管理权限
@@ -408,7 +409,6 @@ Lucky: 手气不错
manage_dashboard: 控制面板 manage_dashboard: 控制面板
manage_join_applications: 加域申请 manage_join_applications: 加域申请
manage_module: 管理模块 manage_module: 管理模块
manage_script: 脚本管理
manage_setting: 系统设置 manage_setting: 系统设置
manage: 管理 manage: 管理
Manage: 管理 Manage: 管理
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "hydrooj", "name": "hydrooj",
"version": "2.12.6", "version": "2.12.7",
"bin": "bin/hydrooj.js", "bin": "bin/hydrooj.js",
"main": "dist/loader.js", "main": "dist/loader.js",
"typings": "dist/loader.d.ts", "typings": "dist/loader.d.ts",
+9 -5
View File
@@ -13,11 +13,15 @@ const lockfile = path.resolve(tmpdir, 'lock.json');
export async function load(call: Entry) { export async function load(call: Entry) {
if (fs.existsSync(lockfile) && !argv.ignorelock) { if (fs.existsSync(lockfile) && !argv.ignorelock) {
const file = require(lockfile); try {
const plist = await pslist(); const file = require(lockfile);
if (file.pid && plist.map((i) => i.pid).includes(file.pid)) { const plist = await pslist();
console.error(`Lockfile exists, pid=${file.pid}`); if (file.pid && plist.map((i) => i.pid).includes(file.pid)) {
process.exit(1); console.error(`Lockfile exists, pid=${file.pid}`);
process.exit(1);
}
} catch {
// Invalid lockfile. ignore.
} }
} }
const context = { const context = {
+6
View File
@@ -84,6 +84,9 @@ export const HomeworkNotLiveError = Err('HomeworkNotLiveError', ForbiddenError,
export const HomeworkNotAttendedError = Err('HomeworkNotAttendedError', ForbiddenError, "You haven't claimed this homework yet."); export const HomeworkNotAttendedError = Err('HomeworkNotAttendedError', ForbiddenError, "You haven't claimed this homework yet.");
export const RoleAlreadyExistError = Err('RoleAlreadyExistError', ForbiddenError, 'This role already exists.'); export const RoleAlreadyExistError = Err('RoleAlreadyExistError', ForbiddenError, 'This role already exists.');
export const CsrfTokenError = Err('CsrfTokenError', ForbiddenError, 'CsrfTokenError'); export const CsrfTokenError = Err('CsrfTokenError', ForbiddenError, 'CsrfTokenError');
export const DomainJoinForbiddenError = Err('DomainJoinForbiddenError', ForbiddenError, 'You are not allowed to join the domain. The link is either invalid or expired.');
export const DomainJoinAlreadyMemberError = Err('DomainJoinAlreadyMemberError', ForbiddenError, 'Failed to join the domain. You are already a member.');
export const InvalidJoinInvitationCodeError = Err('InvalidJoinInvitationCodeError', ForbiddenError, 'The invitation code you provided is invalid.');
export const UserNotFoundError = Err('UserNotFoundError', NotFoundError, 'User {0} not found.'); export const UserNotFoundError = Err('UserNotFoundError', NotFoundError, 'User {0} not found.');
export const NoProblemError = Err('NoProblemError', NotFoundError, 'No problem.'); export const NoProblemError = Err('NoProblemError', NotFoundError, 'No problem.');
@@ -132,6 +135,9 @@ global.Hydro.error = {
ContestNotLiveError, ContestNotLiveError,
ContestScoreboardHiddenError, ContestScoreboardHiddenError,
ContestAlreadyAttendedError, ContestAlreadyAttendedError,
DomainJoinAlreadyMemberError,
DomainJoinForbiddenError,
InvalidJoinInvitationCodeError,
UserFacingError, UserFacingError,
SystemError, SystemError,
TrainingAlreadyEnrollError, TrainingAlreadyEnrollError,
+2 -2
View File
@@ -24,7 +24,7 @@ const ContestHandler = contest.ContestHandlerMixin(Handler);
class ContestListHandler extends ContestHandler { class ContestListHandler extends ContestHandler {
@param('rule', Types.String, true) @param('rule', Types.String, true)
@param('page', Types.UnsignedInt, true) @param('page', Types.PositiveInt, true)
async get(domainId: string, rule = '', page = 1) { async get(domainId: string, rule = '', page = 1) {
this.response.template = 'contest_main.html'; this.response.template = 'contest_main.html';
let tdocs; let tdocs;
@@ -55,7 +55,7 @@ class ContestListHandler extends ContestHandler {
class ContestDetailHandler extends ContestHandler { class ContestDetailHandler extends ContestHandler {
@param('tid', Types.ObjectID) @param('tid', Types.ObjectID)
@param('page', Types.UnsignedInt, true) @param('page', Types.PositiveInt, true)
async get(domainId: string, tid: ObjectID, page = 1) { async get(domainId: string, tid: ObjectID, page = 1) {
const tdoc = await contest.get(domainId, tid); const tdoc = await contest.get(domainId, tid);
this.response.template = 'contest_detail.html'; this.response.template = 'contest_detail.html';
+3 -3
View File
@@ -71,7 +71,7 @@ class DiscussionHandler extends Handler {
} }
class DiscussionMainHandler extends Handler { class DiscussionMainHandler extends Handler {
@param('page', Types.UnsignedInt, true) @param('page', Types.PositiveInt, true)
async get(domainId: string, page = 1) { async get(domainId: string, page = 1) {
const [ddocs, dpcount] = await paginate( const [ddocs, dpcount] = await paginate(
discussion.getMulti(domainId), discussion.getMulti(domainId),
@@ -99,7 +99,7 @@ class DiscussionMainHandler extends Handler {
class DiscussionNodeHandler extends DiscussionHandler { class DiscussionNodeHandler extends DiscussionHandler {
@param('type', Types.String) @param('type', Types.String)
@param('name', Types.String) @param('name', Types.String)
@param('page', Types.UnsignedInt, true) @param('page', Types.PositiveInt, true)
async get(domainId: string, type: string, _name: string, page = 1) { async get(domainId: string, type: string, _name: string, page = 1) {
let name: ObjectID | string | number; let name: ObjectID | string | number;
if (ObjectID.isValid(_name)) name = new ObjectID(_name); if (ObjectID.isValid(_name)) name = new ObjectID(_name);
@@ -177,7 +177,7 @@ class DiscussionCreateHandler extends DiscussionHandler {
class DiscussionDetailHandler extends DiscussionHandler { class DiscussionDetailHandler extends DiscussionHandler {
@param('did', Types.ObjectID) @param('did', Types.ObjectID)
@param('page', Types.UnsignedInt, true) @param('page', Types.PositiveInt, true)
async get(domainId: string, did: ObjectID, page = 1) { async get(domainId: string, did: ObjectID, page = 1) {
const dsdoc = this.user.hasPriv(PRIV.PRIV_USER_PROFILE) const dsdoc = this.user.hasPriv(PRIV.PRIV_USER_PROFILE)
? await discussion.getStatus(domainId, did, this.user._id) ? await discussion.getStatus(domainId, did, this.user._id)
+77 -2
View File
@@ -1,4 +1,8 @@
import { RoleAlreadyExistError, ValidationError } from '../error'; import moment from 'moment-timezone';
import {
RoleAlreadyExistError, ValidationError, DomainJoinForbiddenError,
DomainJoinAlreadyMemberError, InvalidJoinInvitationCodeError,
} from '../error';
import * as user from '../model/user'; import * as user from '../model/user';
import * as domain from '../model/domain'; import * as domain from '../model/domain';
import { DOMAIN_SETTINGS, DOMAIN_SETTINGS_BY_KEY } from '../model/setting'; import { DOMAIN_SETTINGS, DOMAIN_SETTINGS_BY_KEY } from '../model/setting';
@@ -10,7 +14,7 @@ import {
} from '../service/server'; } from '../service/server';
class DomainRankHandler extends Handler { class DomainRankHandler extends Handler {
@param('page', Types.UnsignedInt, true) @param('page', Types.PositiveInt, true)
async get(domainId: string, page = 1) { async get(domainId: string, page = 1) {
const [dudocs, upcount, ucount] = await paginate( const [dudocs, upcount, ucount] = await paginate(
domain.getMultiInDomain(domainId).sort({ rp: -1 }), domain.getMultiInDomain(domainId).sort({ rp: -1 }),
@@ -184,6 +188,75 @@ class DomainRoleHandler extends ManageHandler {
} }
} }
class DomainJoinApplicationsHandler extends ManageHandler {
async get() {
const r = await domain.getRoles(this.domain);
const roles = r.map((role) => role._id).sort();
const rolesWithText = roles.map((role) => [role, role]);
const joinSettings = domain.getJoinSettings(this.domain, roles);
const expirations = { ...domain.JOIN_EXPIRATION_RANGE };
if (!joinSettings) delete expirations[domain.JOIN_EXPIRATION_KEEP_CURRENT];
this.response.template = 'domain_join_applications.html';
this.response.body = { rolesWithText, joinSettings, expirations };
}
@param('method', Types.UnsignedInt)
@param('role', Types.String, true)
@param('expire', Types.UnsignedInt, true)
@param('invitationCode', Types.String, true)
async post(domainId: string, method: number, role: string, expire: number, invitationCode = '') {
const r = await domain.getRoles(this.domain);
const roles = r.map((rl) => rl._id);
const current = domain.getJoinSettings(this.domain, roles);
if (!domain.JOIN_METHOD_RANGE[method]) throw new ValidationError('method');
let joinSettings;
if (method === domain.JOIN_METHOD_NONE) joinSettings = null;
else {
if (!roles.includes(role)) throw new ValidationError('role');
if (!domain.JOIN_EXPIRATION_RANGE[expire]) throw new ValidationError('expire');
if (!current && expire === domain.JOIN_EXPIRATION_KEEP_CURRENT) throw new ValidationError('expire');
joinSettings = { method, role };
if (method === domain.JOIN_METHOD_CODE) joinSettings.code = invitationCode;
if (expire === domain.JOIN_EXPIRATION_KEEP_CURRENT) {
joinSettings.expire = current.expire;
} else if (expire === domain.JOIN_EXPIRATION_UNLIMITED) joinSettings.expire = null;
else joinSettings.expire = moment().add(expire, 'hours').toDate();
}
await domain.edit(this.domain._id, { join: joinSettings });
this.back();
}
}
class DomainJoinHandler extends Handler {
joinSettings: any;
async prepare() {
const r = await domain.getRoles(this.domain);
const roles = r.map((role) => role._id);
this.joinSettings = domain.getJoinSettings(this.domain, roles);
if (!this.joinSettings) throw new DomainJoinForbiddenError(this.domain._id);
if (this.user.role !== 'default') throw new DomainJoinAlreadyMemberError(this.domain._id, this.user._id);
}
// @base.require_priv(builtin.PRIV_USER_PROFILE)
@param('code', Types.String, true)
async get(domainId: string, code: string) {
this.response.template = 'domain_join.html';
this.response.body = { joinSettings: this.joinSettings, code };
}
@param('code', Types.String, true)
async post(domainId: string, code: string) {
if (this.joinSettings.method === domain.JOIN_METHOD_CODE) {
if (this.joinSettings.code !== code) {
throw new InvalidJoinInvitationCodeError(this.domain._id);
}
}
await domain.setUserRole(this.domain._id, this.user._id, this.joinSettings.role);
this.response.redirect = this.url('domain_main');
}
}
class DomainSearchHandler extends Handler { class DomainSearchHandler extends Handler {
@param('q', Types.String) @param('q', Types.String)
async get(domainId: string, q: string) { async get(domainId: string, q: string) {
@@ -202,6 +275,8 @@ export async function apply() {
Route('domain_user', '/domain/user', DomainUserHandler); Route('domain_user', '/domain/user', DomainUserHandler);
Route('domain_permission', '/domain/permission', DomainPermissionHandler); Route('domain_permission', '/domain/permission', DomainPermissionHandler);
Route('domain_role', '/domain/role', DomainRoleHandler); Route('domain_role', '/domain/role', DomainRoleHandler);
Route('domain_join_applications', '/domain/join_applications', DomainJoinApplicationsHandler);
Route('domain_join', '/domain/join', DomainJoinHandler, PRIV.PRIV_USER_PROFILE);
Route('domain_search', '/domain/search', DomainSearchHandler, PRIV.PRIV_USER_PROFILE); Route('domain_search', '/domain/search', DomainSearchHandler, PRIV.PRIV_USER_PROFILE);
} }
+1 -1
View File
@@ -55,7 +55,7 @@ class HomeworkMainHandler extends HomeworkHandler {
class HomeworkDetailHandler extends HomeworkHandler { class HomeworkDetailHandler extends HomeworkHandler {
@param('tid', Types.ObjectID) @param('tid', Types.ObjectID)
@param('page', Types.UnsignedInt, true) @param('page', Types.PositiveInt, true)
async get(domainId: string, tid: ObjectID, page = 1) { async get(domainId: string, tid: ObjectID, page = 1) {
const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK); const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK);
const [tsdoc, pdict] = await Promise.all([ const [tsdoc, pdict] = await Promise.all([
+3 -3
View File
@@ -57,7 +57,7 @@ class ProblemHandler extends Handler {
} }
class ProblemMainHandler extends ProblemHandler { class ProblemMainHandler extends ProblemHandler {
@param('page', Types.UnsignedInt, true) @param('page', Types.PositiveInt, true)
@param('q', Types.String, true) @param('q', Types.String, true)
async get(domainId: string, page = 1, q = '') { async get(domainId: string, page = 1, q = '') {
this.response.template = 'problem_main.html'; this.response.template = 'problem_main.html';
@@ -103,7 +103,7 @@ class ProblemMainHandler extends ProblemHandler {
} }
class ProblemCategoryHandler extends ProblemHandler { class ProblemCategoryHandler extends ProblemHandler {
@param('page', Types.UnsignedInt, true) @param('page', Types.PositiveInt, true)
@param('category', Types.String, null, parseCategory) @param('category', Types.String, null, parseCategory)
async get(domainId: string, page = 1, category: string[]) { async get(domainId: string, page = 1, category: string[]) {
this.response.template = 'problem_main.html'; this.response.template = 'problem_main.html';
@@ -469,7 +469,7 @@ class ProblemDataDownloadHandler extends ProblemDetailHandler {
} }
class ProblemSolutionHandler extends ProblemDetailHandler { class ProblemSolutionHandler extends ProblemDetailHandler {
@param('page', Types.UnsignedInt, true) @param('page', Types.PositiveInt, true)
async get(domainId: string, page = 1) { async get(domainId: string, page = 1) {
this.response.template = 'problem_solution.html'; this.response.template = 'problem_solution.html';
this.checkPerm(PERM.PERM_VIEW_PROBLEM_SOLUTION); this.checkPerm(PERM.PERM_VIEW_PROBLEM_SOLUTION);
+1 -1
View File
@@ -15,7 +15,7 @@ import { Rdoc } from '../interface';
const RecordHandler = contest.ContestHandlerMixin(Handler); const RecordHandler = contest.ContestHandlerMixin(Handler);
class RecordListHandler extends RecordHandler { class RecordListHandler extends RecordHandler {
@param('page', Types.UnsignedInt, true) @param('page', Types.PositiveInt, true)
@param('pid', Types.String, true) @param('pid', Types.String, true)
@param('tid', Types.ObjectID, true) @param('tid', Types.ObjectID, true)
@param('uidOrName', Types.String, true) @param('uidOrName', Types.String, true)
+1 -1
View File
@@ -54,7 +54,7 @@ async function _parseDagJson(domainId, dag) {
} }
class TrainingMainHandler extends Handler { class TrainingMainHandler extends Handler {
@param('page', Types.UnsignedInt, true) @param('page', Types.PositiveInt, true)
async get(domainId: string, page = 1) { async get(domainId: string, page = 1) {
const [tdocs, tpcount] = await paginate( const [tdocs, tpcount] = await paginate(
training.getMulti(domainId), training.getMulti(domainId),
+8 -4
View File
@@ -106,6 +106,13 @@ async function executeCommand(input: string) {
} }
} }
async function reload(count = 1) {
console.log('Reloading');
await stopWorker();
console.log('Worker stopped');
await startWorker(count);
}
async function messageHandler(worker: cluster.Worker, msg: any) { async function messageHandler(worker: cluster.Worker, msg: any) {
if (!msg) msg = worker; if (!msg) msg = worker;
if (msg.event) { if (msg.event) {
@@ -120,10 +127,7 @@ async function messageHandler(worker: cluster.Worker, msg: any) {
} else if (msg.event === 'stat') { } else if (msg.event === 'stat') {
global.Hydro.stat.reqCount += msg.count; global.Hydro.stat.reqCount += msg.count;
} else if (msg.event === 'restart') { } else if (msg.event === 'restart') {
console.log('Restarting'); await reload(msg.count);
await stopWorker();
console.log('Worker stopped');
await startWorker(msg.count);
} else if (msg.event === 'run') { } else if (msg.event === 'run') {
await executeCommand(msg.command); await executeCommand(msg.command);
} }
+47 -2
View File
@@ -1,11 +1,34 @@
import { Dictionary } from 'lodash'; import { Dictionary } from 'lodash';
import { Collection } from 'mongodb';
import { BUILTIN_ROLES, PRIV } from './builtin'; import { BUILTIN_ROLES, PRIV } from './builtin';
import { DomainDoc } from '../interface'; import { DomainDoc } from '../interface';
import * as db from '../service/db'; import * as db from '../service/db';
const coll = db.collection('domain'); const coll: Collection<DomainDoc> = db.collection('domain');
const collUser = db.collection('domain.user'); const collUser = db.collection('domain.user');
export const JOIN_METHOD_NONE = 0;
export const JOIN_METHOD_ALL = 1;
export const JOIN_METHOD_CODE = 2;
export const JOIN_METHOD_RANGE = {
[JOIN_METHOD_NONE]: 'No user is allowed to join this domain',
[JOIN_METHOD_ALL]: 'Any user is allowed to join this domain',
[JOIN_METHOD_CODE]: 'Any user is allowed to join this domain with an invitation code',
};
export const JOIN_EXPIRATION_KEEP_CURRENT = 0;
export const JOIN_EXPIRATION_UNLIMITED = -1;
export const JOIN_EXPIRATION_RANGE = {
[JOIN_EXPIRATION_KEEP_CURRENT]: 'Keep current expiration',
3: 'In 3 hours',
24: 'In 1 day',
[24 * 3]: 'In 3 days',
[24 * 7]: 'In 1 week',
[24 * 30]: 'In 1 month',
[JOIN_EXPIRATION_UNLIMITED]: 'Never expire',
};
export async function add(domainId: string, owner: number, name: string, bulletin: string) { export async function add(domainId: string, owner: number, name: string, bulletin: string) {
const ddoc: DomainDoc = { const ddoc: DomainDoc = {
_id: domainId, _id: domainId,
@@ -32,9 +55,11 @@ export function edit(domainId: string, $set: any) {
return coll.updateOne({ _id: domainId }, { $set }); return coll.updateOne({ _id: domainId }, { $set });
} }
export async function inc(domainId: string, field: string, n: number): Promise<number | null> { export async function inc(domainId: string, field: any, n: number): Promise<number | null> {
const res = await coll.findOneAndUpdate( const res = await coll.findOneAndUpdate(
{ _id: domainId }, { _id: domainId },
// FIXME
// @ts-ignore
{ $inc: { [field]: n } }, { $inc: { [field]: n } },
{ returnOriginal: false }, { returnOriginal: false },
); );
@@ -52,6 +77,8 @@ export function setUserRole(domainId: string, uid: number, role: string) {
return collUser.updateOne({ uid, domainId }, { role }, { upsert: true }); return collUser.updateOne({ uid, domainId }, { role }, { upsert: true });
} }
export async function getRoles(domainId: string): Promise<any[]>
export async function getRoles(domain: DomainDoc): Promise<any[]>
export async function getRoles(arg: string | DomainDoc) { export async function getRoles(arg: string | DomainDoc) {
let ddoc: DomainDoc; let ddoc: DomainDoc;
if (typeof arg === 'string') ddoc = await get(arg); if (typeof arg === 'string') ddoc = await get(arg);
@@ -144,6 +171,15 @@ export async function getDictUserByDomainId(uid: number) {
return dudict; return dudict;
} }
export function getJoinSettings(ddoc: DomainDoc, roles: string[]) {
if (!ddoc.join) return null;
const joinSettings = ddoc.join;
if (joinSettings.method === JOIN_METHOD_NONE) return null;
if (!roles.includes(joinSettings.role)) return null;
if (joinSettings.expire && joinSettings.expire < new Date()) return null;
return joinSettings;
}
export async function getPrefixSearch(prefix: string, limit = 50) { export async function getPrefixSearch(prefix: string, limit = 50) {
const $regex = new RegExp(prefix, 'mi'); const $regex = new RegExp(prefix, 'mi');
const ddocs = await coll.find({ const ddocs = await coll.find({
@@ -153,6 +189,14 @@ export async function getPrefixSearch(prefix: string, limit = 50) {
} }
global.Hydro.model.domain = { global.Hydro.model.domain = {
JOIN_METHOD_NONE,
JOIN_METHOD_ALL,
JOIN_METHOD_CODE,
JOIN_METHOD_RANGE,
JOIN_EXPIRATION_KEEP_CURRENT,
JOIN_EXPIRATION_UNLIMITED,
JOIN_EXPIRATION_RANGE,
getRoles, getRoles,
add, add,
inc, inc,
@@ -170,5 +214,6 @@ global.Hydro.model.domain = {
incUserInDomain, incUserInDomain,
getMultiInDomain, getMultiInDomain,
getDictUserByDomainId, getDictUserByDomainId,
getJoinSettings,
getPrefixSearch, getPrefixSearch,
}; };
+3 -4
View File
@@ -47,15 +47,15 @@ interface ParamOption {
} }
// eslint-disable-next-line no-shadow // eslint-disable-next-line no-shadow
export enum Types { String, Int, UnsignedInt, Float, ObjectID, Boolean, Date, Time } export enum Types { String, Int, UnsignedInt, PositiveInt, Float, ObjectID, Boolean, Date, Time }
const Tools: Array<[Converter, Validator, boolean?]> = [ const Tools: Array<[Converter, Validator, boolean?]> = [
[(v) => v.toString(), null], [(v) => v.toString(), null],
[(v) => parseInt(v, 10), (v) => isSafeInteger(parseInt(v, 10))], [(v) => parseInt(v, 10), (v) => isSafeInteger(parseInt(v, 10))],
[(v) => parseInt(v, 10), (v) => parseInt(v, 10) >= 0],
[(v) => parseInt(v, 10), (v) => parseInt(v, 10) > 0], [(v) => parseInt(v, 10), (v) => parseInt(v, 10) > 0],
[(v) => parseFloat(v), (v) => { [(v) => parseFloat(v), (v) => {
const t = parseFloat(v); const t = parseFloat(v);
console.log(t);
return t && !Number.isNaN(t) && Number.isFinite(t); return t && !Number.isNaN(t) && Number.isFinite(t);
}], }],
[(v) => new ObjectID(v), ObjectID.isValid], [(v) => new ObjectID(v), ObjectID.isValid],
@@ -117,8 +117,7 @@ export function param(name: string, ...args: any): MethodDecorator {
if (args[cursor] !== null) v.validate = args[cursor]; if (args[cursor] !== null) v.validate = args[cursor];
isValidate = false; isValidate = false;
} else { } else {
const I = args[cursor]; v.convert = args[cursor];
v.convert = I;
} }
cursor++; cursor++;
} }