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.
Hydro/hydro/handler/home.ts

373 lines
14 KiB
TypeScript

import { ObjectID } from 'mongodb';
import {
VerifyPasswordError, UserAlreadyExistError, InvalidTokenError,
NotFoundError,
UserNotFoundError,
PermissionError,
} from '../error';
import * as bus from '../service/bus';
import {
Route, Connection, Handler, ConnectionHandler, param, Types,
} from '../service/server';
import * as misc from '../lib/misc';
import md5 from '../lib/md5';
import * as mail from '../lib/mail';
import * as contest from '../model/contest';
import * as message from '../model/message';
import * as document from '../model/document';
import * as system from '../model/system';
import * as user from '../model/user';
import * as file from '../model/file';
import * as setting from '../model/setting';
import * as domain from '../model/domain';
import * as discussion from '../model/discussion';
import * as token from '../model/token';
import * as training from '../model/training';
import { PERM, PRIV } from '../model/builtin';
import {
isContent, isPassword, isEmail, isTitle,
} from '../lib/validator';
const { geoip, useragent } = global.Hydro.lib;
class HomeHandler extends Handler {
4 years ago
async homework(domainId) {
if (this.user.hasPerm(PERM.PERM_VIEW_HOMEWORK)) {
4 years ago
const tdocs = await contest.getMulti(domainId, {}, document.TYPE_HOMEWORK)
4 years ago
.sort('beginAt', -1)
4 years ago
.limit(await system.get('HOMEWORK_ON_MAIN'))
.toArray();
const tsdict = await contest.getListStatus(
domainId, this.user._id,
tdocs.map((tdoc) => tdoc.docId), document.TYPE_HOMEWORK,
);
return [tdocs, tsdict];
}
return [[], {}];
}
async contest(domainId) {
if (this.user.hasPerm(PERM.PERM_VIEW_CONTEST)) {
const tdocs = await contest.getMulti(domainId)
4 years ago
.sort('beginAt', -1)
5 years ago
.limit(await system.get('CONTESTS_ON_MAIN'))
5 years ago
.toArray();
const tsdict = await contest.getListStatus(
domainId, this.user._id, tdocs.map((tdoc) => tdoc.docId),
5 years ago
);
return [tdocs, tsdict];
}
return [[], {}];
}
async training(domainId) {
if (this.user.hasPerm(PERM.PERM_VIEW_TRAINING)) {
const tdocs = await training.getMulti(domainId)
5 years ago
.sort('_id', 1)
5 years ago
.limit(await system.get('TRAININGS_ON_MAIN'))
5 years ago
.toArray();
const tsdict = await training.getListStatus(
domainId, this.user._id, tdocs.map((tdoc) => tdoc.docId),
5 years ago
);
return [tdocs, tsdict];
}
return [[], {}];
}
async discussion(domainId): Promise<[any[], any]> {
if (this.user.hasPerm(PERM.PERM_VIEW_DISCUSSION)) {
const ddocs = await discussion.getMulti(domainId)
5 years ago
.limit(await system.get('DISCUSSIONS_ON_MAIN'))
.toArray();
const vndict = await discussion.getListVnodes(domainId, ddocs, this);
return [ddocs, vndict];
}
5 years ago
return [[], {}];
}
async get({ domainId }) {
4 years ago
const [
[htdocs, htsdict], [tdocs, tsdict],
[trdocs, trsdict], [ddocs, vndict],
] = await Promise.all([
this.homework(domainId), this.contest(domainId),
this.training(domainId), this.discussion(domainId),
5 years ago
]);
const [udict, dodoc, vnodes] = await Promise.all([
user.getList(domainId, ddocs.map((ddoc) => ddoc.owner)),
domain.get(domainId),
discussion.getNodes(domainId),
]);
5 years ago
this.response.template = 'main.html';
this.response.body = {
htdocs,
htsdict,
tdocs,
tsdict,
trdocs,
trsdict,
ddocs,
vndict,
udict,
domain: dodoc,
vnodes,
5 years ago
};
}
}
class HomeSecurityHandler extends Handler {
async get() {
// TODO(iceboy): pagination? or limit session count for uid?
const sessions = await token.getSessionListByUid(this.user._id);
for (const session of sessions) {
session.isCurrent = session._id === this.session._id;
session._id = md5(session._id);
if (useragent) session.updateUa = useragent.parse(session.updateUa || session.createUa || '');
if (geoip) {
session.updateGeoip = geoip.lookup(
session.updateIp || session.createIp,
this.translate('geoip_locale'),
);
}
}
const path = [
['Hydro', 'homepage'],
['home_security', null],
];
this.response.template = 'home_security.html';
this.response.body = { sessions, geoipProvider: geoip?.provider, path };
4 years ago
if (useragent) this.response.body.icon = useragent.icon;
}
@param('current', Types.String)
@param('password', Types.String, isPassword)
@param('verifyPassword', Types.String)
async postChangePassword(current: string, password: string, verifyPassword: string) {
if (password !== verifyPassword) throw new VerifyPasswordError();
await user.changePassword(this.user._id, current, password);
this.back();
}
@param('currentPassword', Types.String)
@param('mail', Types.String, isEmail)
async postChangeMail(domainId: string, currentPassword: string, email: string) {
this.limitRate('send_mail', 3600, 30);
this.user.checkPassword(currentPassword);
const udoc = await user.getByEmail(domainId, email);
if (udoc) throw new UserAlreadyExistError(email);
const [code] = await token.add(
token.TYPE_CHANGEMAIL,
5 years ago
await system.get('changemail_token_expire_seconds'),
{ uid: this.user._id, email },
);
const m = await this.renderHTML('user_changemail_mail.html', {
path: `home/changeMail/${code}`,
uname: this.user.uname,
url_prefix: await system.get('server.url'),
});
await mail.sendMail(email, 'Change Email', 'user_changemail_mail', m);
this.response.template = 'user_changemail_mail_sent.html';
}
@param('tokenDigest', Types.String)
async postDeleteToken(domainId: string, tokenDigest: string) {
const sessions = await token.getSessionListByUid(this.user._id);
5 years ago
for (const session of sessions) {
if (tokenDigest === md5(session._id)) {
// eslint-disable-next-line no-await-in-loop
4 years ago
await token.del(session._id, token.TYPE_SESSION);
return this.back();
}
}
throw new InvalidTokenError(tokenDigest);
}
async postDeleteAllTokens() {
4 years ago
await token.delByUid(this.user._id);
this.response.redirect = this.url('user_login');
}
}
class HomeSettingsHandler extends Handler {
@param('category', Types.String)
async get(domainId: string, category: string) {
const path = [
4 years ago
['Hydro', 'homepage'],
4 years ago
[`home_${category}`, null],
];
this.response.template = 'home_settings.html';
this.response.body = {
category,
page_name: `home_${category}`,
current: this.user,
path,
};
if (category === 'preference') {
this.response.body.settings = setting.PREFERENCE_SETTINGS;
} else if (category === 'account') {
this.response.body.settings = setting.ACCOUNT_SETTINGS;
} else throw new NotFoundError();
}
async post(args: any) {
const $set = {};
for (const key in args) {
if (setting.SETTINGS_BY_KEY[key]
&& !(setting.SETTINGS_BY_KEY[key].flag & setting.FLAG_DISABLED)) {
$set[key] = args[key];
}
}
4 years ago
await user.setById(this.user._id, $set);
this.back();
}
}
class UserChangemailWithCodeHandler extends Handler {
@param('code', Types.String)
async get(domainId: string, code: string) {
const tdoc = await token.get(code, token.TYPE_CHANGEMAIL);
if (!tdoc || tdoc.uid !== this.user._id) {
throw new InvalidTokenError(code);
}
const udoc = await user.getByEmail(domainId, tdoc.email);
if (udoc) throw new UserAlreadyExistError(tdoc.email);
await Promise.all([
user.setEmail(this.user._id, tdoc.email),
4 years ago
token.del(code, token.TYPE_CHANGEMAIL),
]);
this.response.redirect = this.url('home_security');
}
}
class HomeDomainHandler extends Handler {
async get() {
const dudict = await domain.getDictUserByDomainId(this.user._id);
const dids = Object.keys(dudict);
const ddocs = await domain.getMulti({ _id: { $in: dids } }).toArray();
const canManage = {};
for (const ddoc of ddocs) {
// eslint-disable-next-line no-await-in-loop
const udoc = await user.getById(ddoc._id, this.user._id);
canManage[ddoc._id] = udoc.hasPerm(PERM.PERM_EDIT_DOMAIN)
|| udoc.hasPriv(PRIV.PRIV_MANAGE_ALL_DOMAIN);
}
this.response.template = 'home_domain.html';
this.response.body = { ddocs, dudict, canManage };
}
}
class HomeDomainCreateHandler extends Handler {
async get() {
this.response.body = 'home_domain_create.html';
}
@param('id', Types.String)
@param('name', Types.String, isTitle)
@param('bulletin', Types.String, isContent)
@param('gravatar', Types.String, isEmail, true)
async post(_: string, id: string, name: string, bulletin: string, gravatar: string) {
gravatar = gravatar || this.user.gravatar || this.user.mail || 'guest@hydro.local';
const domainId = await domain.add(id, this.user._id, name, bulletin);
await domain.edit(domainId, { gravatar });
this.response.redirect = this.url('domain_manage', { domainId });
this.response.body = { domainId };
}
}
class HomeMessagesHandler extends Handler {
async get() {
// TODO(iceboy): projection, pagination.
4 years ago
const messages = await message.getByUser(this.user._id);
const udict = await user.getList('system', [
4 years ago
...messages.map((mdoc) => mdoc.from),
...messages.map((mdoc) => mdoc.to),
]);
// TODO(twd2): improve here:
4 years ago
const parsed = {};
for (const m of messages) {
const target = m.from === this.user._id ? m.to : m.from;
if (!parsed[target]) {
parsed[target] = {
_id: target,
udoc: { ...udict[target], gravatar: misc.gravatar(udict[target].gravatar) },
messages: [],
};
4 years ago
}
parsed[target].messages.push(m);
}
const path = [
4 years ago
['Hydro', 'homepage'],
['home_messages', null],
];
this.response.body = { messages: parsed, path };
this.response.template = 'home_messages.html';
}
@param('uid', Types.Int)
@param('content', Types.String, isContent)
async postSend(domainId: string, uid: number, content: string) {
const udoc = await user.getById('system', uid);
if (!udoc) throw new UserNotFoundError(uid);
const mdoc = await message.send(this.user._id, uid, content);
// TODO(twd2): improve here: projection
if (this.user._id !== uid) {
bus.publish(`user_message-${uid}`, { mdoc, udoc });
}
this.back({ mdoc, udoc });
}
@param('messageId', Types.ObjectID)
async postDeleteMessage(domainId: string, messageId: ObjectID) {
const msg = await message.get(messageId);
if ([msg.from, msg.to].includes(this.user._id)) await message.del(messageId);
else throw new PermissionError();
this.back();
}
}
class HomeMessagesConnectionHandler extends ConnectionHandler {
async prepare() {
bus.subscribe([`user_message-${this.user._id}`], this, 'onMessageReceived');
}
async onMessageReceived(e) {
this.send(e.value);
}
async cleanup() {
bus.unsubscribe([`user_message-${this.user._id}`], this, 'onMessageReceived');
}
}
class HomeFileHandler extends Handler {
async get() {
const ufdocs = await file.getMulti({ owner: this.user._id }).toArray();
const fdict = await file.getMetaDict(ufdocs.map((ufdoc) => ufdoc._id));
this.response.template = 'home_file.html';
this.response.body = { ufdocs, fdict };
}
async postDelete(ufid) {
const ufdoc = await file.getMeta(ufid);
if (ufdoc.owner !== this.user._id) this.checkPriv(PRIV.PRIV_DELETE_FILE);
else this.checkPriv(PRIV.PRIV_DELETE_FILE_SELF);
const result = await file.del(ufdoc._id);
if (result) await user.inc(this.user._id, 'usage', ufdoc.length);
this.back();
}
}
5 years ago
async function apply() {
Route('homepage', '/', HomeHandler);
Route('home_security', '/home/security', HomeSecurityHandler, PRIV.PRIV_USER_PROFILE);
Route('user_changemail_with_code', '/home/changeMail/:code', UserChangemailWithCodeHandler, PRIV.PRIV_USER_PROFILE);
Route('home_settings', '/home/settings/:category', HomeSettingsHandler, PRIV.PRIV_USER_PROFILE);
Route('home_domain', '/home/domain', HomeDomainHandler, PRIV.PRIV_USER_PROFILE);
Route('home_domain_create', '/home/domain/create', HomeDomainCreateHandler, PRIV.PRIV_CREATE_DOMAIN);
Route('home_messages', '/home/messages', HomeMessagesHandler, PRIV.PRIV_USER_PROFILE);
Route('home_file', '/home/file', HomeFileHandler, PRIV.PRIV_USER_PROFILE);
Connection('home_messages_conn', '/home/messages-conn', HomeMessagesConnectionHandler, PRIV.PRIV_USER_PROFILE);
5 years ago
}
4 years ago
global.Hydro.handler.home = module.exports = apply;