diff --git a/services/web/app/coffee/Features/User/UserController.coffee b/services/web/app/coffee/Features/User/UserController.coffee index c284da2711..47c093d590 100644 --- a/services/web/app/coffee/Features/User/UserController.coffee +++ b/services/web/app/coffee/Features/User/UserController.coffee @@ -15,12 +15,26 @@ settings = require "settings-sharelatex" module.exports = UserController = - deleteUser: (req, res)-> + tryDeleteUser: (req, res, next) -> user_id = AuthenticationController.getLoggedInUserId(req) - UserDeleter.deleteUser user_id, (err)-> - if !err? + password = req.body.password + logger.log {user_id}, "trying to delete user account" + if !password? or password == '' + logger.err {user_id}, 'no password supplied for attempt to delete account' + return res.sendStatus(403) + AuthenticationManager.authenticate {_id: user_id}, password, (err, user) -> + if err? + logger.err {user_id}, 'error authenticating during attempt to delete account' + return next(err) + if !user + logger.err {user_id}, 'auth failed during attempt to delete account' + return res.sendStatus(403) + UserDeleter.deleteUser user_id, (err) -> + if err? + logger.err {user_id}, "error while deleting user account" + return next(err) req.session?.destroy() - res.sendStatus(200) + res.sendStatus(200) unsubscribe: (req, res)-> user_id = AuthenticationController.getLoggedInUserId(req) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 56dd8d821b..a027f6359e 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -92,7 +92,7 @@ module.exports = class Router webRouter.post '/user/sessions/clear', AuthenticationController.requireLogin(), UserController.clearSessions webRouter.delete '/user/newsletter/unsubscribe', AuthenticationController.requireLogin(), UserController.unsubscribe - webRouter.delete '/user', AuthenticationController.requireLogin(), UserController.deleteUser + webRouter.post '/user/delete', AuthenticationController.requireLogin(), UserController.tryDeleteUser webRouter.get '/user/personal_info', AuthenticationController.requireLogin(), UserInfoController.getLoggedInUsersPersonalInfo apiRouter.get '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo diff --git a/services/web/app/views/user/settings.jade b/services/web/app/views/user/settings.jade index d2fa8326d1..eb5bf671fa 100644 --- a/services/web/app/views/user/settings.jade +++ b/services/web/app/views/user/settings.jade @@ -150,16 +150,32 @@ block content script(type='text/ng-template', id='deleteAccountModalTemplate') .modal-header h3 #{translate("delete_account")} - .modal-body - p !{translate("delete_account_warning_message_2")} + div.modal-body#delete-account-modal + p !{translate("delete_account_warning_message_3")} form(novalidate, name="deleteAccountForm") + label #{translate('email')} input.form-control( type="text", + autocomplete="off", placeholder="", ng-model="state.deleteText", focus-on="open", ng-keyup="checkValidation()" ) + label #{translate('password')} + input.form-control( + type="password", + autocomplete="off", + placeholder="", + ng-model="state.password", + ng-keyup="checkValidation()" + ) + div(ng-if="state.error") + div.alert.alert-danger + | #{translate('generic_something_went_wrong')} + div(ng-if="state.invalidCredentials") + div.alert.alert-danger + | #{translate('email_or_password_wrong_try_again')} .modal-footer button.btn.btn-default( ng-click="cancel()" diff --git a/services/web/public/coffee/main/account-settings.coffee b/services/web/public/coffee/main/account-settings.coffee index 29ec146051..08226ab399 100644 --- a/services/web/public/coffee/main/account-settings.coffee +++ b/services/web/public/coffee/main/account-settings.coffee @@ -29,10 +29,13 @@ define [ App.controller "DeleteAccountModalController", [ "$scope", "$modalInstance", "$timeout", "$http", ($scope, $modalInstance, $timeout, $http) -> - $scope.state = + $scope.state = isValid : false deleteText: "" + password: "" inflight: false + error: false + invalidCredentials: false $modalInstance.opened.then () -> $timeout () -> @@ -40,20 +43,33 @@ define [ , 700 $scope.checkValidation = -> - $scope.state.isValid = $scope.state.deleteText == $scope.email + $scope.state.isValid = $scope.state.deleteText == $scope.email and $scope.state.password.length > 0 $scope.delete = () -> $scope.state.inflight = true - + $scope.state.error = false + $scope.state.invalidCredentials = false $http({ - method: "DELETE" - url: "/user" + method: "POST" + url: "/user/delete" headers: "X-CSRF-Token": window.csrfToken + "Content-Type": 'application/json' + data: + password: $scope.state.password }) .success () -> $modalInstance.close() + $scope.state.inflight = false + $scope.state.error = false + $scope.state.invalidCredentials = false window.location = "/" + .error (data, status) -> + $scope.state.inflight = false + if status == 403 + $scope.state.invalidCredentials = true + else + $scope.state.error = true $scope.cancel = () -> $modalInstance.dismiss('cancel') diff --git a/services/web/public/stylesheets/app/account-settings.less b/services/web/public/stylesheets/app/account-settings.less index 23769e055b..c232e36fab 100644 --- a/services/web/public/stylesheets/app/account-settings.less +++ b/services/web/public/stylesheets/app/account-settings.less @@ -2,4 +2,11 @@ .alert { margin-bottom: 0; } -} \ No newline at end of file +} + +#delete-account-modal { + .alert { + margin-top: 25px; + margin-bottom: 4px; + } +} diff --git a/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee b/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee index a9c98e02ec..cc1d2190ef 100644 --- a/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee @@ -84,15 +84,81 @@ describe "UserController", -> sendStatus: sinon.stub() json: sinon.stub() @next = sinon.stub() - describe "deleteUser", -> - it "should delete the user", (done)-> + describe 'tryDeleteUser', -> - @res.sendStatus = (code)=> - @UserDeleter.deleteUser.calledWith(@user_id) + beforeEach -> + @req.body.password = 'wat' + @AuthenticationController.getLoggedInUserId = sinon.stub().returns(@user._id) + @AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, null, @user) + @UserDeleter.deleteUser = sinon.stub().callsArgWith(1, null) + + it 'should send 200', (done) -> + @res.sendStatus = (code) => code.should.equal 200 done() - @UserController.deleteUser @req, @res + @UserController.tryDeleteUser @req, @res, @next + + it 'should try to authenticate user', (done) -> + @res.sendStatus = (code) => + @AuthenticationManager.authenticate.callCount.should.equal 1 + @AuthenticationManager.authenticate.calledWith({_id: @user._id}, @req.body.password).should.equal true + done() + @UserController.tryDeleteUser @req, @res, @next + + it 'should delete the user', (done) -> + @res.sendStatus = (code) => + @UserDeleter.deleteUser.callCount.should.equal 1 + @UserDeleter.deleteUser.calledWith(@user._id).should.equal true + done() + @UserController.tryDeleteUser @req, @res, @next + + describe 'when no password is supplied', -> + + beforeEach -> + @req.body.password = '' + + it 'should return 403', (done) -> + @res.sendStatus = (code) => + code.should.equal 403 + done() + @UserController.tryDeleteUser @req, @res, @next + + describe 'when authenticate produces an error', -> + + beforeEach -> + @AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, new Error('woops')) + + it 'should call next with an error', (done) -> + @next = (err) => + expect(err).to.not.equal null + expect(err).to.be.instanceof Error + done() + @UserController.tryDeleteUser @req, @res, @next + + describe 'when authenticate does not produce a user', -> + + beforeEach -> + @AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, null, null) + + it 'should return 403', (done) -> + @res.sendStatus = (code) => + code.should.equal 403 + done() + @UserController.tryDeleteUser @req, @res, @next + + describe 'when deleteUser produces an error', -> + + beforeEach -> + @UserDeleter.deleteUser = sinon.stub().callsArgWith(1, new Error('woops')) + + it 'should call next with an error', (done) -> + @next = (err) => + expect(err).to.not.equal null + expect(err).to.be.instanceof Error + done() + @UserController.tryDeleteUser @req, @res, @next + describe "unsubscribe", ->