diff --git a/config.json.example b/config.json.example index fbf18f1b82..04c1c535b4 100644 --- a/config.json.example +++ b/config.json.example @@ -32,8 +32,8 @@ "LOGGLY_SUBDOMAIN": "example-subdomain", "LOGGLY_TOKEN": "example-token", "MAINTENANCE_MODE": "false", - "NODE_DB_URI": "mongodb://localhost:27017/habitica-dev", - "TEST_DB_URI": "mongodb://localhost:27017/habitica-test", + "NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs", + "TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs", "MONGODB_POOL_SIZE": "10", "NODE_ENV": "development", "PATH": "bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin", diff --git a/test/api/v3/integration/user/auth/GET-user_auth_apple.test.js b/test/api/v3/integration/user/auth/GET-user_auth_apple.test.js index b1c97f6ca4..975fccfa3f 100644 --- a/test/api/v3/integration/user/auth/GET-user_auth_apple.test.js +++ b/test/api/v3/integration/user/auth/GET-user_auth_apple.test.js @@ -1,4 +1,3 @@ -import { v4 as generateUUID } from 'uuid'; import { generateUser, requester, @@ -10,18 +9,15 @@ describe('GET /user/auth/apple', () => { let api; let user; 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 () => { 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 () => { @@ -30,7 +26,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(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'); }); diff --git a/test/api/v3/integration/user/auth/POST-user_auth_social.test.js b/test/api/v3/integration/user/auth/POST-user_auth_social.test.js index c4ffe57efb..eff5525603 100644 --- a/test/api/v3/integration/user/auth/POST-user_auth_social.test.js +++ b/test/api/v3/integration/user/auth/POST-user_auth_social.test.js @@ -1,5 +1,4 @@ import passport from 'passport'; -import { v4 as generateUUID } from 'uuid'; import { generateUser, requester, @@ -11,15 +10,14 @@ describe('POST /user/auth/social', () => { let api; let user; const endpoint = '/user/auth/social'; - let randomAccessToken = '123456'; - let randomFacebookId = 'facebookId'; - let randomGoogleId = 'googleId'; + const randomAccessToken = '123456'; + const facebookId = 'facebookId'; + const googleId = 'googleId'; let network = 'NoNetwork'; beforeEach(async () => { api = requester(); user = await generateUser(); - randomAccessToken = generateUUID(); }); it('fails if network is not supported', async () => { @@ -34,23 +32,12 @@ describe('POST /user/auth/social', () => { }); describe('facebook', () => { - beforeEach(async () => { - randomFacebookId = generateUUID(); - const expectedResult = { - id: randomFacebookId, - displayName: 'a facebook user', - emails: [ - { value: `${user.auth.local.username}+facebook@example.com` }, - ], - }; + before(async () => { + const expectedResult = { id: facebookId, displayName: 'a facebook user' }; 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 @@ -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, '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(randomFacebookId); + await expect(getProperty('users', response.id, 'auth.facebook.id')).to.eventually.equal(facebookId); }); 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.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 () => { @@ -141,28 +76,11 @@ describe('POST /user/auth/social', () => { network, }); - expect(response.apiToken).to.eql(user.apiToken); - expect(response.id).to.eql(user._id); + expect(response.apiToken).to.exist; + expect(response.id).to.exist; 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 @@ -174,23 +92,12 @@ describe('POST /user/auth/social', () => { }); describe('google', () => { - beforeEach(async () => { - randomGoogleId = generateUUID(); - const expectedResult = { - id: randomGoogleId, - displayName: 'a google user', - emails: [ - { value: `${user.auth.local.username}+google@example.com` }, - ], - }; + before(async () => { + const expectedResult = { id: googleId, displayName: 'a google user' }; 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 @@ -200,8 +107,7 @@ 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(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, 'auth.google.id')).to.eventually.equal(googleId); 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.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 () => { @@ -278,28 +133,11 @@ describe('POST /user/auth/social', () => { network, }); - expect(response.apiToken).to.eql(user.apiToken); - expect(response.id).to.eql(user._id); + expect(response.apiToken).to.exist; + expect(response.id).to.exist; 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 diff --git a/test/api/v3/integration/user/auth/POST-user_reset_password.test.js b/test/api/v3/integration/user/auth/POST-user_reset_password.test.js index da66e5fd3e..8603fa7cb3 100644 --- a/test/api/v3/integration/user/auth/POST-user_reset_password.test.js +++ b/test/api/v3/integration/user/auth/POST-user_reset_password.test.js @@ -25,19 +25,6 @@ describe('POST /user/reset-password', async () => { 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 () => { const response = await user.post(endpoint, { email: 'nonExistent@email.com', diff --git a/website/client/src/components/auth/registerLoginReset.vue b/website/client/src/components/auth/registerLoginReset.vue index 0ea5cf8f84..b406b124c3 100644 --- a/website/client/src/components/auth/registerLoginReset.vue +++ b/website/client/src/components/auth/registerLoginReset.vue @@ -20,8 +20,8 @@
{{ context.item.text }} - +

diff --git a/website/client/src/components/snackbars/notification.vue b/website/client/src/components/snackbars/notification.vue index 0334995c1a..0a007a4f4d 100644 --- a/website/client/src/components/snackbars/notification.vue +++ b/website/client/src/components/snackbars/notification.vue @@ -1,17 +1,10 @@ diff --git a/website/client/src/components/snackbars/notifications.vue b/website/client/src/components/snackbars/notifications.vue index eeab99bacf..40d2ceca0f 100644 --- a/website/client/src/components/snackbars/notifications.vue +++ b/website/client/src/components/snackbars/notifications.vue @@ -81,12 +81,12 @@ const DELAY_DELETE_AND_NEW = 60; const DELAY_FILLING_ENTRIES = 240; export default { - components: { - notification, - }, mixins: [ worldStateMixin, ], + components: { + notification, + }, props: { preventQueue: { type: Boolean, diff --git a/website/common/locales/en/front.json b/website/common/locales/en/front.json index 9b1fae34ef..58c3e06b80 100644 --- a/website/common/locales/en/front.json +++ b/website/common/locales/en/front.json @@ -133,7 +133,6 @@ "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.", diff --git a/website/server/controllers/api-v3/auth.js b/website/server/controllers/api-v3/auth.js index 6b7c16636e..7860a3db61 100644 --- a/website/server/controllers/api-v3/auth.js +++ b/website/server/controllers/api-v3/auth.js @@ -341,14 +341,7 @@ api.resetPassword = { if (validationErrors) throw validationErrors; const email = req.body.email.toLowerCase(); - const user = await User.findOne({ - $or: [ - { 'auth.local.email': email }, - { 'auth.apple.emails.value': email }, - { 'auth.google.emails.value': email }, - { 'auth.facebook.emails.value': email }, - ], - }).exec(); + const user = await User.findOne({ 'auth.local.email': email }).exec(); if (user) { // create an encrypted link to be used to reset the password diff --git a/website/server/libs/auth/social.js b/website/server/libs/auth/social.js index 61883a2f87..02e4d64faf 100644 --- a/website/server/libs/auth/social.js +++ b/website/server/libs/auth/social.js @@ -1,6 +1,6 @@ import passport from 'passport'; import common from '../../../common'; -import { BadRequest, NotAuthorized } from '../errors'; +import { BadRequest } 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 - let existingUser = res.locals.user; + const existingUser = res.locals.user; const { network } = req.body; const isSupportedNetwork = common.constants.SUPPORTED_SOCIAL_NETWORKS @@ -47,52 +47,37 @@ 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); } - let email; - if (profile.emails && profile.emails[0] && profile.emails[0].value) { - email = profile.emails[0].value.toLowerCase(); - } + const generatedUsername = generateUsername(); - if (!existingUser && email) { - existingUser = await User.findOne({ 'auth.local.email': email }).exec(); - } + 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) { - existingUser.auth[network] = { - id: profile.id, - emails: profile.emails, - }; + existingUser.auth[network] = user.auth[network]; 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 } @@ -100,15 +85,19 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre const savedUser = await user.save(); if (!existingUser) { - savedUser.newUser = true; + user.newUser = true; } - const response = loginRes(savedUser, req, res); + const response = loginRes(user, req, res); // Clean previous email preferences - if (email) { + if ( + savedUser.auth[network].emails + && savedUser.auth[network].emails[0] + && savedUser.auth[network].emails[0].value + ) { EmailUnsubscription - .remove({ email }) + .remove({ email: savedUser.auth[network].emails[0].value.toLowerCase() }) .exec() .then(() => { if (!existingUser) {