diff --git a/test/api/v3/integration/user/POST-user_update_username.test.js b/test/api/v3/integration/user/POST-user_update_username.test.js deleted file mode 100644 index 4e59fcb5b8..0000000000 --- a/test/api/v3/integration/user/POST-user_update_username.test.js +++ /dev/null @@ -1,82 +0,0 @@ -import { - generateUser, - translate as t, -} from '../../../../helpers/api-integration/v3'; -import { model as User } from '../../../../../website/src/models/user'; - -describe('POST /user/update-username', async () => { - let endpoint = '/user/update-username'; - let user; - let newUsername = 'new-username'; - let existingUsername = 'existing-username'; - let password = 'password'; // from habitrpg/test/helpers/api-integration/v3/object-generators.js - let wrongPassword = 'wrong-password'; - - beforeEach(async () => { - user = await generateUser(); - }); - - it('successfully changes username', async () => { - let response = await user.post(endpoint, { - username: newUsername, - password, - }); - expect(response).to.eql({ username: newUsername }); - user = await User.findOne({ _id: user._id }); - expect(user.auth.local.username).to.eql(newUsername); - }); - - context('errors', async () => { - describe('new username is unavailable', async () => { - beforeEach(async () => { - user = await generateUser(); - await user.update({'auth.local.username': existingUsername, 'auth.local.lowerCaseUsername': existingUsername }); - }); - it('prevents username update', async () => { - await expect(user.post(endpoint, { - username: existingUsername, - password, - })).to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: t('usernameTaken'), - }); - }); - }); - it('password is wrong', async () => { - await expect(user.post(endpoint, { - username: newUsername, - password: wrongPassword, - })).to.eventually.be.rejected.and.eql({ - code: 401, - error: 'NotAuthorized', - message: t('wrongPassword'), - }); - }); - describe('social-only user', async () => { - beforeEach(async () => { - user = await generateUser(); - await user.update({ 'auth.local': { ok: true } }); - }); - it('prevents username update', async () => { - await expect(user.post(endpoint, { - username: newUsername, - password, - })).to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: t('userHasNoLocalRegistration'), - }); - }); - }); - it('new username is not provided', async () => { - await expect(user.post(endpoint, { - password, - })).to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: t('invalidReqParams'), - }); - }); - }); -}); diff --git a/test/api/v3/integration/user/POST-user_update_email.test.js b/test/api/v3/integration/user/auth/PUT-user_update_email.test.js similarity index 60% rename from test/api/v3/integration/user/POST-user_update_email.test.js rename to test/api/v3/integration/user/auth/PUT-user_update_email.test.js index 5a8392ec1e..47357d3c85 100644 --- a/test/api/v3/integration/user/POST-user_update_email.test.js +++ b/test/api/v3/integration/user/auth/PUT-user_update_email.test.js @@ -1,23 +1,23 @@ import { generateUser, translate as t, -} from '../../../../helpers/api-v3-integration.helper'; -import { model as User } from '../../../../../website/src/models/user'; +} from '../../../../../helpers/api-v3-integration.helper'; -describe('POST /user/update-email', () => { - let user; - let fbUser; - let endpoint = '/user/update-email'; +const ENDPOINT = '/user/auth/update-email'; + +describe('PUT /user/auth/update-email', () => { let newEmail = 'some-new-email_2@example.net'; - let thePassword = 'password'; // from habitrpg/test/helpers/api-integration/v3/object-generators.js + let oldPassword = 'password'; // from habitrpg/test/helpers/api-integration/v3/object-generators.js + + context('Local Authenticaion User', async () => { + let user; - describe('local user', async () => { beforeEach(async () => { user = await generateUser(); }); - it('does not change email if one is not provided', async () => { - await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({ + it('does not change email if email is not provided', async () => { + await expect(user.put(ENDPOINT)).to.eventually.be.rejected.and.eql({ code: 400, error: 'BadRequest', message: t('invalidReqParams'), @@ -25,7 +25,7 @@ describe('POST /user/update-email', () => { }); it('does not change email if password is not provided', async () => { - await expect(user.post(endpoint, { + await expect(user.put(ENDPOINT, { newEmail, })).to.eventually.be.rejected.and.eql({ code: 400, @@ -35,7 +35,7 @@ describe('POST /user/update-email', () => { }); it('does not change email if wrong password is provided', async () => { - await expect(user.post(endpoint, { + await expect(user.put(ENDPOINT, { newEmail, password: 'wrong password', })).to.eventually.be.rejected.and.eql({ @@ -46,27 +46,29 @@ describe('POST /user/update-email', () => { }); it('changes email if new email and existing password are provided', async () => { - let response = await user.post(endpoint, { + let response = await user.put(ENDPOINT, { newEmail, - password: thePassword, + password: oldPassword, }); expect(response).to.eql({ email: 'some-new-email_2@example.net' }); - let id = user._id; - user = await User.findOne({ _id: id }); + + await user.sync(); expect(user.auth.local.email).to.eql(newEmail); }); }); - describe('facebook user', async () => { + context('Social Login User', async () => { + let socialUser; + beforeEach(async () => { - fbUser = await generateUser(); - await fbUser.update({ 'auth.local': { ok: true } }); + socialUser = await generateUser(); + await socialUser.update({ 'auth.local': { ok: true } }); }); it('does not change email if user.auth.local.email does not exist for this user', async () => { - await expect(fbUser.post(endpoint, { + await expect(socialUser.put(ENDPOINT, { newEmail, - password: thePassword, + password: oldPassword, })).to.eventually.be.rejected.and.eql({ code: 400, error: 'BadRequest', diff --git a/test/api/v3/integration/user/POST-user_update_password.test.js b/test/api/v3/integration/user/auth/PUT-user_update_password.test.js similarity index 65% rename from test/api/v3/integration/user/POST-user_update_password.test.js rename to test/api/v3/integration/user/auth/PUT-user_update_password.test.js index 728abb13db..bcc1ac25d3 100644 --- a/test/api/v3/integration/user/POST-user_update_password.test.js +++ b/test/api/v3/integration/user/auth/PUT-user_update_password.test.js @@ -1,12 +1,13 @@ import { generateUser, translate as t, -} from '../../../../helpers/api-integration/v3'; +} from '../../../../../helpers/api-v3-integration.helper'; -describe('POST /user/update-password', async () => { - let endpoint = '/user/update-password'; +const ENDPOINT = '/user/auth/update-password'; + +describe('PUT /user/auth/update-password', async () => { let user; - let password = 'password'; + let password = 'password'; // from habitrpg/test/helpers/api-integration/v3/object-generators.js let wrongPassword = 'wrong-password'; let newPassword = 'new-password'; @@ -16,7 +17,7 @@ describe('POST /user/update-password', async () => { it('successfully changes the password', async () => { let previousHashedPassword = user.auth.local.hashed_password; - let response = await user.post(endpoint, { + let response = await user.put(ENDPOINT, { password, newPassword, confirmPassword: newPassword, @@ -26,8 +27,8 @@ describe('POST /user/update-password', async () => { expect(user.auth.local.hashed_password).to.not.eql(previousHashedPassword); }); - it('new passwords mismatch', async () => { - await expect(user.post(endpoint, { + it('returns an error when confirmPassword does not match newPassword', async () => { + await expect(user.put(ENDPOINT, { password, newPassword, confirmPassword: `${newPassword}-wrong-confirmation`, @@ -38,8 +39,8 @@ describe('POST /user/update-password', async () => { }); }); - it('existing password is wrong', async () => { - await expect(user.post(endpoint, { + it('returns an error when existing password is wrong', async () => { + await expect(user.put(ENDPOINT, { password: wrongPassword, newPassword, confirmPassword: newPassword, diff --git a/test/api/v3/integration/user/auth/PUT-user_update_username.test.js b/test/api/v3/integration/user/auth/PUT-user_update_username.test.js new file mode 100644 index 0000000000..c61e85ab1e --- /dev/null +++ b/test/api/v3/integration/user/auth/PUT-user_update_username.test.js @@ -0,0 +1,77 @@ +import { + generateUser, + translate as t, +} from '../../../../../helpers/api-v3-integration.helper'; + +const ENDPOINT = '/user/auth/update-username'; + +describe('PUT /user/auth/update-username', async () => { + let user; + let newUsername = 'new-username'; + let password = 'password'; // from habitrpg/test/helpers/api-integration/v3/object-generators.js + + beforeEach(async () => { + user = await generateUser(); + }); + + it('successfully changes username', async () => { + let response = await user.put(ENDPOINT, { + username: newUsername, + password, + }); + expect(response).to.eql({ username: newUsername }); + await user.sync(); + expect(user.auth.local.username).to.eql(newUsername); + }); + + context('errors', async () => { + it('prevents username update if new username is already taken', async () => { + let existingUsername = 'existing-username'; + await generateUser({'auth.local.username': existingUsername, 'auth.local.lowerCaseUsername': existingUsername }); + + await expect(user.put(ENDPOINT, { + username: existingUsername, + password, + })).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('usernameTaken'), + }); + }); + + it('errors if password is wrong', async () => { + await expect(user.put(ENDPOINT, { + username: newUsername, + password: 'wrong-password', + })).to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('wrongPassword'), + }); + }); + + + it('prevents social-only user from changing username', async () => { + let socialUser = await generateUser({ 'auth.local': { ok: true } }); + + await expect(socialUser.put(ENDPOINT, { + username: newUsername, + password, + })).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('userHasNoLocalRegistration'), + }); + }); + + it('errors if new username is not provided', async () => { + await expect(user.put(ENDPOINT, { + password, + })).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('invalidReqParams'), + }); + }); + }); +}); diff --git a/website/src/controllers/api-v3/auth.js b/website/src/controllers/api-v3/auth.js index ad3c26bbdd..875d8672ca 100644 --- a/website/src/controllers/api-v3/auth.js +++ b/website/src/controllers/api-v3/auth.js @@ -9,6 +9,7 @@ import { import cron from '../../middlewares/api-v3/cron'; import { NotAuthorized, + BadRequest, NotFound, } from '../../libs/api-v3/errors'; import Q from 'q'; @@ -283,6 +284,125 @@ api.loginSocial = { }, }; +/** + * @api {put} /user/auth/update-username + * @apiVersion 3.0.0 + * @apiName updateUsername + * @apiGroup User + * @apiParam {string} password The password + * @apiParam {string} username New username + * @apiSuccess {Object} The new username + **/ +api.updateUsername = { + method: 'PUT', + middlewares: [authWithHeaders(), cron], + url: '/user/auth/update-username', + async handler (req, res) { + let user = res.locals.user; + + req.checkBody({ + password: { + notEmpty: {errorMessage: res.t('missingPassword')}, + }, + username: { + notEmpty: { errorMessage: res.t('missingUsername') }, + }, + }); + + let validationErrors = req.validationErrors(); + if (validationErrors) throw validationErrors; + + if (!user.auth.local.username) throw new BadRequest(res.t('userHasNoLocalRegistration')); + + let oldPassword = passwordUtils.encrypt(req.body.password, user.auth.local.salt); + if (oldPassword !== user.auth.local.hashed_password) throw new NotAuthorized(res.t('wrongPassword')); + + let count = await User.count({ 'auth.local.lowerCaseUsername': req.body.username.toLowerCase() }); + if (count > 0) throw new BadRequest(res.t('usernameTaken')); + + // save username + user.auth.local.lowerCaseUsername = req.body.username.toLowerCase(); + user.auth.local.username = req.body.username; + await user.save(); + + res.respond(200, { username: req.body.username }); + }, +}; + +/** + * @api {put} /user/auth/update-password + * @apiVersion 3.0.0 + * @apiName updatePassword + * @apiGroup User + * @apiParam {string} password The old password + * @apiParam {string} newPassword The new password + * @apiParam {string} confirmPassword Password confirmation + * @apiSuccess {Object} The success message + **/ +api.updatePassword = { + method: 'PUT', + middlewares: [authWithHeaders(), cron], + url: '/user/auth/update-password', + async handler (req, res) { + let user = res.locals.user; + + if (!user.auth.local.hashed_password) throw new BadRequest(res.t('userHasNoLocalRegistration')); + + let oldPassword = passwordUtils.encrypt(req.body.password, user.auth.local.salt); + if (oldPassword !== user.auth.local.hashed_password) throw new NotAuthorized(res.t('wrongPassword')); + + req.checkBody({ + password: { + notEmpty: {errorMessage: res.t('missingNewPassword')}, + }, + newPassword: { + notEmpty: {errorMessage: res.t('missingPassword')}, + }, + }); + + if (req.body.newPassword !== req.body.confirmPassword) throw new NotAuthorized(res.t('passwordConfirmationMatch')); + + user.auth.local.hashed_password = passwordUtils.encrypt(req.body.newPassword, user.auth.local.salt); // eslint-disable-line camelcase + await user.save(); + res.respond(200, {}); + }, +}; + +/** + * @api {put} /user/auth/update-email + * @apiVersion 3.0.0 + * @apiName UpdateEmail + * @apiGroup User + * + * @apiParam {string} newEmail The new email address. + * @apiParam {string} password The user password. + * + * @apiSuccess {Object} An object containing the new email address + */ +api.updateEmail = { + method: 'PUT', + middlewares: [authWithHeaders(), cron], + url: '/user/auth/update-email', + async handler (req, res) { + let user = res.locals.user; + + if (!user.auth.local.email) throw new BadRequest(res.t('userHasNoLocalRegistration')); + + req.checkBody('newEmail', res.t('newEmailRequired')).notEmpty().isEmail(); + req.checkBody('password', res.t('missingPassword')).notEmpty(); + let validationErrors = req.validationErrors(); + if (validationErrors) throw validationErrors; + + let candidatePassword = passwordUtils.encrypt(req.body.password, user.auth.local.salt); + if (candidatePassword !== user.auth.local.hashed_password) throw new NotAuthorized(res.t('wrongPassword')); + + user.auth.local.email = req.body.newEmail; + await user.save(); + + return res.respond(200, { email: user.auth.local.email }); + }, +}; + const firebaseTokenGenerator = new FirebaseTokenGenerator(nconf.get('FIREBASE:SECRET')); // Internal route TODO expose? diff --git a/website/src/controllers/api-v3/user.js b/website/src/controllers/api-v3/user.js index 17c37aaf84..cb5b0524ff 100644 --- a/website/src/controllers/api-v3/user.js +++ b/website/src/controllers/api-v3/user.js @@ -11,7 +11,6 @@ import { model as Group } from '../../models/group'; import { model as User } from '../../models/user'; import Q from 'q'; import _ from 'lodash'; -import * as passwordUtils from '../../libs/api-v3/password'; let api = {}; @@ -42,126 +41,6 @@ api.getUser = { }, }; -/** - * @api {post} /user/update-password - * @apiVersion 3.0.0 - * @apiName updatePassword - * @apiGroup User - * @apiParam {string} password The old password - * @apiParam {string} newPassword The new password - * @apiParam {string} confirmPassword Password confirmation - * @apiSuccess {Object} The success message - **/ -api.updatePassword = { - method: 'POST', - middlewares: [authWithHeaders(), cron], - url: '/user/update-password', - async handler (req, res) { - let user = res.locals.user; - - if (!user.auth.local.hashed_password) throw new BadRequest(res.t('userHasNoLocalRegistration')); - - let oldPassword = passwordUtils.encrypt(req.body.password, user.auth.local.salt); - if (oldPassword !== user.auth.local.hashed_password) throw new NotAuthorized(res.t('wrongPassword')); - - req.checkBody({ - password: { - notEmpty: {errorMessage: res.t('missingNewPassword')}, - }, - newPassword: { - notEmpty: {errorMessage: res.t('missingPassword')}, - }, - }); - - if (req.body.newPassword !== req.body.confirmPassword) throw new NotAuthorized(res.t('passwordConfirmationMatch')); - - user.auth.local.hashed_password = passwordUtils.encrypt(req.body.newPassword, user.auth.local.salt); // eslint-disable-line camelcase - await user.save(); - res.respond(200, {}); - }, -}; - -/** - * @api {post} /user/update-username - * @apiVersion 3.0.0 - * @apiName updateUsername - * @apiGroup User - * @apiParam {string} password The password - * @apiParam {string} username New username - * @apiSuccess {Object} The new username - **/ -api.updateUsername = { - method: 'POST', - middlewares: [authWithHeaders(), cron], - url: '/user/update-username', - async handler (req, res) { - let user = res.locals.user; - - req.checkBody({ - password: { - notEmpty: {errorMessage: res.t('missingPassword')}, - }, - username: { - notEmpty: { errorMessage: res.t('missingUsername') }, - }, - }); - - let validationErrors = req.validationErrors(); - if (validationErrors) throw validationErrors; - - if (!user.auth.local.username) throw new BadRequest(res.t('userHasNoLocalRegistration')); - - let oldPassword = passwordUtils.encrypt(req.body.password, user.auth.local.salt); - if (oldPassword !== user.auth.local.hashed_password) throw new NotAuthorized(res.t('wrongPassword')); - - let count = await User.count({ 'auth.local.lowerCaseUsername': req.body.username.toLowerCase() }); - if (count > 0) throw new BadRequest(res.t('usernameTaken')); - - // save username - user.auth.local.lowerCaseUsername = req.body.username.toLowerCase(); - user.auth.local.username = req.body.username; - await user.save(); - - res.respond(200, { username: req.body.username }); - }, -}; - - -/** - * @api {post} /user/update-email - * @apiVersion 3.0.0 - * @apiName UpdateEmail - * @apiGroup User - * - * @apiParam {string} newEmail The new email address. - * @apiParam {string} password The user password. - * - * @apiSuccess {Object} An object containing the new email address - */ -api.updateEmail = { - method: 'POST', - middlewares: [authWithHeaders(), cron], - url: '/user/update-email', - async handler (req, res) { - let user = res.locals.user; - - if (!user.auth.local.email) throw new BadRequest(res.t('userHasNoLocalRegistration')); - - req.checkBody('newEmail', res.t('newEmailRequired')).notEmpty().isEmail(); - req.checkBody('password', res.t('missingPassword')).notEmpty(); - let validationErrors = req.validationErrors(); - if (validationErrors) throw validationErrors; - - let candidatePassword = passwordUtils.encrypt(req.body.password, user.auth.local.salt); - if (candidatePassword !== user.auth.local.hashed_password) throw new NotAuthorized(res.t('wrongPassword')); - - user.auth.local.email = req.body.newEmail; - await user.save(); - - return res.respond(200, { email: user.auth.local.email }); - }, -}; - const partyMembersFields = 'profile.name stats achievements items.special'; /**