Revert "Prerequisites to removing Facebook authentication (#13683)"

This reverts commit 1177ad8b8c.
This commit is contained in:
SabreCat
2022-01-25 09:30:37 -06:00
parent 9a6b05fe67
commit b3c466ad5a
12 changed files with 72 additions and 288 deletions

View File

@@ -32,8 +32,8 @@
"LOGGLY_SUBDOMAIN": "example-subdomain", "LOGGLY_SUBDOMAIN": "example-subdomain",
"LOGGLY_TOKEN": "example-token", "LOGGLY_TOKEN": "example-token",
"MAINTENANCE_MODE": "false", "MAINTENANCE_MODE": "false",
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev", "NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs",
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test", "TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs",
"MONGODB_POOL_SIZE": "10", "MONGODB_POOL_SIZE": "10",
"NODE_ENV": "development", "NODE_ENV": "development",
"PATH": "bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin", "PATH": "bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin",

View File

@@ -1,4 +1,3 @@
import { v4 as generateUUID } from 'uuid';
import { import {
generateUser, generateUser,
requester, requester,
@@ -10,18 +9,15 @@ describe('GET /user/auth/apple', () => {
let api; let api;
let user; let user;
const appleEndpoint = '/user/auth/apple'; const appleEndpoint = '/user/auth/apple';
let randomAppleId = '123456';
before(async () => {
const expectedResult = { id: 'appleId', name: 'an apple user' };
sandbox.stub(appleAuth, 'appleProfile').returns(Promise.resolve(expectedResult));
});
beforeEach(async () => { beforeEach(async () => {
api = requester(); api = requester();
user = await generateUser(); user = await generateUser();
randomAppleId = generateUUID();
const expectedResult = { id: randomAppleId, name: 'an apple user' };
sandbox.stub(appleAuth, 'appleProfile').returns(Promise.resolve(expectedResult));
});
afterEach(async () => {
appleAuth.appleProfile.restore();
}); });
it('registers a new user', async () => { it('registers a new user', async () => {
@@ -30,7 +26,7 @@ describe('GET /user/auth/apple', () => {
expect(response.apiToken).to.exist; expect(response.apiToken).to.exist;
expect(response.id).to.exist; expect(response.id).to.exist;
expect(response.newUser).to.be.true; expect(response.newUser).to.be.true;
await expect(getProperty('users', response.id, 'auth.apple.id')).to.eventually.equal(randomAppleId); await expect(getProperty('users', response.id, 'auth.apple.id')).to.eventually.equal('appleId');
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('an apple user'); await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('an apple user');
}); });

View File

@@ -1,5 +1,4 @@
import passport from 'passport'; import passport from 'passport';
import { v4 as generateUUID } from 'uuid';
import { import {
generateUser, generateUser,
requester, requester,
@@ -11,15 +10,14 @@ describe('POST /user/auth/social', () => {
let api; let api;
let user; let user;
const endpoint = '/user/auth/social'; const endpoint = '/user/auth/social';
let randomAccessToken = '123456'; const randomAccessToken = '123456';
let randomFacebookId = 'facebookId'; const facebookId = 'facebookId';
let randomGoogleId = 'googleId'; const googleId = 'googleId';
let network = 'NoNetwork'; let network = 'NoNetwork';
beforeEach(async () => { beforeEach(async () => {
api = requester(); api = requester();
user = await generateUser(); user = await generateUser();
randomAccessToken = generateUUID();
}); });
it('fails if network is not supported', async () => { it('fails if network is not supported', async () => {
@@ -34,23 +32,12 @@ describe('POST /user/auth/social', () => {
}); });
describe('facebook', () => { describe('facebook', () => {
beforeEach(async () => { before(async () => {
randomFacebookId = generateUUID(); const expectedResult = { id: facebookId, displayName: 'a facebook user' };
const expectedResult = {
id: randomFacebookId,
displayName: 'a facebook user',
emails: [
{ value: `${user.auth.local.username}+facebook@example.com` },
],
};
sandbox.stub(passport._strategies.facebook, 'userProfile').yields(null, expectedResult); sandbox.stub(passport._strategies.facebook, 'userProfile').yields(null, expectedResult);
network = 'facebook'; network = 'facebook';
}); });
afterEach(async () => {
passport._strategies.facebook.userProfile.restore();
});
it('registers a new user', async () => { it('registers a new user', async () => {
const response = await api.post(endpoint, { const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
@@ -64,8 +51,7 @@ describe('POST /user/auth/social', () => {
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a facebook user'); await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a facebook user');
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.exist; await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.exist;
await expect(getProperty('users', response.id, 'auth.local.email')).to.eventually.equal(`${user.auth.local.username}+facebook@example.com`); await expect(getProperty('users', response.id, 'auth.facebook.id')).to.eventually.equal(facebookId);
await expect(getProperty('users', response.id, 'auth.facebook.id')).to.eventually.equal(randomFacebookId);
}); });
it('logs an existing user in', async () => { it('logs an existing user in', async () => {
@@ -82,57 +68,6 @@ describe('POST /user/auth/social', () => {
expect(response.apiToken).to.eql(registerResponse.apiToken); expect(response.apiToken).to.eql(registerResponse.apiToken);
expect(response.id).to.eql(registerResponse.id); expect(response.id).to.eql(registerResponse.id);
expect(response.newUser).to.be.false; expect(response.newUser).to.be.false;
expect(registerResponse.newUser).to.be.true;
});
it('logs an existing user in if they have local auth with matching email', async () => {
passport._strategies.facebook.userProfile.restore();
const expectedResult = {
id: randomFacebookId,
displayName: 'a facebook user',
emails: [
{ value: user.auth.local.email },
],
};
sandbox.stub(passport._strategies.facebook, 'userProfile').yields(null, expectedResult);
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
expect(response.apiToken).to.eql(user.apiToken);
expect(response.id).to.eql(user._id);
expect(response.newUser).to.be.false;
});
it('logs an existing user into their social account if they have local auth with matching email', async () => {
const registerResponse = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
expect(registerResponse.newUser).to.be.true;
// This is important for existing accounts before the new social handling
passport._strategies.facebook.userProfile.restore();
const expectedResult = {
id: randomFacebookId,
displayName: 'a facebook user',
emails: [
{ value: user.auth.local.email },
],
};
sandbox.stub(passport._strategies.facebook, 'userProfile').yields(null, expectedResult);
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
expect(response.apiToken).to.eql(registerResponse.apiToken);
expect(response.id).to.eql(registerResponse.id);
expect(response.apiToken).not.to.eql(user.apiToken);
expect(response.id).not.to.eql(user._id);
expect(response.newUser).to.be.false;
}); });
it('add social auth to an existing user', async () => { it('add social auth to an existing user', async () => {
@@ -141,28 +76,11 @@ describe('POST /user/auth/social', () => {
network, network,
}); });
expect(response.apiToken).to.eql(user.apiToken); expect(response.apiToken).to.exist;
expect(response.id).to.eql(user._id); expect(response.id).to.exist;
expect(response.newUser).to.be.false; expect(response.newUser).to.be.false;
}); });
it('does not log into other account if social auth already exists', async () => {
const registerResponse = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
expect(registerResponse.newUser).to.be.true;
await expect(user.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('socialAlreadyExists'),
});
});
xit('enrolls a new user in an A/B test', async () => { xit('enrolls a new user in an A/B test', async () => {
await api.post(endpoint, { await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
@@ -174,23 +92,12 @@ describe('POST /user/auth/social', () => {
}); });
describe('google', () => { describe('google', () => {
beforeEach(async () => { before(async () => {
randomGoogleId = generateUUID(); const expectedResult = { id: googleId, displayName: 'a google user' };
const expectedResult = {
id: randomGoogleId,
displayName: 'a google user',
emails: [
{ value: `${user.auth.local.username}+google@example.com` },
],
};
sandbox.stub(passport._strategies.google, 'userProfile').yields(null, expectedResult); sandbox.stub(passport._strategies.google, 'userProfile').yields(null, expectedResult);
network = 'google'; network = 'google';
}); });
afterEach(async () => {
passport._strategies.google.userProfile.restore();
});
it('registers a new user', async () => { it('registers a new user', async () => {
const response = await api.post(endpoint, { const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
@@ -200,8 +107,7 @@ describe('POST /user/auth/social', () => {
expect(response.apiToken).to.exist; expect(response.apiToken).to.exist;
expect(response.id).to.exist; expect(response.id).to.exist;
expect(response.newUser).to.be.true; expect(response.newUser).to.be.true;
await expect(getProperty('users', response.id, 'auth.google.id')).to.eventually.equal(randomGoogleId); await expect(getProperty('users', response.id, 'auth.google.id')).to.eventually.equal(googleId);
await expect(getProperty('users', response.id, 'auth.local.email')).to.eventually.equal(`${user.auth.local.username}+google@example.com`);
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user'); await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user');
}); });
@@ -219,57 +125,6 @@ describe('POST /user/auth/social', () => {
expect(response.apiToken).to.eql(registerResponse.apiToken); expect(response.apiToken).to.eql(registerResponse.apiToken);
expect(response.id).to.eql(registerResponse.id); expect(response.id).to.eql(registerResponse.id);
expect(response.newUser).to.be.false; expect(response.newUser).to.be.false;
expect(registerResponse.newUser).to.be.true;
});
it('logs an existing user in if they have local auth with matching email', async () => {
passport._strategies.google.userProfile.restore();
const expectedResult = {
id: randomGoogleId,
displayName: 'a google user',
emails: [
{ value: user.auth.local.email },
],
};
sandbox.stub(passport._strategies.google, 'userProfile').yields(null, expectedResult);
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
expect(response.apiToken).to.eql(user.apiToken);
expect(response.id).to.eql(user._id);
expect(response.newUser).to.be.false;
});
it('logs an existing user into their social account if they have local auth with matching email', async () => {
const registerResponse = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
expect(registerResponse.newUser).to.be.true;
// This is important for existing accounts before the new social handling
passport._strategies.google.userProfile.restore();
const expectedResult = {
id: randomGoogleId,
displayName: 'a google user',
emails: [
{ value: user.auth.local.email },
],
};
sandbox.stub(passport._strategies.google, 'userProfile').yields(null, expectedResult);
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
expect(response.apiToken).to.eql(registerResponse.apiToken);
expect(response.id).to.eql(registerResponse.id);
expect(response.apiToken).not.to.eql(user.apiToken);
expect(response.id).not.to.eql(user._id);
expect(response.newUser).to.be.false;
}); });
it('add social auth to an existing user', async () => { it('add social auth to an existing user', async () => {
@@ -278,28 +133,11 @@ describe('POST /user/auth/social', () => {
network, network,
}); });
expect(response.apiToken).to.eql(user.apiToken); expect(response.apiToken).to.exist;
expect(response.id).to.eql(user._id); expect(response.id).to.exist;
expect(response.newUser).to.be.false; expect(response.newUser).to.be.false;
}); });
it('does not log into other account if social auth already exists', async () => {
const registerResponse = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
expect(registerResponse.newUser).to.be.true;
await expect(user.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('socialAlreadyExists'),
});
});
xit('enrolls a new user in an A/B test', async () => { xit('enrolls a new user in an A/B test', async () => {
await api.post(endpoint, { await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase

View File

@@ -25,19 +25,6 @@ describe('POST /user/reset-password', async () => {
expect(user.auth.local.hashed_password).to.not.eql(previousPassword); expect(user.auth.local.hashed_password).to.not.eql(previousPassword);
}); });
it('resets password for social users', async () => {
const email = `${user.auth.local.username}+google@example.com`;
await user.update({ 'auth.google.emails': [{ value: email }] });
await user.sync();
const previousPassword = user.auth.local.passwordResetCode;
const response = await user.post(endpoint, {
email,
});
expect(response).to.eql({ data: {}, message: t('passwordReset') });
await user.sync();
expect(user.auth.local.passwordResetCode).to.not.eql(previousPassword);
});
it('same message on error as on success', async () => { it('same message on error as on success', async () => {
const response = await user.post(endpoint, { const response = await user.post(endpoint, {
email: 'nonExistent@email.com', email: 'nonExistent@email.com',

View File

@@ -20,8 +20,8 @@
</div> </div>
</div> </div>
<div <div
v-if="!registering"
class="form-group row text-center" class="form-group row text-center"
v-if="!registering"
> >
<div class="col-12 col-md-12"> <div class="col-12 col-md-12">
<div <div

View File

@@ -178,12 +178,12 @@ import sword from '@/assets/svg/sword.svg';
import { worldStateMixin } from '@/mixins/worldState'; import { worldStateMixin } from '@/mixins/worldState';
export default { export default {
components: {
BaseNotification,
},
mixins: [ mixins: [
worldStateMixin, worldStateMixin,
], ],
components: {
BaseNotification,
},
data () { data () {
const questData = quests.quests.dysheartener; const questData = quests.quests.dysheartener;

View File

@@ -194,10 +194,7 @@
<h4 class="popover-content-title"> <h4 class="popover-content-title">
{{ context.item.text }} {{ context.item.text }}
</h4> </h4>
<questInfo <questInfo :quest="context.item" :purchased="true" />
:quest="context.item"
:purchased="true"
/>
</div> </div>
<div v-else> <div v-else>
<h4 class="popover-content-title"> <h4 class="popover-content-title">

View File

@@ -1,17 +1,10 @@
<template> <template>
<div class="notification-animation-holder"> <div class="notification-animation-holder">
<div <div class="notification-holder"
class="notification-holder" @click="handleOnClick()">
@click="handleOnClick()" <div v-if="notification.type === 'drop'"
> class="icon-item">
<div <div :class="notification.icon" class="icon-negative-margin"></div>
v-if="notification.type === 'drop'"
class="icon-item"
>
<div
:class="notification.icon"
class="icon-negative-margin"
></div>
</div> </div>
<div <div
@@ -39,10 +32,7 @@
class="svg-icon" class="svg-icon"
v-html="icons.gold" v-html="icons.gold"
></div> ></div>
<div <div class="icon-text" v-html="notification.text"></div>
class="icon-text"
v-html="notification.text"
></div>
</div> </div>
</div> </div>
<div <div
@@ -73,10 +63,7 @@
class="svg-icon" class="svg-icon"
v-html="icons.mana" v-html="icons.mana"
></div> ></div>
<div <div class="icon-text" v-html="notification.text"></div>
class="icon-text"
v-html="notification.text"
></div>
</div> </div>
</div> </div>
<div <div
@@ -91,10 +78,7 @@
class="svg-icon" class="svg-icon"
v-html="icons.sword" v-html="icons.sword"
></div> ></div>
<div <div class="icon-text" v-html="notification.text"></div>
class="icon-text"
v-html="notification.text"
></div>
</div> </div>
</div> </div>
<div <div
@@ -114,6 +98,7 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -81,12 +81,12 @@ const DELAY_DELETE_AND_NEW = 60;
const DELAY_FILLING_ENTRIES = 240; const DELAY_FILLING_ENTRIES = 240;
export default { export default {
components: {
notification,
},
mixins: [ mixins: [
worldStateMixin, worldStateMixin,
], ],
components: {
notification,
},
props: { props: {
preventQueue: { preventQueue: {
type: Boolean, type: Boolean,

View File

@@ -133,7 +133,6 @@
"unsupportedNetwork": "This network is not currently supported.", "unsupportedNetwork": "This network is not currently supported.",
"cantDetachSocial": "Account lacks another authentication method; can't detach this authentication method.", "cantDetachSocial": "Account lacks another authentication method; can't detach this authentication method.",
"onlySocialAttachLocal": "Local authentication can be added to only a social account.", "onlySocialAttachLocal": "Local authentication can be added to only a social account.",
"socialAlreadyExists": "This social login is already linked to an existing Habitica account.",
"invalidReqParams": "Invalid request parameters.", "invalidReqParams": "Invalid request parameters.",
"memberIdRequired": "\"member\" must be a valid UUID.", "memberIdRequired": "\"member\" must be a valid UUID.",
"heroIdRequired": "\"heroId\" must be a valid UUID.", "heroIdRequired": "\"heroId\" must be a valid UUID.",

View File

@@ -341,14 +341,7 @@ api.resetPassword = {
if (validationErrors) throw validationErrors; if (validationErrors) throw validationErrors;
const email = req.body.email.toLowerCase(); const email = req.body.email.toLowerCase();
const user = await User.findOne({ const user = await User.findOne({ 'auth.local.email': email }).exec();
$or: [
{ 'auth.local.email': email },
{ 'auth.apple.emails.value': email },
{ 'auth.google.emails.value': email },
{ 'auth.facebook.emails.value': email },
],
}).exec();
if (user) { if (user) {
// create an encrypted link to be used to reset the password // create an encrypted link to be used to reset the password

View File

@@ -1,6 +1,6 @@
import passport from 'passport'; import passport from 'passport';
import common from '../../../common'; import common from '../../../common';
import { BadRequest, NotAuthorized } from '../errors'; import { BadRequest } from '../errors';
import logger from '../logger'; import logger from '../logger';
import { import {
generateUsername, generateUsername,
@@ -24,7 +24,7 @@ function _passportProfile (network, accessToken) {
} }
export async function loginSocial (req, res) { // eslint-disable-line import/prefer-default-export export async function loginSocial (req, res) { // eslint-disable-line import/prefer-default-export
let existingUser = res.locals.user; const existingUser = res.locals.user;
const { network } = req.body; const { network } = req.body;
const isSupportedNetwork = common.constants.SUPPORTED_SOCIAL_NETWORKS const isSupportedNetwork = common.constants.SUPPORTED_SOCIAL_NETWORKS
@@ -47,28 +47,9 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre
// User already signed up // User already signed up
if (user) { if (user) {
if (existingUser) {
throw new NotAuthorized(res.t('socialAlreadyExists'));
}
return loginRes(user, req, res); return loginRes(user, req, res);
} }
let email;
if (profile.emails && profile.emails[0] && profile.emails[0].value) {
email = profile.emails[0].value.toLowerCase();
}
if (!existingUser && email) {
existingUser = await User.findOne({ 'auth.local.email': email }).exec();
}
if (existingUser) {
existingUser.auth[network] = {
id: profile.id,
emails: profile.emails,
};
user = existingUser;
} else {
const generatedUsername = generateUsername(); const generatedUsername = generateUsername();
user = { user = {
@@ -80,7 +61,6 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre
local: { local: {
username: generatedUsername, username: generatedUsername,
lowerCaseUsername: generatedUsername, lowerCaseUsername: generatedUsername,
email,
}, },
}, },
profile: { profile: {
@@ -93,6 +73,11 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre
verifiedUsername: true, verifiedUsername: true,
}, },
}; };
if (existingUser) {
existingUser.auth[network] = user.auth[network];
user = existingUser;
} else {
user = new User(user); user = new User(user);
user.registeredThrough = req.headers['x-client']; // Not saved, used to create the correct tasks based on the device used user.registeredThrough = req.headers['x-client']; // Not saved, used to create the correct tasks based on the device used
} }
@@ -100,15 +85,19 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre
const savedUser = await user.save(); const savedUser = await user.save();
if (!existingUser) { if (!existingUser) {
savedUser.newUser = true; user.newUser = true;
} }
const response = loginRes(savedUser, req, res); const response = loginRes(user, req, res);
// Clean previous email preferences // Clean previous email preferences
if (email) { if (
savedUser.auth[network].emails
&& savedUser.auth[network].emails[0]
&& savedUser.auth[network].emails[0].value
) {
EmailUnsubscription EmailUnsubscription
.remove({ email }) .remove({ email: savedUser.auth[network].emails[0].value.toLowerCase() })
.exec() .exec()
.then(() => { .then(() => {
if (!existingUser) { if (!existingUser) {