mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-18 01:36:08 +08:00
632 lines
27 KiB
TypeScript
632 lines
27 KiB
TypeScript
import { generateAuthenticationOptions, verifyAuthenticationResponse } from '@simplewebauthn/server';
|
|
import { isoBase64URL } from '@simplewebauthn/server/helpers';
|
|
import moment from 'moment-timezone';
|
|
import { Binary } from 'mongodb';
|
|
import Schema from 'schemastery';
|
|
import type { Context } from '../context';
|
|
import {
|
|
AuthOperationError, BadRequestError, BlacklistedError, BuiltinLoginError,
|
|
ForbiddenError, InvalidTokenError, NotFoundError,
|
|
SystemError, UserAlreadyExistError, UserFacingError,
|
|
UserNotFoundError, ValidationError, VerifyPasswordError,
|
|
} from '../error';
|
|
import { TokenDoc, Udoc, User } from '../interface';
|
|
import avatar from '../lib/avatar';
|
|
import { sendMail } from '../lib/mail';
|
|
import { verifyTFA } from '../lib/verifyTFA';
|
|
import BlackListModel from '../model/blacklist';
|
|
import { PERM, PRIV, STATUS } from '../model/builtin';
|
|
import * as ContestModel from '../model/contest';
|
|
import domain from '../model/domain';
|
|
import * as oplog from '../model/oplog';
|
|
import problem, { ProblemDoc } from '../model/problem';
|
|
import ScheduleModel from '../model/schedule';
|
|
import SolutionModel from '../model/solution';
|
|
import * as system from '../model/system';
|
|
import token from '../model/token';
|
|
import user, { deleteUserCache } from '../model/user';
|
|
import {
|
|
Handler, param, post, Query, Types,
|
|
} from '../service/server';
|
|
|
|
async function successfulAuth(this: Handler, udoc: User) {
|
|
await user.setById(udoc._id, { loginat: new Date(), loginip: this.request.ip });
|
|
this.context.HydroContext.user = udoc;
|
|
this.session.viewLang = '';
|
|
this.session.uid = udoc._id;
|
|
this.session.sudo = null;
|
|
this.session.sudoUid = null;
|
|
this.session.scope = PERM.PERM_ALL.toString();
|
|
this.session.oauthBind = null;
|
|
this.session.recreate = true;
|
|
}
|
|
|
|
class UserLoginHandler extends Handler {
|
|
noCheckPermView = true;
|
|
async prepare() {
|
|
if (!system.get('server.login')) throw new BuiltinLoginError();
|
|
}
|
|
|
|
async get() {
|
|
this.response.template = 'user_login.html';
|
|
}
|
|
|
|
@param('uname', Types.Username)
|
|
@param('password', Types.Password)
|
|
@param('rememberme', Types.Boolean)
|
|
@param('redirect', Types.String, true)
|
|
@param('tfa', Types.String, true)
|
|
@param('authnChallenge', Types.String, true)
|
|
async post(
|
|
domainId: string, uname: string, password: string, rememberme = false, redirect = '',
|
|
tfa = '', authnChallenge = '',
|
|
) {
|
|
let udoc = await user.getByEmail(domainId, uname);
|
|
udoc ||= await user.getByUname(domainId, uname);
|
|
if (!udoc) throw new UserNotFoundError(uname);
|
|
if (system.get('system.contestmode') && !udoc.hasPriv(PRIV.PRIV_EDIT_SYSTEM)) {
|
|
if (udoc._loginip && udoc._loginip !== this.request.ip) throw new ValidationError('ip');
|
|
if (system.get('system.contestmode') === 'strict') {
|
|
const udocs = await user.getMulti({ loginip: this.request.ip, _id: { $ne: udoc._id } }).toArray();
|
|
if (udocs.length) throw new ValidationError('ip');
|
|
}
|
|
}
|
|
await Promise.all([
|
|
this.limitRate('user_login', 60, 30),
|
|
this.limitRate('user_login_id', 60, 5, uname),
|
|
oplog.log(this, 'user.login', { redirect }),
|
|
]);
|
|
if (udoc.tfa || udoc.authn) {
|
|
if (udoc.tfa && tfa) {
|
|
if (!verifyTFA(udoc._tfa, tfa)) throw new InvalidTokenError('2FA');
|
|
} else if (udoc.authn && authnChallenge) {
|
|
const challenge = await token.get(authnChallenge, token.TYPE_WEBAUTHN);
|
|
if (!challenge || challenge.uid !== udoc._id) throw new InvalidTokenError(token.TYPE_TEXTS[token.TYPE_WEBAUTHN]);
|
|
if (!challenge.verified) throw new ValidationError('challenge');
|
|
await token.del(authnChallenge, token.TYPE_WEBAUTHN);
|
|
} else throw new ValidationError('2FA', 'Authn');
|
|
}
|
|
await udoc.checkPassword(password);
|
|
if (!udoc.hasPriv(PRIV.PRIV_USER_PROFILE)) throw new BlacklistedError(uname, udoc.banReason);
|
|
await successfulAuth.call(this, udoc);
|
|
this.session.save = rememberme;
|
|
this.response.redirect = redirect || ((this.request.referer || '/login').endsWith('/login')
|
|
? this.url('homepage') : this.request.referer);
|
|
}
|
|
}
|
|
|
|
class UserSudoHandler extends Handler {
|
|
async get() {
|
|
if (!this.session.sudoArgs?.method) throw new ForbiddenError();
|
|
this.response.template = 'user_sudo.html';
|
|
}
|
|
|
|
@param('password', Types.String, true)
|
|
@param('tfa', Types.String, true)
|
|
@param('authnChallenge', Types.String, true)
|
|
async post(domainId: string, password = '', tfa = '', authnChallenge = '') {
|
|
if (!this.session.sudoArgs?.method) throw new ForbiddenError();
|
|
await Promise.all([
|
|
this.limitRate('user_sudo', 60, 5, '{{user}}'),
|
|
oplog.log(this, 'user.sudo', {}),
|
|
]);
|
|
if (this.user.authn && authnChallenge) {
|
|
const challenge = await token.get(authnChallenge, token.TYPE_WEBAUTHN);
|
|
if (challenge?.uid !== this.user._id) throw new InvalidTokenError(token.TYPE_TEXTS[token.TYPE_WEBAUTHN]);
|
|
if (!challenge.verified) throw new ValidationError('challenge');
|
|
await token.del(authnChallenge, token.TYPE_WEBAUTHN);
|
|
} else if (this.user.tfa && tfa) {
|
|
if (!verifyTFA(this.user._tfa, tfa)) throw new InvalidTokenError('2FA');
|
|
} else await this.user.checkPassword(password);
|
|
this.session.sudo = Date.now();
|
|
if (this.session.sudoArgs.method.toLowerCase() !== 'get') {
|
|
this.response.template = 'user_sudo_redirect.html';
|
|
this.response.body = this.session.sudoArgs;
|
|
} else this.response.redirect = this.session.sudoArgs.redirect;
|
|
this.session.sudoArgs.method = null;
|
|
}
|
|
}
|
|
|
|
class UserTFAHandler extends Handler {
|
|
noCheckPermView = true;
|
|
|
|
@param('q', Types.String)
|
|
async get({ }, q: string) {
|
|
let udoc = await user.getByUname('system', q);
|
|
udoc ||= await user.getByEmail('system', q);
|
|
if (!udoc) this.response.body = { tfa: false, authn: false };
|
|
else this.response.body = { tfa: udoc.tfa, authn: udoc.authn };
|
|
}
|
|
}
|
|
|
|
class UserWebauthnHandler extends Handler {
|
|
noCheckPermView = true;
|
|
|
|
getAuthnHost() {
|
|
return system.get('authn.host') && this.request.hostname.includes(system.get('authn.host'))
|
|
? system.get('authn.host') : this.request.hostname;
|
|
}
|
|
|
|
@param('uname', Types.Username, true)
|
|
@param('login', Types.Boolean)
|
|
async get(domainId: string, uname: string, login: boolean) {
|
|
let allowCredentials = [];
|
|
let uid = 0;
|
|
if (!login) {
|
|
const udoc = this.user._id ? this.user : ((await user.getByEmail(domainId, uname)) || await user.getByUname(domainId, uname));
|
|
if (!udoc._id) throw new UserNotFoundError(uname || 'user');
|
|
if (!udoc.authn) throw new AuthOperationError('authn', 'disabled');
|
|
allowCredentials = udoc._authenticators.map((authenticator) => ({
|
|
id: isoBase64URL.fromBuffer(authenticator.credentialID.buffer),
|
|
}));
|
|
uid = udoc._id;
|
|
}
|
|
const options = await generateAuthenticationOptions({
|
|
allowCredentials,
|
|
rpID: this.getAuthnHost(),
|
|
userVerification: 'preferred',
|
|
});
|
|
await token.add(token.TYPE_WEBAUTHN, 60, { uid: login ? 'login' : uid }, options.challenge);
|
|
this.session.challenge = options.challenge;
|
|
this.response.body.authOptions = options;
|
|
}
|
|
|
|
async post({ domainId, result, redirect }) {
|
|
const challenge = this.session.challenge;
|
|
if (!challenge) throw new ForbiddenError();
|
|
const tdoc = await token.get(challenge, token.TYPE_WEBAUTHN);
|
|
if (!tdoc) throw new InvalidTokenError(token.TYPE_TEXTS[token.TYPE_WEBAUTHN]);
|
|
const udoc = await (tdoc.uid === 'login'
|
|
? (async () => {
|
|
const u = await user.coll.findOne({ 'authenticators.credentialID': Binary.createFromBase64(result.id) });
|
|
return u ? await user.getById(domainId, u._id) : null;
|
|
})()
|
|
: user.getById(domainId, tdoc.uid));
|
|
if (!udoc) throw new NotFoundError();
|
|
const parseId = (id: Binary) => Buffer.from(id.toString('hex'), 'hex').toString('base64url');
|
|
const authenticator = udoc._authenticators?.find((c) => parseId(c.credentialID) === result.id);
|
|
if (!authenticator) throw new ValidationError('authenticator');
|
|
const verification = await verifyAuthenticationResponse({
|
|
response: result,
|
|
expectedChallenge: challenge,
|
|
expectedOrigin: this.request.headers.origin,
|
|
expectedRPID: this.getAuthnHost(),
|
|
credential: {
|
|
...authenticator,
|
|
id: isoBase64URL.fromBuffer(authenticator.credentialID.buffer),
|
|
publicKey: authenticator.credentialPublicKey.buffer,
|
|
},
|
|
}).catch(() => null);
|
|
if (!verification?.verified) throw new ValidationError('authenticator');
|
|
authenticator.counter = verification.authenticationInfo.newCounter;
|
|
await user.setById(udoc._id, { authenticators: udoc._authenticators });
|
|
if (tdoc.uid === 'login') {
|
|
await successfulAuth.call(this, await user.getById(domainId, udoc._id));
|
|
await token.del(challenge, token.TYPE_WEBAUTHN);
|
|
this.response.redirect = redirect || ((this.request.referer || '/login').endsWith('/login')
|
|
? this.url('homepage') : this.request.referer);
|
|
} else {
|
|
await token.update(challenge, token.TYPE_WEBAUTHN, 60, { verified: true });
|
|
this.back();
|
|
}
|
|
}
|
|
}
|
|
|
|
class UserLogoutHandler extends Handler {
|
|
noCheckPermView = true;
|
|
|
|
async get() {
|
|
this.response.template = 'user_logout.html';
|
|
}
|
|
|
|
async post({ domainId }) {
|
|
await successfulAuth.call(this, await user.getById(domainId, 0));
|
|
this.response.redirect = '/';
|
|
}
|
|
}
|
|
|
|
// rename to RegisterSendMailHandler
|
|
export class UserRegisterHandler extends Handler {
|
|
noCheckPermView = true;
|
|
async prepare() {
|
|
if (!system.get('server.login')) throw new BuiltinLoginError();
|
|
}
|
|
|
|
async get() {
|
|
this.response.template = 'user_register.html';
|
|
}
|
|
|
|
@post('mail', Types.Email)
|
|
async post({ }, mail: string) {
|
|
if (await user.getByEmail('system', mail)) throw new UserAlreadyExistError(mail);
|
|
const mailDomain = mail.split('@')[1];
|
|
if (await BlackListModel.get(`mail::${mailDomain}`)) throw new BlacklistedError(mailDomain);
|
|
await Promise.all([
|
|
this.limitRate('send_mail', 60, 1, mail),
|
|
this.limitRate('send_mail', 3600, 30),
|
|
oplog.log(this, 'user.register', {}),
|
|
]);
|
|
const t = await token.add(
|
|
token.TYPE_REGISTRATION,
|
|
system.get('session.unsaved_expire_seconds'),
|
|
{
|
|
mail,
|
|
redirect: this.domain.registerRedirect,
|
|
identity: {
|
|
provider: 'mail',
|
|
platform: 'mail',
|
|
id: mail,
|
|
},
|
|
},
|
|
);
|
|
const prefix = this.domain.host
|
|
? `${this.domain.host instanceof Array ? this.domain.host[0] : this.domain.host}`
|
|
: system.get('server.url');
|
|
if (system.get('smtp.verify') && system.get('smtp.user')) {
|
|
const m = await this.renderHTML('user_register_mail.html', {
|
|
path: `/register/${t[0]}`,
|
|
url_prefix: prefix.endsWith('/') ? prefix.slice(0, -1) : prefix,
|
|
});
|
|
await sendMail(mail, 'Sign Up', 'user_register_mail', m.toString());
|
|
this.response.template = 'user_register_mail_sent.html';
|
|
this.response.body = { mail };
|
|
} else this.response.redirect = this.url('user_register_with_code', { code: t[0] });
|
|
}
|
|
}
|
|
|
|
class UserRegisterWithCodeHandler extends Handler {
|
|
noCheckPermView = true;
|
|
tdoc: TokenDoc;
|
|
|
|
@param('code', Types.String)
|
|
async prepare({ }, code: string) {
|
|
this.tdoc = await token.get(code, token.TYPE_REGISTRATION);
|
|
if (!this.tdoc?.identity) {
|
|
// prevent brute forcing tokens
|
|
await this.limitRate('user_register_with_code', 60, 5);
|
|
throw new InvalidTokenError(token.TYPE_TEXTS[token.TYPE_REGISTRATION], code);
|
|
}
|
|
}
|
|
|
|
async get() {
|
|
this.response.template = 'user_register_with_code.html';
|
|
this.response.body = this.tdoc;
|
|
}
|
|
|
|
@param('password', Types.Password)
|
|
@param('verifyPassword', Types.Password)
|
|
@param('uname', Types.Username, true)
|
|
@param('code', Types.String)
|
|
async post(
|
|
domainId: string, password: string, verify: string,
|
|
uname = '', code: string,
|
|
) {
|
|
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
|
|
const uid = await user.create(this.tdoc.mail || randomEmail, uname, password, undefined, this.request.ip);
|
|
await token.del(code, token.TYPE_REGISTRATION);
|
|
const [id, mailDomain] = this.tdoc.mail.split('@');
|
|
const $set: any = this.tdoc.set || {};
|
|
if (mailDomain === 'qq.com' && !Number.isNaN(+id)) $set.avatar = `qq:${id}`;
|
|
if (this.session.viewLang) $set.viewLang = this.session.viewLang;
|
|
if (Object.keys($set).length) await user.setById(uid, $set);
|
|
if (Object.keys(this.tdoc.setInDomain || {}).length) await domain.setUserInDomain(domainId, uid, this.tdoc.setInDomain);
|
|
await this.ctx.oauth.set(this.tdoc.identity.platform, this.tdoc.identity.id, uid);
|
|
await successfulAuth.call(this, await user.getById(domainId, uid));
|
|
this.response.redirect = this.tdoc.redirect || this.url('home_settings', { category: 'preference' });
|
|
}
|
|
}
|
|
|
|
class UserLostPassHandler extends Handler {
|
|
noCheckPermView = true;
|
|
|
|
async get() {
|
|
this.response.template = 'user_lostpass.html';
|
|
}
|
|
|
|
@param('mail', Types.Email)
|
|
async post(domainId: string, mail: string) {
|
|
if (!system.get('smtp.user')) throw new SystemError('Cannot send mail');
|
|
const udoc = await user.getByEmail('system', mail);
|
|
if (!udoc) throw new UserNotFoundError(mail);
|
|
await Promise.all([
|
|
this.limitRate('send_mail', 3600, 30),
|
|
this.limitRate('send_mail', 60, 1, mail),
|
|
oplog.log(this, 'user.lostpass', {}),
|
|
]);
|
|
const [tid] = await token.add(
|
|
token.TYPE_LOSTPASS,
|
|
system.get('session.unsaved_expire_seconds'),
|
|
{ uid: udoc._id },
|
|
);
|
|
const prefix = this.domain.host
|
|
? `${this.domain.host instanceof Array ? this.domain.host[0] : this.domain.host}`
|
|
: system.get('server.url');
|
|
const m = await this.renderHTML('user_lostpass_mail.html', {
|
|
url: `/lostpass/${tid}`,
|
|
url_prefix: prefix.endsWith('/') ? prefix.slice(0, -1) : prefix,
|
|
uname: udoc.uname,
|
|
});
|
|
await sendMail(mail, 'Lost Password', 'user_lostpass_mail', m.toString());
|
|
this.response.template = 'user_lostpass_mail_sent.html';
|
|
}
|
|
}
|
|
|
|
class UserLostPassWithCodeHandler extends Handler {
|
|
noCheckPermView = true;
|
|
|
|
async get({ domainId, code }) {
|
|
const tdoc = await token.get(code, token.TYPE_LOSTPASS);
|
|
if (!tdoc) throw new InvalidTokenError(token.TYPE_TEXTS[token.TYPE_LOSTPASS], code);
|
|
const udoc = await user.getById(domainId, tdoc.uid);
|
|
this.response.body = { uname: udoc.uname };
|
|
this.response.template = 'user_lostpass_with_code.html';
|
|
}
|
|
|
|
@param('code', Types.String)
|
|
@param('password', Types.Password)
|
|
@param('verifyPassword', Types.Password)
|
|
async post(domainId: string, code: string, password: string, verifyPassword: string) {
|
|
const tdoc = await token.get(code, token.TYPE_LOSTPASS);
|
|
if (!tdoc) throw new InvalidTokenError(token.TYPE_TEXTS[token.TYPE_LOSTPASS], code);
|
|
if (password !== verifyPassword) throw new VerifyPasswordError();
|
|
await user.setById(tdoc.uid, { authenticators: [], tfa: false });
|
|
await user.setPassword(tdoc.uid, password);
|
|
await token.del(code, token.TYPE_LOSTPASS);
|
|
this.response.redirect = this.url('homepage');
|
|
}
|
|
}
|
|
|
|
class UserDetailHandler extends Handler {
|
|
@param('uid', Types.Int)
|
|
async get(domainId: string, uid: number) {
|
|
if (uid === 0) throw new UserNotFoundError(0);
|
|
const isSelfProfile = this.user._id === uid;
|
|
const [udoc, sdoc] = await Promise.all([
|
|
user.getById(domainId, uid),
|
|
token.getMostRecentSessionByUid(uid, ['createAt', 'updateAt']),
|
|
]);
|
|
if (!udoc) throw new UserNotFoundError(uid);
|
|
const pdocs: ProblemDoc[] = [];
|
|
const acInfo: Record<string, number> = {};
|
|
const canViewHidden = this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN) || this.user._id;
|
|
if (this.user.hasPerm(PERM.PERM_VIEW_PROBLEM)) {
|
|
const psdocs = await problem.getMultiStatus(domainId, { uid, status: STATUS.STATUS_ACCEPTED }).toArray();
|
|
pdocs.push(...Object.values(
|
|
await problem.getList(
|
|
domainId, psdocs.map((i) => i.docId), canViewHidden,
|
|
false, problem.PROJECTION_LIST, true,
|
|
),
|
|
));
|
|
}
|
|
for (const pdoc of pdocs) {
|
|
for (const tag of pdoc.tag) {
|
|
if (acInfo[tag]) acInfo[tag]++;
|
|
else acInfo[tag] = 1;
|
|
}
|
|
}
|
|
const tags = Object.entries(acInfo).sort((a, b) => b[1] - a[1]).slice(0, 20);
|
|
const tsdocs = await ContestModel.getMultiStatus(domainId, { uid, attend: { $exists: true } }).project({ docId: 1 }).toArray();
|
|
const tdocs = await ContestModel.getMulti(domainId, { docId: { $in: tsdocs.map((i) => i.docId) } })
|
|
.project({ docId: 1, title: 1, rule: 1 }).sort({ _id: -1 }).toArray();
|
|
this.response.template = 'user_detail.html';
|
|
this.response.body = {
|
|
isSelfProfile, udoc, sdoc, pdocs, tags, tdocs,
|
|
};
|
|
if (this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_SOLUTION)) {
|
|
const psdocs = await SolutionModel.getByUser(domainId, uid).limit(10).toArray();
|
|
this.response.body.psdocs = psdocs;
|
|
if (this.user.hasPerm(PERM.PERM_VIEW_PROBLEM)) {
|
|
this.response.body.pdict = await problem.getList(
|
|
domainId, psdocs.map((i) => i.parentId), canViewHidden,
|
|
false, problem.PROJECTION_LIST,
|
|
);
|
|
}
|
|
}
|
|
this.UiContext.extraTitleContent = udoc.uname;
|
|
}
|
|
}
|
|
|
|
class UserDeleteHandler extends Handler {
|
|
async post({ password }) {
|
|
await this.user.checkPassword(password);
|
|
const tid = await ScheduleModel.add({
|
|
executeAfter: moment().add(7, 'days').toDate(),
|
|
type: 'script',
|
|
id: 'deleteUser',
|
|
args: { uid: this.user._id },
|
|
});
|
|
await user.setById(this.user._id, { del: tid });
|
|
this.response.template = 'user_delete_pending.html';
|
|
}
|
|
}
|
|
|
|
class OauthHandler extends Handler {
|
|
noCheckPermView = true;
|
|
|
|
@param('type', Types.Key)
|
|
async get(domainId: string, type: string) {
|
|
await this.ctx.oauth.providers[type]?.get.call(this);
|
|
}
|
|
}
|
|
|
|
class OauthCallbackHandler extends Handler {
|
|
noCheckPermView = true;
|
|
|
|
async get(args: any) {
|
|
const provider = this.ctx.oauth.providers[args.type];
|
|
if (!provider) throw new UserFacingError('Oauth type');
|
|
const r = await provider.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);
|
|
if (existing && existing !== this.user._id) {
|
|
throw new BadRequestError('Already binded to another account');
|
|
}
|
|
this.response.redirect = '/home/security';
|
|
if (existing !== this.user._id) await this.ctx.oauth.set(args.type, r._id, this.user._id);
|
|
return;
|
|
}
|
|
|
|
const uid = await this.ctx.oauth.get(args.type, r._id) || await this.ctx.oauth.get('mail', r.email);
|
|
if (uid) {
|
|
await successfulAuth.call(this, await user.getById('system', uid));
|
|
this.response.redirect = '/';
|
|
return;
|
|
}
|
|
if (!provider.canRegister) throw new ForbiddenError('No binded account found');
|
|
this.checkPriv(PRIV.PRIV_REGISTER_USER);
|
|
let username = '';
|
|
r.uname ||= [];
|
|
const mailDomain = r.email.split('@')[1];
|
|
if (await BlackListModel.get(`mail::${mailDomain}`)) throw new BlacklistedError(mailDomain);
|
|
for (const uname of r.uname) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
const nudoc = await user.getByUname('system', uname);
|
|
if (!nudoc) {
|
|
username = uname;
|
|
break;
|
|
}
|
|
}
|
|
const set: Partial<Udoc> = { ...r.set };
|
|
if (r.bio) set.bio = r.bio;
|
|
if (r.viewLang) set.viewLang = r.viewLang;
|
|
if (r.avatar) set.avatar = r.avatar;
|
|
const [t] = await token.add(
|
|
token.TYPE_REGISTRATION,
|
|
system.get('session.unsaved_expire_seconds'),
|
|
{
|
|
mail: r.email,
|
|
username,
|
|
redirect: this.domain.registerRedirect,
|
|
set,
|
|
setInDomain: r.setInDomain,
|
|
identity: {
|
|
provider: args.type,
|
|
platform: args.type,
|
|
id: r._id,
|
|
},
|
|
},
|
|
);
|
|
this.response.redirect = this.url('user_register_with_code', { code: t });
|
|
}
|
|
}
|
|
|
|
class ContestModeHandler extends Handler {
|
|
async get() {
|
|
const bindings = await user.getMulti({ loginip: { $exists: true } })
|
|
.project<{ _id: number, loginip: string }>({ _id: 1, loginip: 1 }).toArray();
|
|
this.response.body = { bindings };
|
|
this.response.template = 'contest_mode.html';
|
|
}
|
|
|
|
@param('uid', Types.Int, true)
|
|
async postReset(domainId: string, uid: number) {
|
|
if (uid) await user.setById(uid, {}, { loginip: '' });
|
|
else {
|
|
await user.coll.updateMany({}, { $unset: { loginip: 1 } });
|
|
deleteUserCache(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
export const inject = ['oauth'];
|
|
|
|
const UserApi = {
|
|
user: Query(Schema.object({
|
|
id: Schema.number().step(1),
|
|
uname: Schema.string(),
|
|
mail: Schema.string(),
|
|
domainId: Schema.string().required(),
|
|
}), (c, arg) => {
|
|
if (arg.id) return user.getById(arg.domainId, arg.id);
|
|
if (arg.mail) return user.getByEmail(arg.domainId, arg.mail);
|
|
if (arg.uname) return user.getByUname(arg.domainId, arg.uname);
|
|
return user.getById(arg.domainId, c.user._id);
|
|
}),
|
|
users: Query(Schema.object({
|
|
ids: Schema.array(Schema.number().step(1)),
|
|
auto: Schema.array(Schema.string()),
|
|
search: Schema.string(),
|
|
limit: Schema.number().step(1),
|
|
exact: Schema.boolean(),
|
|
}), async (c, arg) => {
|
|
const auto = (arg.ids?.length && arg.ids) || arg.auto || [];
|
|
if (auto.length) {
|
|
const maybeId = auto.filter((i) => !Number.isNaN(+i));
|
|
const result = [];
|
|
if (maybeId.length) {
|
|
const udocs = await user.getList(arg.domainId, maybeId.map((i) => +i));
|
|
for (const i in udocs) udocs[i].avatarUrl = avatar(udocs[i].avatar);
|
|
result.push(...Object.values(udocs));
|
|
}
|
|
const notFound = auto.filter((i) => !result.find((j) => j._id === +i));
|
|
if (notFound.length > 50) return result; // reject if too many
|
|
for (const i of notFound) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
const udoc = await user.getByUname(arg.domainId, i.toString()) || await user.getByEmail(arg.domainId, i.toString());
|
|
if (udoc) result.push(udoc);
|
|
}
|
|
return result;
|
|
}
|
|
if (!arg.search) return [];
|
|
const udoc = await user.getById(arg.domainId, +arg.search)
|
|
|| await user.getByUname(arg.domainId, arg.search)
|
|
|| await user.getByEmail(arg.domainId, arg.search);
|
|
const udocs: User[] = arg.exact
|
|
? []
|
|
: await user.getPrefixList(arg.domainId, arg.search, Math.min(arg.limit || 10, 10));
|
|
if (udoc && !udocs.find((i) => i._id === udoc._id)) {
|
|
udocs.pop();
|
|
udocs.unshift(udoc);
|
|
}
|
|
for (const i in udocs) {
|
|
udocs[i].avatarUrl = avatar(udocs[i].avatar);
|
|
}
|
|
return udocs;
|
|
}),
|
|
} as const;
|
|
|
|
declare module '@hydrooj/framework' {
|
|
interface Apis {
|
|
user: typeof UserApi;
|
|
}
|
|
}
|
|
|
|
export async function apply(ctx: Context) {
|
|
ctx.Route('user_login', '/login', UserLoginHandler);
|
|
ctx.Route('user_oauth', '/oauth/:type', OauthHandler);
|
|
ctx.Route('user_sudo', '/user/sudo', UserSudoHandler, PRIV.PRIV_USER_PROFILE);
|
|
ctx.Route('user_tfa', '/user/tfa', UserTFAHandler);
|
|
ctx.Route('user_webauthn', '/user/webauthn', UserWebauthnHandler);
|
|
ctx.Route('user_oauth_callback', '/oauth/:type/callback', OauthCallbackHandler);
|
|
ctx.Route('user_register', '/register', UserRegisterHandler, PRIV.PRIV_REGISTER_USER);
|
|
ctx.Route('user_register_with_code', '/register/:code', UserRegisterWithCodeHandler, PRIV.PRIV_REGISTER_USER);
|
|
ctx.Route('user_logout', '/logout', UserLogoutHandler, PRIV.PRIV_USER_PROFILE);
|
|
ctx.Route('user_lostpass', '/lostpass', UserLostPassHandler);
|
|
ctx.Route('user_lostpass_with_code', '/lostpass/:code', UserLostPassWithCodeHandler);
|
|
ctx.Route('user_delete', '/user/delete', UserDeleteHandler, PRIV.PRIV_USER_PROFILE);
|
|
ctx.Route('user_detail', '/user/:uid', UserDetailHandler);
|
|
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.provide(UserApi);
|
|
});
|
|
}
|