mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-19 09:38:19 +08:00
core: support oauth bind/unbind account (#971)
Co-authored-by: panda <panda_dtdyy@outlook.com>
This commit is contained in:
1 parent
1a8232899c
commit
536c697f4b
16 files changed
+393
-141
No files matched your search
@@ -193,6 +193,7 @@ class HomeSecurityHandler extends Handler {
|
||||
this.translate('geoip_locale'),
|
||||
);
|
||||
}
|
||||
const relations = await this.ctx.oauth.list(this.user._id);
|
||||
this.response.template = 'home_security.html';
|
||||
this.response.body = {
|
||||
sudoUid: this.session.sudoUid || null,
|
||||
@@ -202,6 +203,7 @@ class HomeSecurityHandler extends Handler {
|
||||
'authenticatorAttachment', 'regat', 'fmt',
|
||||
])),
|
||||
geoipProvider: this.ctx.geoip?.provider,
|
||||
relations,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -250,6 +252,13 @@ class HomeSecurityHandler extends Handler {
|
||||
this.response.template = 'user_changemail_mail_sent.html';
|
||||
}
|
||||
|
||||
@param('platform', Types.String)
|
||||
async postLinkAccount({ }, platform: string) {
|
||||
if (!global.Hydro.module.oauth[platform]) throw new ValidationError('platform');
|
||||
this.session.oauthBind = platform;
|
||||
await global.Hydro.module.oauth[platform].get.call(this);
|
||||
}
|
||||
|
||||
@param('tokenDigest', Types.String)
|
||||
async postDeleteToken({ }, tokenDigest: string) {
|
||||
const sessions = await token.getSessionListByUid(this.user._id);
|
||||
@@ -609,7 +618,7 @@ class HomeMessagesConnectionHandler extends ConnectionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
export const inject = { geoip: { required: false } };
|
||||
export const inject = { geoip: { required: false }, oauth: {} };
|
||||
export function apply(ctx: Context) {
|
||||
ctx.Route('homepage', '/', HomeHandler);
|
||||
ctx.Route('home_security', '/home/security', HomeSecurityHandler, PRIV.PRIV_USER_PROFILE);
|
||||
|
||||
@@ -4,7 +4,7 @@ import moment from 'moment-timezone';
|
||||
import type { Binary } from 'mongodb';
|
||||
import type { Context } from '../context';
|
||||
import {
|
||||
AuthOperationError, BlacklistedError, BuiltinLoginError, ForbiddenError, InvalidTokenError,
|
||||
AuthOperationError, BadRequestError, BlacklistedError, BuiltinLoginError, ForbiddenError, InvalidTokenError,
|
||||
SystemError, UserAlreadyExistError, UserFacingError,
|
||||
UserNotFoundError, ValidationError, VerifyPasswordError,
|
||||
} from '../error';
|
||||
@@ -16,7 +16,6 @@ import BlackListModel from '../model/blacklist';
|
||||
import { PERM, PRIV, STATUS } from '../model/builtin';
|
||||
import * as ContestModel from '../model/contest';
|
||||
import domain from '../model/domain';
|
||||
import oauth from '../model/oauth';
|
||||
import * as oplog from '../model/oplog';
|
||||
import problem, { ProblemDoc } from '../model/problem';
|
||||
import ScheduleModel from '../model/schedule';
|
||||
@@ -28,6 +27,18 @@ import {
|
||||
Handler, param, post, 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() {
|
||||
@@ -74,15 +85,9 @@ class UserLoginHandler extends Handler {
|
||||
} else throw new ValidationError('2FA', 'Authn');
|
||||
}
|
||||
await udoc.checkPassword(password);
|
||||
await user.setById(udoc._id, { loginat: new Date(), loginip: this.request.ip });
|
||||
if (!udoc.hasPriv(PRIV.PRIV_USER_PROFILE)) throw new BlacklistedError(uname, udoc.banReason);
|
||||
this.context.HydroContext.user = udoc;
|
||||
this.session.viewLang = '';
|
||||
this.session.uid = udoc._id;
|
||||
this.session.sudo = null;
|
||||
this.session.scope = PERM.PERM_ALL.toString();
|
||||
await successfulAuth.call(this, udoc);
|
||||
this.session.save = rememberme;
|
||||
this.session.recreate = true;
|
||||
this.response.redirect = redirect || ((this.request.referer || '/login').endsWith('/login')
|
||||
? this.url('homepage') : this.request.referer);
|
||||
}
|
||||
@@ -193,15 +198,12 @@ class UserLogoutHandler extends Handler {
|
||||
}
|
||||
|
||||
async post({ domainId }) {
|
||||
this.context.HydroContext.user = await user.getById(domainId, 0);
|
||||
this.session.uid = 0;
|
||||
this.session.sudo = null;
|
||||
this.session.sudoUid = null;
|
||||
this.session.scope = PERM.PERM_ALL.toString();
|
||||
await successfulAuth.call(this, await user.getById(domainId, 0));
|
||||
this.response.redirect = '/';
|
||||
}
|
||||
}
|
||||
|
||||
// rename to RegisterSendMailHandler
|
||||
export class UserRegisterHandler extends Handler {
|
||||
noCheckPermView = true;
|
||||
async prepare() {
|
||||
@@ -225,7 +227,15 @@ export class UserRegisterHandler extends Handler {
|
||||
const t = await token.add(
|
||||
token.TYPE_REGISTRATION,
|
||||
system.get('session.unsaved_expire_seconds'),
|
||||
{ mail, redirect: this.domain.registerRedirect },
|
||||
{
|
||||
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}`
|
||||
@@ -249,7 +259,7 @@ class UserRegisterWithCodeHandler extends Handler {
|
||||
@param('code', Types.String)
|
||||
async prepare({ }, code: string) {
|
||||
this.tdoc = await token.get(code, token.TYPE_REGISTRATION);
|
||||
if (!this.tdoc || (!this.tdoc.mail && !this.tdoc.phone)) {
|
||||
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);
|
||||
@@ -269,28 +279,22 @@ class UserRegisterWithCodeHandler extends Handler {
|
||||
domainId: string, password: string, verify: string,
|
||||
uname = '', code: string,
|
||||
) {
|
||||
if (this.tdoc.oauth?.[0] && global.Hydro.module.oauth[this.tdoc.oauth[0]].lockUsername) {
|
||||
uname = this.tdoc.username;
|
||||
if (global.Hydro.module.oauth[this.tdoc.identity.provider].lockUsername) {
|
||||
uname = this.tdoc.identity.username;
|
||||
}
|
||||
if (!Types.Username[1](uname)) throw new ValidationError('uname');
|
||||
if (password !== verify) throw new VerifyPasswordError();
|
||||
if (this.tdoc.phone) this.tdoc.mail = `${String.random(12)}@hydro.local`;
|
||||
const uid = await user.create(this.tdoc.mail, uname, password, undefined, this.request.ip);
|
||||
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 (this.tdoc.phone) $set.phone = this.tdoc.phone;
|
||||
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);
|
||||
if (this.tdoc.oauth) await oauth.set(this.tdoc.oauth[1], uid);
|
||||
this.context.HydroContext.user = await user.getById(domainId, uid);
|
||||
this.session.viewLang = '';
|
||||
this.session.uid = uid;
|
||||
this.session.sudoUid = null;
|
||||
this.session.scope = PERM.PERM_ALL.toString();
|
||||
this.session.recreate = true;
|
||||
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' });
|
||||
}
|
||||
}
|
||||
@@ -434,56 +438,57 @@ class OauthCallbackHandler extends Handler {
|
||||
async get(args: any) {
|
||||
if (!global.Hydro.module.oauth[args.type]) throw new UserFacingError('Oauth type');
|
||||
const r = await global.Hydro.module.oauth[args.type].callback.call(this, args);
|
||||
const uid = await oauth.get(r._id);
|
||||
if (uid) {
|
||||
await user.setById(uid, { loginat: new Date(), loginip: this.request.ip });
|
||||
this.session.uid = uid;
|
||||
this.session.scope = PERM.PERM_ALL.toString();
|
||||
this.response.redirect = '/';
|
||||
} else {
|
||||
if (r.email) {
|
||||
const udoc = await user.getByEmail('system', r.email);
|
||||
if (udoc) {
|
||||
await oauth.set(r._id, udoc._id);
|
||||
await user.setById(udoc._id, { loginat: new Date(), loginip: this.request.ip });
|
||||
this.session.uid = udoc._id;
|
||||
this.session.scope = PERM.PERM_ALL.toString();
|
||||
this.session.recreate = true;
|
||||
this.response.redirect = '/';
|
||||
return;
|
||||
}
|
||||
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.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, oauth: args.type };
|
||||
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,
|
||||
oauth: [args.type, r._id],
|
||||
},
|
||||
);
|
||||
this.response.redirect = this.url('user_register_with_code', { code: t });
|
||||
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;
|
||||
}
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,6 +510,8 @@ class ContestModeHandler extends Handler {
|
||||
}
|
||||
}
|
||||
|
||||
export const inject = ['oauth'];
|
||||
|
||||
export async function apply(ctx: Context) {
|
||||
ctx.Route('user_login', '/login', UserLoginHandler);
|
||||
ctx.Route('user_oauth', '/oauth/:type', OauthHandler);
|
||||
|
||||
Reference in new issue
Block a user