mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-18 15:17:25 +01:00
Add Google Signin (#7969)
* Start adding google login * fix local js issue * implement syntax suggestions * fix delete social tests * Add service for authentication alerts * fix social login tests * make suggested google sign in changes * fix accidentally deleted code * refactor social network sign in * fix incorrect find * implement suggested google sign in changes * fix(tests): Inject fake Auth module for auth controller * fix(test): prevent social service from causing page reload * fix loading user info * Use lodash's implimentation of find for IE compatibility * chore: increase test coverage around deletion route * chore: clean up social auth test * chore: Fix social login tests * remove profile from login scope * fix(api): Allow social accounts to deregister as user has auth backup * temporarily disable google login button
This commit is contained in:
committed by
Matteo Pagliazzi
parent
941000d737
commit
e3b484b29a
@@ -10,7 +10,6 @@ import {
|
||||
BadRequest,
|
||||
NotFound,
|
||||
} from '../../libs/errors';
|
||||
import Bluebird from 'bluebird';
|
||||
import * as passwordUtils from '../../libs/password';
|
||||
import logger from '../../libs/logger';
|
||||
import { model as User } from '../../models/user';
|
||||
@@ -20,6 +19,7 @@ import { sendTxn as sendTxnEmail } from '../../libs/email';
|
||||
import { decrypt } from '../../libs/encryption';
|
||||
import { send as sendEmail } from '../../libs/email';
|
||||
import pusher from '../../libs/pusher';
|
||||
import common from '../../../common';
|
||||
|
||||
let api = {};
|
||||
|
||||
@@ -50,6 +50,18 @@ async function _handleGroupInvitation (user, invite) {
|
||||
}
|
||||
}
|
||||
|
||||
function hasBackupAuth (user, networkToRemove) {
|
||||
if (user.auth.local.username) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let hasAlternateNetwork = common.constants.SUPPORTED_SOCIAL_NETWORKS.find((network) => {
|
||||
return network.key !== networkToRemove && user.auth[network.key].id;
|
||||
});
|
||||
|
||||
return hasAlternateNetwork;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/user/auth/local/register Register
|
||||
* @apiDescription Register a new user with email, username and password or attach local auth to a social user
|
||||
@@ -69,7 +81,7 @@ api.registerLocal = {
|
||||
middlewares: [authWithHeaders(true)],
|
||||
url: '/user/auth/local/register',
|
||||
async handler (req, res) {
|
||||
let fbUser = res.locals.user; // If adding local auth to social user
|
||||
let existingUser = res.locals.user; // If adding local auth to social user
|
||||
|
||||
req.checkBody({
|
||||
email: {
|
||||
@@ -121,10 +133,15 @@ api.registerLocal = {
|
||||
},
|
||||
};
|
||||
|
||||
if (fbUser) {
|
||||
if (!fbUser.auth.facebook.id) throw new NotAuthorized(res.t('onlySocialAttachLocal'));
|
||||
fbUser.auth.local = newUser.auth.local;
|
||||
newUser = fbUser;
|
||||
if (existingUser) {
|
||||
let hasSocialAuth = common.constants.SUPPORTED_SOCIAL_NETWORKS.find(network => {
|
||||
if (existingUser.auth.hasOwnProperty(network.key)) {
|
||||
return existingUser.auth[network.key].id;
|
||||
}
|
||||
});
|
||||
if (!hasSocialAuth) throw new NotAuthorized(res.t('onlySocialAttachLocal'));
|
||||
existingUser.auth.local = newUser.auth.local;
|
||||
newUser = existingUser;
|
||||
} else {
|
||||
newUser = new User(newUser);
|
||||
newUser.registeredThrough = req.headers['x-client']; // Not saved, used to create the correct tasks based on the device used
|
||||
@@ -137,7 +154,7 @@ api.registerLocal = {
|
||||
|
||||
let savedUser = await newUser.save();
|
||||
|
||||
if (savedUser.auth.facebook.id) {
|
||||
if (existingUser) {
|
||||
res.respond(200, savedUser.toJSON().auth.local); // We convert to toJSON to hide private fields
|
||||
} else {
|
||||
res.respond(201, savedUser);
|
||||
@@ -148,7 +165,7 @@ api.registerLocal = {
|
||||
.remove({email: savedUser.auth.local.email})
|
||||
.then(() => sendTxnEmail(savedUser, 'welcome'));
|
||||
|
||||
if (!savedUser.auth.facebook.id) {
|
||||
if (!existingUser) {
|
||||
res.analytics.track('register', {
|
||||
category: 'acquisition',
|
||||
type: 'local',
|
||||
@@ -228,9 +245,9 @@ api.loginLocal = {
|
||||
},
|
||||
};
|
||||
|
||||
function _passportFbProfile (accessToken) {
|
||||
return new Bluebird((resolve, reject) => {
|
||||
passport._strategies.facebook.userProfile(accessToken, (err, profile) => {
|
||||
function _passportProfile (network, accessToken) {
|
||||
return new Promise((resolve, reject) => {
|
||||
passport._strategies[network].userProfile(accessToken, (err, profile) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
@@ -243,14 +260,19 @@ function _passportFbProfile (accessToken) {
|
||||
// Called as a callback by Facebook (or other social providers). Internal route
|
||||
api.loginSocial = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders(true)],
|
||||
url: '/user/auth/social', // this isn't the most appropriate url but must be the same as v2
|
||||
async handler (req, res) {
|
||||
let existingUser = res.locals.user;
|
||||
let accessToken = req.body.authResponse.access_token;
|
||||
let network = req.body.network;
|
||||
|
||||
if (network !== 'facebook') throw new NotAuthorized(res.t('onlyFbSupported'));
|
||||
let isSupportedNetwork = common.constants.SUPPORTED_SOCIAL_NETWORKS.find(supportedNetwork => {
|
||||
return supportedNetwork.key === network;
|
||||
});
|
||||
if (!isSupportedNetwork) throw new BadRequest(res.t('unsupportedNetwork'));
|
||||
|
||||
let profile = await _passportFbProfile(accessToken);
|
||||
let profile = await _passportProfile(network, accessToken);
|
||||
|
||||
let user = await User.findOne({
|
||||
[`auth.${network}.id`]: profile.id,
|
||||
@@ -260,37 +282,47 @@ api.loginSocial = {
|
||||
if (user) {
|
||||
_loginRes(user, ...arguments);
|
||||
} else { // Create new user
|
||||
user = new User({
|
||||
user = {
|
||||
auth: {
|
||||
[network]: profile,
|
||||
},
|
||||
preferences: {
|
||||
language: req.language,
|
||||
},
|
||||
});
|
||||
user.registeredThrough = req.headers['x-client'];
|
||||
};
|
||||
if (existingUser) {
|
||||
existingUser.auth[network] = user.auth[network];
|
||||
user = existingUser;
|
||||
} else {
|
||||
user = new User(user);
|
||||
user.registeredThrough = req.headers['x-client']; // Not saved, used to create the correct tasks based on the device used
|
||||
}
|
||||
|
||||
let savedUser = await user.save();
|
||||
|
||||
user.newUser = true;
|
||||
if (!existingUser) {
|
||||
user.newUser = true;
|
||||
}
|
||||
_loginRes(user, ...arguments);
|
||||
|
||||
// Clean previous email preferences
|
||||
if (savedUser.auth[network].emails && savedUser.auth.facebook.emails[0] && savedUser.auth[network].emails[0].value) {
|
||||
if (savedUser.auth[network].emails && savedUser.auth[network].emails[0] && savedUser.auth[network].emails[0].value) {
|
||||
EmailUnsubscription
|
||||
.remove({email: savedUser.auth[network].emails[0].value.toLowerCase()})
|
||||
.exec()
|
||||
.then(() => sendTxnEmail(savedUser, 'welcome')); // eslint-disable-line max-nested-callbacks
|
||||
}
|
||||
|
||||
res.analytics.track('register', {
|
||||
category: 'acquisition',
|
||||
type: network,
|
||||
gaLabel: network,
|
||||
uuid: savedUser._id,
|
||||
headers: req.headers,
|
||||
user: savedUser,
|
||||
});
|
||||
if (!existingUser) {
|
||||
res.analytics.track('register', {
|
||||
category: 'acquisition',
|
||||
type: network,
|
||||
gaLabel: network,
|
||||
uuid: savedUser._id,
|
||||
headers: req.headers,
|
||||
user: savedUser,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -576,11 +608,15 @@ api.deleteSocial = {
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let network = req.params.network;
|
||||
|
||||
if (network !== 'facebook') throw new NotAuthorized(res.t('onlyFbSupported'));
|
||||
if (!user.auth.local.username) throw new NotAuthorized(res.t('cantDetachFb'));
|
||||
|
||||
await User.update({_id: user._id}, {$unset: {'auth.facebook': 1}}).exec();
|
||||
let isSupportedNetwork = common.constants.SUPPORTED_SOCIAL_NETWORKS.find(supportedNetwork => {
|
||||
return supportedNetwork.key === network;
|
||||
});
|
||||
if (!isSupportedNetwork) throw new BadRequest(res.t('unsupportedNetwork'));
|
||||
if (!hasBackupAuth(user, network)) throw new NotAuthorized(res.t('cantDetachSocial'));
|
||||
let unset = {
|
||||
[`auth.${network}`]: 1,
|
||||
};
|
||||
await User.update({_id: user._id}, {$unset: unset}).exec();
|
||||
|
||||
res.respond(200, {});
|
||||
},
|
||||
|
||||
@@ -607,6 +607,7 @@ async function _inviteByEmail (invite, group, inviter, req, res) {
|
||||
let userToContact = await User.findOne({$or: [
|
||||
{'auth.local.email': invite.email},
|
||||
{'auth.facebook.emails.value': invite.email},
|
||||
{'auth.google.emails.value': invite.email},
|
||||
]})
|
||||
.select({_id: true, 'preferences.emailNotifications': true})
|
||||
.exec();
|
||||
|
||||
Reference in New Issue
Block a user