Pull in missing changes from FB removal PR (#13931)

* fix(auth): hide post hoc Facebook reg

* Pull in missing changes

* fix(lint): whitespace

* fix(strings): missing error message

* fix(tests): update to match functionality

Co-authored-by: SabreCat <sabe@habitica.com>
This commit is contained in:
Phillip Thelen
2022-04-14 19:58:37 +02:00
committed by GitHub
parent afd1248ea3
commit 664f960a8b
5 changed files with 233 additions and 63 deletions

View File

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

View File

@@ -1,4 +1,5 @@
import passport from 'passport';
import { v4 as generateUUID } from 'uuid';
import {
generateUser,
requester,
@@ -10,14 +11,15 @@ describe('POST /user/auth/social', () => {
let api;
let user;
const endpoint = '/user/auth/social';
const randomAccessToken = '123456';
const facebookId = 'facebookId';
const googleId = 'googleId';
let randomAccessToken = '123456';
let randomFacebookId = 'facebookId';
let randomGoogleId = 'googleId';
let network = 'NoNetwork';
beforeEach(async () => {
api = requester();
user = await generateUser();
randomAccessToken = generateUUID();
});
it('fails if network is not supported', async () => {
@@ -32,12 +34,23 @@ describe('POST /user/auth/social', () => {
});
describe('facebook', () => {
before(async () => {
const expectedResult = { id: facebookId, displayName: 'a facebook user' };
beforeEach(async () => {
randomFacebookId = generateUUID();
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);
network = 'facebook';
});
afterEach(async () => {
passport._strategies.facebook.userProfile.restore();
});
it('registers a new user', async () => {
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
@@ -51,7 +64,8 @@ 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, 'auth.local.lowerCaseUsername')).to.exist;
await expect(getProperty('users', response.id, 'auth.facebook.id')).to.eventually.equal(facebookId);
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(randomFacebookId);
});
it('logs an existing user in', async () => {
@@ -68,6 +82,57 @@ describe('POST /user/auth/social', () => {
expect(response.apiToken).to.eql(registerResponse.apiToken);
expect(response.id).to.eql(registerResponse.id);
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 () => {
@@ -76,11 +141,28 @@ describe('POST /user/auth/social', () => {
network,
});
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.apiToken).to.eql(user.apiToken);
expect(response.id).to.eql(user._id);
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 () => {
await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
@@ -92,12 +174,23 @@ describe('POST /user/auth/social', () => {
});
describe('google', () => {
before(async () => {
const expectedResult = { id: googleId, displayName: 'a google user' };
beforeEach(async () => {
randomGoogleId = generateUUID();
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);
network = 'google';
});
afterEach(async () => {
passport._strategies.google.userProfile.restore();
});
it('registers a new user', async () => {
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
@@ -107,7 +200,8 @@ describe('POST /user/auth/social', () => {
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.newUser).to.be.true;
await expect(getProperty('users', response.id, 'auth.google.id')).to.eventually.equal(googleId);
await expect(getProperty('users', response.id, 'auth.google.id')).to.eventually.equal(randomGoogleId);
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');
});
@@ -125,6 +219,57 @@ describe('POST /user/auth/social', () => {
expect(response.apiToken).to.eql(registerResponse.apiToken);
expect(response.id).to.eql(registerResponse.id);
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 () => {
@@ -133,11 +278,28 @@ describe('POST /user/auth/social', () => {
network,
});
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.apiToken).to.eql(user.apiToken);
expect(response.id).to.eql(user._id);
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 () => {
await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase

View File

@@ -226,7 +226,7 @@
:key="network.key"
>
<button
v-if="!user.auth[network.key].id"
v-if="!user.auth[network.key].id && network.key !== 'facebook'"
class="btn btn-primary mb-2"
@click="socialAuth(network.key, user)"
>
@@ -387,7 +387,7 @@
{{ $t('saveAndConfirm') }}
</button>
</div>
<h5>
<h5 v-if="user.auth.local.has_password">
{{ $t('changeEmail') }}
</h5>
<div
@@ -497,25 +497,20 @@
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
input {
color: $gray-50;
}
.usersettings h5 {
margin-top: 1em;
}
.iconalert > div > span {
line-height: 25px;
}
.iconalert > div:after {
clear: both;
content: '';
display: table;
}
.input-error {
color: $red-50;
font-size: 90%;
@@ -768,12 +763,10 @@ export default {
if (network === 'apple') {
window.location.href = buildAppleAuthUrl();
} else {
const auth = await hello(network).login({ scope: 'email', options: { force: true } });
const auth = await hello(network).login({ scope: 'email' });
await this.$store.dispatch('auth:socialAuth', {
auth,
});
window.location.href = '/';
}
},
@@ -792,7 +785,6 @@ export default {
}
await axios.post('/api/v4/user/auth/local/register', this.localAuth);
window.alert(this.$t('addedLocalAuth')); // eslint-disable-line no-alert
window.location.href = '/';
},
restoreEmptyUsername () {
if (this.usernameUpdates.username.length < 1) {

View File

@@ -133,6 +133,7 @@
"unsupportedNetwork": "This network is not currently supported.",
"cantDetachSocial": "Account lacks another authentication method; can't detach this authentication method.",
"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.",
"memberIdRequired": "\"member\" must be a valid UUID.",
"heroIdRequired": "\"heroId\" must be a valid UUID.",

View File

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