You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
275 lines
10 KiB
TypeScript
275 lines
10 KiB
TypeScript
4 years ago
|
import moment from 'moment-timezone';
|
||
4 years ago
|
import {
|
||
|
UserAlreadyExistError, InvalidTokenError, VerifyPasswordError,
|
||
|
UserNotFoundError, SystemError, BlacklistedError,
|
||
|
UserFacingError,
|
||
|
} from '../error';
|
||
4 years ago
|
import {
|
||
|
Route, Handler, Types, param,
|
||
|
} from '../service/server';
|
||
4 years ago
|
import * as user from '../model/user';
|
||
|
import * as token from '../model/token';
|
||
|
import * as record from '../model/record';
|
||
|
import * as problem from '../model/problem';
|
||
|
import * as task from '../model/task';
|
||
|
import * as system from '../model/system';
|
||
|
import { PERM, PRIV } from '../model/builtin';
|
||
4 years ago
|
import { isEmail, isPassword, isUname } from '../lib/validator';
|
||
4 years ago
|
import { sendMail } from '../lib/mail';
|
||
|
import * as misc from '../lib/misc';
|
||
4 years ago
|
import paginate from '../lib/paginate';
|
||
5 years ago
|
|
||
5 years ago
|
class UserLoginHandler extends Handler {
|
||
|
async get() {
|
||
|
this.response.template = 'user_login.html';
|
||
|
}
|
||
5 years ago
|
|
||
4 years ago
|
@param('uname', Types.String)
|
||
|
@param('password', Types.String)
|
||
4 years ago
|
@param('rememberme', Types.Boolean)
|
||
4 years ago
|
async post(domainId: string, uname: string, password: string, rememberme = false) {
|
||
4 years ago
|
const udoc = await user.getByUname(domainId, uname);
|
||
4 years ago
|
if (!udoc) throw new UserNotFoundError(uname);
|
||
4 years ago
|
udoc.checkPassword(password);
|
||
5 years ago
|
await user.setById(udoc._id, { loginat: new Date(), loginip: this.request.ip });
|
||
4 years ago
|
if (udoc.priv === PRIV.PRIV_NONE) throw new BlacklistedError(uname);
|
||
5 years ago
|
this.session.uid = udoc._id;
|
||
4 years ago
|
this.session.save = rememberme;
|
||
4 years ago
|
this.response.redirect = this.request.referer.endsWith('/login') ? '/' : this.request.referer;
|
||
5 years ago
|
}
|
||
|
}
|
||
5 years ago
|
|
||
5 years ago
|
class UserLogoutHandler extends Handler {
|
||
|
async get() {
|
||
|
this.response.template = 'user_logout.html';
|
||
|
}
|
||
5 years ago
|
|
||
5 years ago
|
async post() {
|
||
4 years ago
|
this.session.uid = 0;
|
||
5 years ago
|
}
|
||
|
}
|
||
5 years ago
|
|
||
5 years ago
|
class UserRegisterHandler extends Handler {
|
||
|
async get() {
|
||
|
this.response.template = 'user_register.html';
|
||
|
}
|
||
5 years ago
|
|
||
4 years ago
|
@param('mail', Types.String, isEmail)
|
||
|
async post(domainId: string, mail: string) {
|
||
4 years ago
|
if (await user.getByEmail('system', mail)) throw new UserAlreadyExistError(mail);
|
||
5 years ago
|
this.limitRate('send_mail', 3600, 30);
|
||
5 years ago
|
const t = await token.add(
|
||
|
token.TYPE_REGISTRATION,
|
||
5 years ago
|
await system.get('registration_token_expire_seconds'),
|
||
5 years ago
|
{ mail },
|
||
|
);
|
||
5 years ago
|
if (await system.get('smtp.user')) {
|
||
4 years ago
|
const m = await this.renderHTML('user_register_mail.html', {
|
||
4 years ago
|
path: `register/${t[0]}`,
|
||
|
url_prefix: await system.get('server.url'),
|
||
4 years ago
|
});
|
||
5 years ago
|
await sendMail(mail, 'Sign Up', 'user_register_mail', m);
|
||
|
this.response.template = 'user_register_mail_sent.html';
|
||
|
} else {
|
||
4 years ago
|
this.response.redirect = this.url('user_register_with_code', { code: t[0] });
|
||
5 years ago
|
}
|
||
|
}
|
||
|
}
|
||
5 years ago
|
|
||
5 years ago
|
class UserRegisterWithCodeHandler extends Handler {
|
||
4 years ago
|
@param('code', Types.String)
|
||
|
async get(domainId: string, code: string) {
|
||
5 years ago
|
this.response.template = 'user_register_with_code.html';
|
||
5 years ago
|
const { mail } = await token.get(code, token.TYPE_REGISTRATION);
|
||
5 years ago
|
if (!mail) throw new InvalidTokenError(token.TYPE_REGISTRATION, code);
|
||
|
this.response.body = { mail };
|
||
|
}
|
||
5 years ago
|
|
||
4 years ago
|
@param('password', Types.String, isPassword)
|
||
|
@param('verifyPassword', Types.String)
|
||
|
@param('uname', Types.String, isUname)
|
||
|
@param('code', Types.String)
|
||
|
async post(
|
||
4 years ago
|
domainId: string, password: string, verify: string,
|
||
4 years ago
|
uname: string, code: string,
|
||
|
) {
|
||
5 years ago
|
const { mail } = await token.get(code, token.TYPE_REGISTRATION);
|
||
5 years ago
|
if (!mail) throw new InvalidTokenError(token.TYPE_REGISTRATION, code);
|
||
4 years ago
|
if (password !== verify) throw new VerifyPasswordError();
|
||
|
const uid = await user.create(mail, uname, password, undefined, this.request.ip);
|
||
4 years ago
|
await token.del(code, token.TYPE_REGISTRATION);
|
||
5 years ago
|
this.session.uid = uid;
|
||
4 years ago
|
this.response.redirect = this.url('homepage');
|
||
5 years ago
|
}
|
||
|
}
|
||
5 years ago
|
|
||
5 years ago
|
class UserLostPassHandler extends Handler {
|
||
|
async get() {
|
||
5 years ago
|
if (!await system.get('smtp.user')) throw new SystemError('Cannot send mail');
|
||
5 years ago
|
this.response.template = 'user_lostpass.html';
|
||
|
}
|
||
5 years ago
|
|
||
4 years ago
|
@param('mail', Types.String, isEmail)
|
||
|
async post(domainId: string, mail: string) {
|
||
5 years ago
|
if (!await system.get('smtp.user')) throw new SystemError('Cannot send mail');
|
||
4 years ago
|
const udoc = await user.getByEmail('system', mail);
|
||
5 years ago
|
if (!udoc) throw new UserNotFoundError(mail);
|
||
4 years ago
|
const [tid] = await token.add(
|
||
5 years ago
|
token.TYPE_LOSTPASS,
|
||
5 years ago
|
await system.get('lostpass_token_expire_seconds'),
|
||
5 years ago
|
{ uid: udoc._id },
|
||
5 years ago
|
);
|
||
4 years ago
|
const m = await this.renderHTML('user_lostpass_mail', { url: `lostpass/${tid}`, uname: udoc.uname });
|
||
5 years ago
|
await sendMail(mail, 'Lost Password', 'user_lostpass_mail', m);
|
||
|
this.response.template = 'user_lostpass_mail_sent.html';
|
||
|
}
|
||
|
}
|
||
5 years ago
|
|
||
5 years ago
|
class UserLostPassWithCodeHandler extends Handler {
|
||
4 years ago
|
async get({ domainId, code }) {
|
||
5 years ago
|
const tdoc = await token.get(code, token.TYPE_LOSTPASS);
|
||
5 years ago
|
if (!tdoc) throw new InvalidTokenError(token.TYPE_LOSTPASS, code);
|
||
4 years ago
|
const udoc = await user.getById(domainId, tdoc.uid);
|
||
5 years ago
|
this.response.body = { uname: udoc.uname };
|
||
|
}
|
||
5 years ago
|
|
||
4 years ago
|
@param('code', Types.String)
|
||
|
@param('password', Types.String, isPassword)
|
||
|
@param('verifyPassword', Types.String)
|
||
|
async post(domainId: string, code: string, password: string, verifyPassword: string) {
|
||
5 years ago
|
const tdoc = await token.get(code, token.TYPE_LOSTPASS);
|
||
5 years ago
|
if (!tdoc) throw new InvalidTokenError(token.TYPE_LOSTPASS, code);
|
||
5 years ago
|
if (password !== verifyPassword) throw new VerifyPasswordError();
|
||
5 years ago
|
await user.setPassword(tdoc.uid, password);
|
||
4 years ago
|
await token.del(code, token.TYPE_LOSTPASS);
|
||
4 years ago
|
this.response.redirect = this.url('homepage');
|
||
5 years ago
|
}
|
||
|
}
|
||
5 years ago
|
|
||
5 years ago
|
class UserDetailHandler extends Handler {
|
||
4 years ago
|
@param('uid', Types.Int)
|
||
|
async get(domainId: string, uid: number) {
|
||
5 years ago
|
const isSelfProfile = this.user._id === uid;
|
||
4 years ago
|
const udoc = await user.getById(domainId, uid);
|
||
|
if (!udoc) throw new UserNotFoundError(uid);
|
||
4 years ago
|
const [sdoc, rdocs, [pdocs, pcount]] = await Promise.all([
|
||
|
token.getMostRecentSessionByUid(uid),
|
||
|
record.getByUid(domainId, uid, 30),
|
||
|
paginate(
|
||
|
problem.getMulti(domainId, { owner: this.user._id }),
|
||
|
1,
|
||
|
100,
|
||
|
),
|
||
|
]);
|
||
4 years ago
|
const pdict = await problem.getList(
|
||
|
domainId, rdocs.map((rdoc) => rdoc.pid),
|
||
4 years ago
|
this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN), false,
|
||
4 years ago
|
);
|
||
4 years ago
|
// Remove sensitive data
|
||
4 years ago
|
if (!isSelfProfile && sdoc) {
|
||
|
sdoc.createIp = '';
|
||
|
sdoc.updateIp = '';
|
||
|
sdoc._id = '';
|
||
|
}
|
||
4 years ago
|
const path = [
|
||
|
['Hydro', 'homepage'],
|
||
|
['user_detail', 'user_detail', { uid }],
|
||
|
];
|
||
5 years ago
|
this.response.template = 'user_detail.html';
|
||
4 years ago
|
this.response.body = {
|
||
4 years ago
|
isSelfProfile, udoc, sdoc, rdocs, pdocs, pcount, pdict, path,
|
||
4 years ago
|
};
|
||
5 years ago
|
}
|
||
5 years ago
|
}
|
||
5 years ago
|
|
||
4 years ago
|
class UserDeleteHandler extends Handler {
|
||
|
async post({ password }) {
|
||
|
this.user.checkPassword(password);
|
||
|
const tid = await task.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';
|
||
|
}
|
||
|
}
|
||
|
|
||
5 years ago
|
class UserSearchHandler extends Handler {
|
||
4 years ago
|
@param('q', Types.String)
|
||
4 years ago
|
@param('exectMatch', Types.Boolean)
|
||
4 years ago
|
async get(domainId: string, q: string, exactMatch = false) {
|
||
4 years ago
|
let udoc = await user.getById(domainId, parseInt(q, 10));
|
||
|
const udocs = udoc ? [udoc] : [];
|
||
|
udoc = await user.getByUname(domainId, q);
|
||
4 years ago
|
if (udoc) udocs.push(udoc);
|
||
4 years ago
|
udoc = await user.getByEmail(domainId, q);
|
||
|
if (udoc) udocs.push(udoc);
|
||
|
if (!exactMatch) udocs.push(...await user.getPrefixList(q, 20));
|
||
5 years ago
|
for (const i in udocs) {
|
||
4 years ago
|
udocs[i].gravatar = misc.gravatar(udocs[i].gravatar || '');
|
||
5 years ago
|
}
|
||
5 years ago
|
this.response.body = udocs;
|
||
5 years ago
|
}
|
||
5 years ago
|
}
|
||
|
|
||
4 years ago
|
class OauthHandler extends Handler {
|
||
4 years ago
|
@param('type', Types.String)
|
||
|
async get(domainId: string, type: string) {
|
||
|
if (global.Hydro.lib[`oauth_${type}`]) await global.Hydro.lib[`oauth_${type}`].get.call(this);
|
||
4 years ago
|
}
|
||
|
}
|
||
|
|
||
|
class OauthCallbackHandler extends Handler {
|
||
4 years ago
|
async get(args: any) {
|
||
4 years ago
|
let r;
|
||
4 years ago
|
if (global.Hydro.lib[`oauth_${args.type}`]) r = await global.Hydro.lib[`oauth_${args.type}`].callback(args);
|
||
4 years ago
|
else throw new UserFacingError('Oauth type');
|
||
4 years ago
|
const udoc = await user.getByEmail('system', r.email);
|
||
4 years ago
|
if (udoc) {
|
||
|
this.session.uid = udoc._id;
|
||
|
} else {
|
||
4 years ago
|
this.checkPriv(PRIV.PRIV_REGISTER_USER);
|
||
4 years ago
|
let username = '';
|
||
|
r.uname = r.uname || [];
|
||
|
r.uname.push(String.random(16));
|
||
|
for (const uname of r.uname) {
|
||
|
// eslint-disable-next-line no-await-in-loop
|
||
4 years ago
|
const nudoc = await user.getByUname('system', uname);
|
||
4 years ago
|
if (!nudoc) {
|
||
|
username = uname;
|
||
|
break;
|
||
4 years ago
|
}
|
||
|
}
|
||
4 years ago
|
const uid = await user.create(
|
||
|
r.email, username, String.random(32),
|
||
|
undefined, this.request.ip,
|
||
|
);
|
||
4 years ago
|
const $set: any = {
|
||
4 years ago
|
oauth: args.type,
|
||
|
};
|
||
|
if (r.bio) $set.bio = r.bio;
|
||
|
if (r.viewLang) $set.viewLang = r.viewLang;
|
||
|
await user.setById(uid, $set);
|
||
|
this.session.uid = uid;
|
||
4 years ago
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
4 years ago
|
export async function apply() {
|
||
4 years ago
|
Route('user_login', '/login', UserLoginHandler);
|
||
4 years ago
|
Route('user_oauth', '/oauth/:type', OauthHandler);
|
||
|
Route('user_oauth_callback', '/oauth/:type/callback', OauthCallbackHandler);
|
||
4 years ago
|
Route('user_register', '/register', UserRegisterHandler, PRIV.PRIV_REGISTER_USER);
|
||
|
Route('user_register_with_code', '/register/:code', UserRegisterWithCodeHandler, PRIV.PRIV_REGISTER_USER);
|
||
|
Route('user_logout', '/logout', UserLogoutHandler, PRIV.PRIV_USER_PROFILE);
|
||
4 years ago
|
Route('user_lostpass', '/lostpass', UserLostPassHandler);
|
||
|
Route('user_lostpass_with_code', '/lostpass/:code', UserLostPassWithCodeHandler);
|
||
4 years ago
|
Route('user_search', '/user/search', UserSearchHandler, PRIV.PRIV_USER_PROFILE);
|
||
4 years ago
|
Route('user_delete', '/user/delete', UserDeleteHandler, PRIV.PRIV_USER_PROFILE);
|
||
4 years ago
|
Route('user_detail', '/user/:uid', UserDetailHandler);
|
||
5 years ago
|
}
|
||
|
|
||
4 years ago
|
global.Hydro.handler.user = apply;
|