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/user.js

314 lines
12 KiB
JavaScript

4 years ago
const superagent = require('superagent');
5 years ago
const { Route, Handler } = require('../service/server.js');
const user = require('../model/user');
const token = require('../model/token');
const system = require('../model/system');
const { sendMail } = require('../lib/mail');
const misc = require('../lib/misc');
const {
UserAlreadyExistError, InvalidTokenError, VerifyPasswordError,
UserNotFoundError, LoginError, SystemError,
PermissionError, BlacklistedError, UserFacingError,
5 years ago
} = require('../error');
class UserLoginHandler extends Handler {
async get() {
const [loginWithGithub, loginWithGoogle] = await system.getMany(
['oauth.githubappid', 'oauth.googleappid'],
);
this.response.body = {
loginWithGithub, loginWithGoogle,
};
this.response.template = 'user_login.html';
}
5 years ago
async post({
domainId, uname, password, rememberme = false,
}) {
const udoc = await user.getByUname(domainId, uname);
if (!udoc) throw new LoginError(uname);
if (udoc) udoc.checkPassword(password);
await user.setById(udoc._id, { loginat: new Date(), loginip: this.request.ip });
4 years ago
if (udoc.ban) throw new BlacklistedError(uname);
this.session.uid = udoc._id;
this.session.rememberme = rememberme;
this.response.redirect = this.request.referer.endsWith('/login') ? '/' : this.request.referer;
}
}
class UserLogoutHandler extends Handler {
async get() {
this.response.template = 'user_logout.html';
}
5 years ago
async post() {
this.session.uid = 1;
}
}
class UserRegisterHandler extends Handler {
4 years ago
async prepare() { // eslint-disable-line class-methods-use-this
if (!await system.get('user.register')) {
throw new PermissionError('Register');
}
}
5 years ago
async get() {
this.response.template = 'user_register.html';
}
5 years ago
async post({ mail }) {
if (await user.getByEmail('system', mail, true)) throw new UserAlreadyExistError(mail);
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')) {
const m = await this.renderHTML('user_register_mail.html', {
path: `register/${t[0]}`,
url_prefix: await system.get('server.url'),
});
await sendMail(mail, 'Sign Up', 'user_register_mail', m);
this.response.template = 'user_register_mail_sent.html';
} else {
this.response.redirect = `/register/${t[0]}`;
}
}
}
class UserRegisterWithCodeHandler extends Handler {
async get({ code }) {
this.response.template = 'user_register_with_code.html';
5 years ago
const { mail } = await token.get(code, token.TYPE_REGISTRATION);
if (!mail) throw new InvalidTokenError(token.TYPE_REGISTRATION, code);
this.response.body = { mail };
}
5 years ago
async post({
code, password, verifyPassword, uname,
}) {
const { mail } = await token.get(code, token.TYPE_REGISTRATION);
if (!mail) throw new InvalidTokenError(token.TYPE_REGISTRATION, code);
5 years ago
if (password !== verifyPassword) throw new VerifyPasswordError();
const uid = await system.inc('user');
5 years ago
await user.create({
uid, uname, password, mail, regip: this.request.ip,
});
4 years ago
await token.del(code, token.TYPE_REGISTRATION);
this.session.uid = uid;
this.response.redirect = '/';
}
}
class UserLostPassHandler extends Handler {
async get() {
5 years ago
if (!await system.get('smtp.user')) throw new SystemError('Cannot send mail');
this.response.template = 'user_lostpass.html';
}
5 years ago
async post({ mail }) {
5 years ago
if (!await system.get('smtp.user')) throw new SystemError('Cannot send mail');
5 years ago
const udoc = await user.getByEmail(mail);
if (!udoc) throw new UserNotFoundError(mail);
5 years ago
const tid = await token.add(
token.TYPE_LOSTPASS,
5 years ago
await system.get('lostpass_token_expire_seconds'),
5 years ago
{ uid: udoc._id },
);
5 years ago
const m = await this.renderHTML('user_lostpass_mail', { url: `/lostpass/${tid}`, uname: udoc.uname });
await sendMail(mail, 'Lost Password', 'user_lostpass_mail', m);
this.response.template = 'user_lostpass_mail_sent.html';
}
}
class UserLostPassWithCodeHandler extends Handler {
async get({ domainId, code }) {
5 years ago
const tdoc = await token.get(code, token.TYPE_LOSTPASS);
if (!tdoc) throw new InvalidTokenError(token.TYPE_LOSTPASS, code);
const udoc = await user.getById(domainId, tdoc.uid);
this.response.body = { uname: udoc.uname };
}
5 years ago
async post({ code, password, verifyPassword }) {
const tdoc = await token.get(code, token.TYPE_LOSTPASS);
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);
this.response.redirect = '/';
}
}
class UserDetailHandler extends Handler {
async get({ domainId, uid }) {
5 years ago
const isSelfProfile = this.user._id === uid;
const udoc = await user.getById(domainId, uid, true);
5 years ago
const sdoc = await token.getMostRecentSessionByUid(uid);
5 years ago
this.response.template = 'user_detail.html';
5 years ago
this.response.body = { isSelfProfile, udoc, sdoc };
}
}
class UserSearchHandler extends Handler {
async get({ domainId, q, exactMatch = false }) {
let udocs;
5 years ago
if (exactMatch) udocs = [];
else udocs = await user.getPrefixList(q, 20);
const udoc = await user.getById(domainId, parseInt(q, 10));
if (udoc) udocs.push(udoc);
5 years ago
for (const i in udocs) {
if (udocs[i].gravatar) {
udocs[i].gravatar_url = misc.gravatar(udocs[i].gravatar);
5 years ago
}
}
this.response.body = udocs;
}
}
class OauthHandler extends Handler {
4 years ago
async github() {
const [appid, [state]] = await Promise.all([
system.get('oauth.githubappid'),
token.add(token.TYPE_OAUTH, 600, { redirect: this.request.referer }),
]);
this.response.redirect = `https://github.com/login/oauth/authorize?client_id=${appid}&state=${state}`;
}
async google() {
const [appid, url, [state]] = await Promise.all([
system.get('oauth.googleappid'),
system.get('server.url'),
token.add(token.TYPE_OAUTH, 600, { redirect: this.request.referer }),
]);
this.response.redirect = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${appid}&response_type=code&redirect_uri=${url}oauth/google/callback&scope=https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile&state=${state}`;
}
async get({ type }) {
4 years ago
if (type === 'github') await this.github();
else if (type === 'google') await this.google();
}
}
class OauthCallbackHandler extends Handler {
4 years ago
async github({ state, code }) {
const [appid, secret, url, proxy, s] = await Promise.all([
4 years ago
system.get('oauth.githubappid'),
system.get('oauth.githubsecret'),
system.get('server.url'),
system.get('proxy'),
4 years ago
token.get(state, token.TYPE_OAUTH),
]);
const res = await superagent.post('https://github.com/login/oauth/access_token')
.proxy(proxy)
4 years ago
.send({
client_id: appid,
client_secret: secret,
code,
redirect_uri: `${url}oauth/github/callback`,
state,
4 years ago
})
.set('accept', 'application/json');
if (res.body.error) {
throw new UserFacingError(
res.body.error, res.body.error_description, res.body.error_uri,
);
}
const t = res.body.access_token;
const userInfo = await superagent.get('https://api.github.com/user')
.proxy(proxy)
.set('User-Agent', 'Hydro-OAuth')
4 years ago
.set('Authorization', `token ${t}`);
const ret = {
email: userInfo.body.email,
4 years ago
bio: userInfo.body.bio,
uname: [userInfo.body.name, userInfo.body.login],
};
this.response.redirect = s.redirect;
await token.del(s, token.TYPE_OAUTH);
return ret;
}
async google({
code, scope, authuser, prompt, error, state,
}) {
if (error) throw new UserFacingError(error);
console.log(scope, authuser, prompt);
const [
[appid, secret, url, proxy],
s,
] = await Promise.all([
system.getMany([
'oauth.googleappid', 'oauth.googlesecret', 'server.url', 'proxy',
]),
token.get(state, token.TYPE_OAUTH),
]);
const res = await superagent.post('https://oauth2.googleapis.com/token')
.proxy(proxy)
.send({
client_id: appid,
client_secret: secret,
code,
grant_type: 'authorization_code',
redirect_uri: `${url}oauth/google/callback`,
});
4 years ago
const payload = global.Hydro.lib.jwt.decode(res.body.id_token);
await token.del(state, token.TYPE_OAUTH);
this.response.redirect = s.redirect;
return {
email: payload.email,
uname: [payload.given_name, payload.name, payload.family_name],
viewLang: payload.locale.replace('-', '_'),
};
}
async get(args) {
let r;
if (args.type === 'github') r = await this.github(args);
else if (args.type === 'google') r = await this.google(args);
else throw new UserFacingError('Oauth type');
const udoc = await user.getByEmail('system', r.email, true);
if (udoc) {
this.session.uid = udoc._id;
} else {
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
const nudoc = await user.getByUname('system', uname, true);
if (!nudoc) {
username = uname;
break;
}
}
4 years ago
const uid = await user.create({
mail: r.email, uname: username, password: String.random(32), regip: this.request.ip,
});
const $set = {
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;
}
}
}
5 years ago
async function apply() {
Route('user_login', '/login', UserLoginHandler);
Route('user_oauth', '/oauth/:type', OauthHandler);
Route('user_oauth_callback', '/oauth/:type/callback', OauthCallbackHandler);
Route('user_register', '/register', UserRegisterHandler);
Route('user_register_with_code', '/register/:code', UserRegisterWithCodeHandler);
Route('user_logout', '/logout', UserLogoutHandler);
Route('user_lostpass', '/lostpass', UserLostPassHandler);
Route('user_lostpass_with_code', '/lostpass/:code', UserLostPassWithCodeHandler);
Route('user_search', '/user/search', UserSearchHandler);
Route('user_detail', '/user/:uid', UserDetailHandler);
5 years ago
}
4 years ago
global.Hydro.handler.user = module.exports = apply;