diff --git a/services/web/app/src/Features/Authentication/AuthenticationController.js b/services/web/app/src/Features/Authentication/AuthenticationController.js index 17f5c5bc67..3fa161ebd1 100644 --- a/services/web/app/src/Features/Authentication/AuthenticationController.js +++ b/services/web/app/src/Features/Authentication/AuthenticationController.js @@ -121,6 +121,10 @@ const AuthenticationController = { return AsyncFormHelper.redirect(req, res, '/login') } // OAuth2 'state' mismatch + if (user.suspended) { + return AsyncFormHelper.redirect(req, res, '/account-suspended') + } + if (Settings.adminOnlyLogin && !hasAdminAccess(user)) { return res.status(403).json({ message: { type: 'error', text: 'Admin only panel' }, diff --git a/services/web/app/src/Features/User/UserPagesController.js b/services/web/app/src/Features/User/UserPagesController.js index b98bd6efc7..c54140cded 100644 --- a/services/web/app/src/Features/User/UserPagesController.js +++ b/services/web/app/src/Features/User/UserPagesController.js @@ -196,7 +196,18 @@ async function settingsPage(req, res) { }) } +async function accountSuspended(req, res) { + if (SessionManager.isUserLoggedIn(req.session)) { + return res.redirect('/project') + } + res.render('user/accountSuspended', { + title: 'your_account_is_suspended', + }) +} + const UserPagesController = { + accountSuspended: expressify(accountSuspended), + registerPage(req, res) { const sharedProjectData = { project_name: req.query.project_name, diff --git a/services/web/app/src/models/User.js b/services/web/app/src/models/User.js index 36fdb14ccb..bdcca49b10 100644 --- a/services/web/app/src/models/User.js +++ b/services/web/app/src/models/User.js @@ -196,6 +196,7 @@ const UserSchema = new Schema( splitTests: Schema.Types.Mixed, analyticsId: { type: String }, completedTutorials: Schema.Types.Mixed, + suspended: { type: Boolean }, }, { minimize: false } ) diff --git a/services/web/app/src/router.js b/services/web/app/src/router.js index 65d72e9bdf..ca59aa73ee 100644 --- a/services/web/app/src/router.js +++ b/services/web/app/src/router.js @@ -224,6 +224,8 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) { AuthenticationController.passportLogin ) + webRouter.get('/account-suspended', UserPagesController.accountSuspended) + if (Settings.enableLegacyLogin) { AuthenticationController.addEndpointToLoginWhitelist('/login/legacy') webRouter.get('/login/legacy', UserPagesController.loginPage) diff --git a/services/web/app/views/user/accountSuspended.pug b/services/web/app/views/user/accountSuspended.pug new file mode 100644 index 0000000000..dd473c9179 --- /dev/null +++ b/services/web/app/views/user/accountSuspended.pug @@ -0,0 +1,14 @@ +extends ../layout-marketing + +block vars + - var suppressNavbar = true + - var suppressFooter = true + - metadata.robotsNoindexNofollow = true + +block content + main.content.content-alt#main-content + .container-custom-sm.mx-auto + .card + h3 #{translate('your_account_is_suspended')} + p #{translate('sorry_this_account_has_been_suspended')} + p !{translate('please_contact_us_if_you_think_this_is_in_error', {}, [{name: 'a', attrs: {href: `mailto:${settings.adminEmail}`}}])} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 8d1602df20..ebb86054d7 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1335,6 +1335,7 @@ "please_confirm_email": "Please confirm your email __emailAddress__ by clicking on the link in the confirmation email ", "please_confirm_your_email_before_making_it_default": "Please confirm your email before making it the primary.", "please_contact_support_to_makes_change_to_your_plan": "Please <0>contact support to make changes to your plan", + "please_contact_us_if_you_think_this_is_in_error": "Please <0>contact us if you think this is in error.", "please_enter_confirmation_code": "Please enter your confirmation code", "please_enter_email": "Please enter your email address", "please_get_in_touch": "Please get in touch", @@ -1714,6 +1715,7 @@ "sorry_detected_sales_restricted_region": "Sorry, we’ve detected that you are in a region from which we cannot presently accept payments. If you think you’ve received this message in error, please contact us with details of your location, and we will look into this for you. We apologize for the inconvenience.", "sorry_something_went_wrong_opening_the_document_please_try_again": "Sorry, an unexpected error occurred when trying to open this content on Overleaf. Please try again.", "sorry_the_connection_to_the_server_is_down": "Sorry, the connection to the server is down.", + "sorry_this_account_has_been_suspended": "Sorry, this account has been suspended.", "sorry_your_table_cant_be_displayed_at_the_moment": "Sorry, your table can’t be displayed at the moment.", "sorry_your_token_expired": "Sorry, your token expired", "sort_by": "Sort by", @@ -2209,6 +2211,7 @@ "youll_need_to_ask_the_github_repository_owner": "You’ll need to ask the GitHub repository owner (<0>__repoOwnerEmail__) to reconnect the project.", "youll_no_longer_need_to_remember_credentials": "You’ll no longer need to remember a separate email address and password. Instead, you will use single-sign on to login to Overleaf. <0>Read more about SSO.", "your_account_is_managed_by_admin_cant_join_additional_group": "Your __appName__ account is managed by your current group admin (__admin__). This means you can’t join additional group subscriptions. <0>Read more about Managed Users.", + "your_account_is_suspended": "Your account is suspended", "your_affiliation_is_confirmed": "Your <0>__institutionName__ affiliation is confirmed.", "your_browser_does_not_support_this_feature": "Sorry, your browser doesn’t support this feature. Please update your browser to its latest version.", "your_compile_timed_out": "Your compile timed out", diff --git a/services/web/test/acceptance/src/helpers/User.js b/services/web/test/acceptance/src/helpers/User.js index d281471799..cbdcc3da50 100644 --- a/services/web/test/acceptance/src/helpers/User.js +++ b/services/web/test/acceptance/src/helpers/User.js @@ -237,6 +237,10 @@ class User { UserModel.updateOne({ _id: this.id }, { emails }, callback) } + setSuspended(suspended, callback) { + UserModel.updateOne({ _id: this.id }, { suspended }, callback) + } + logout(callback) { this.getCsrfToken(error => { if (error != null) { diff --git a/services/web/test/unit/src/Authentication/AuthenticationControllerTests.js b/services/web/test/unit/src/Authentication/AuthenticationControllerTests.js index b477a531c9..2c59df9ffe 100644 --- a/services/web/test/unit/src/Authentication/AuthenticationControllerTests.js +++ b/services/web/test/unit/src/Authentication/AuthenticationControllerTests.js @@ -1188,6 +1188,28 @@ describe('AuthenticationController', function () { }) }) + describe('when user account is suspended', function () { + beforeEach(function () { + this.req.session = {} + this.user.suspended = true + }) + it('should not log in and instead redirect to suspended account page', function () { + this.AuthenticationController.finishLogin( + this.user, + this.req, + this.res, + this.next + ) + sinon.assert.notCalled(this.req.login) + sinon.assert.calledWith( + this.AsyncFormHelper.redirect, + this.req, + this.res, + '/account-suspended' + ) + }) + }) + describe('preFinishLogin hook', function () { it('call hook and proceed', function () { this.Modules.hooks.fire = sinon.stub().yields(null, [])