[misc] move /user/activate into a module (#2962)

* [misc] move /user/activate into a module

Co-Authored-By: Nate Stemen <nate.stemen@overleaf.com>

* [misc] setup copybara for the new user-activate module

* [misc] move the /user/activate route behind a feature flag

...which is by default enabled.

Co-authored-by: Nate Stemen <nate.stemen@overleaf.com>
GitOrigin-RevId: 87fc5ae869a7e282ffdbeea0ff7b7c55b8b9b31b
This commit is contained in:
Jakob Ackermann
2020-07-16 02:06:51 +00:00
committed by Copybot
co-authored by Nate Stemen
parent c660497859
commit 53927bca95
9 changed files with 157 additions and 96 deletions
@@ -0,0 +1,43 @@
const Path = require('path')
const UserGetter = require('../../../../app/src/Features/User/UserGetter')
const ErrorController = require('../../../../app/src/Features/Errors/ErrorController')
module.exports = {
activateAccountPage(req, res, next) {
// An 'activation' is actually just a password reset on an account that
// was set with a random password originally.
if (req.query.user_id == null || req.query.token == null) {
return ErrorController.notFound(req, res)
}
if (typeof req.query.user_id !== 'string') {
return ErrorController.forbidden(req, res)
}
UserGetter.getUser(
req.query.user_id,
{ email: 1, loginCount: 1 },
(error, user) => {
if (error != null) {
return next(error)
}
if (!user) {
return ErrorController.notFound(req, res)
}
if (user.loginCount > 0) {
// Already seen this user, so account must be activate
// This lets users keep clicking the 'activate' link in their email
// as a way to log in which, if I know our users, they will.
res.redirect(`/login`)
} else {
req.session.doLoginAfterPasswordReset = true
res.render(Path.resolve(__dirname, '../views/user/activate'), {
title: 'activate_account',
email: user.email,
token: req.query.token
})
}
}
)
}
}
@@ -0,0 +1,17 @@
const logger = require('logger-sharelatex')
const Settings = require('settings-sharelatex')
const UserActivateController = require('./UserActivateController')
const AuthenticationController = require('../../../../app/src/Features/Authentication/AuthenticationController')
module.exports = {
apply(webRouter) {
if (Settings.disableModule['user-activate']) {
logger.log({}, 'Skipping Init UserActivate router')
return
}
logger.log({}, 'Init UserActivate router')
webRouter.get('/user/activate', UserActivateController.activateAccountPage)
AuthenticationController.addEndpointToLoginWhitelist('/user/activate')
}
}
@@ -0,0 +1,63 @@
extends ../../../../app/views/layout
block content
.content.content-alt
.container
.row
.col-md-6.col-md-offset-3.col-lg-4.col-lg-offset-4
.alert.alert-success #{translate("nearly_activated")}
.row
.col-md-6.col-md-offset-3.col-lg-4.col-lg-offset-4
.card
.page-header
h1 #{translate("please_set_a_password")}
form(
async-form="activate",
name="activationForm",
action="/user/password/set",
method="POST",
ng-cloak
)
input(name='_csrf', type='hidden', value=csrfToken)
input(
type="hidden",
name="passwordResetToken",
value=token
)
.alert.alert-danger(ng-show="activationForm.response.error")
| #{translate("activation_token_expired")}
.form-group
label(for='email') #{translate("email")}
input.form-control(
type='email',
name='email',
placeholder="email@example.com"
required,
ng-model="email",
ng-init="email = "+JSON.stringify(email),
ng-model-options="{ updateOn: 'blur' }",
disabled
)
.form-group
label(for='password') #{translate("password")}
input.form-control#passwordField(
type='password',
name='password',
placeholder="********",
required,
ng-model="password",
complex-password,
focus="true"
)
span.small.text-primary(ng-show="activationForm.password.$error.complexPassword", ng-bind-html="complexPasswordErrorMessage")
.actions
button.btn-primary.btn(
type='submit'
ng-disabled="activationForm.inflight || activationForm.password.$error.required|| activationForm.password.$error.complexPassword"
)
span(ng-show="!activationForm.inflight") #{translate("activate")}
span(ng-show="activationForm.inflight") #{translate("activating")}…
script(type='text/javascript').
window.passwordStrengthOptions = !{StringHelper.stringifyJsonForScript(settings.passwordStrengthOptions || {})}
@@ -0,0 +1,2 @@
const UserActivateRouter = require('./app/src/UserActivateRouter')
module.exports = { router: UserActivateRouter }
@@ -0,0 +1,92 @@
const Path = require('path')
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const MODULE_PATH = Path.join(
__dirname,
'../../../app/src/UserActivateController.js'
)
const VIEW_PATH = Path.join(__dirname, '../../../app/views/user/activate')
describe('UserActivateController', function() {
beforeEach(function() {
this.user = {
_id: (this.user_id = 'kwjewkl'),
features: {},
email: 'joe@example.com'
}
this.UserGetter = { getUser: sinon.stub() }
this.ErrorController = { notFound: sinon.stub() }
this.UserActivateController = SandboxedModule.require(MODULE_PATH, {
globals: {
console: console
},
requires: {
'../../../../app/src/Features/User/UserGetter': this.UserGetter,
'../../../../app/src/Features/Errors/ErrorController': this
.ErrorController
}
})
this.req = {
query: {},
session: {
user: this.user
}
}
this.res = {}
})
describe('activateAccountPage', function() {
beforeEach(function() {
this.UserGetter.getUser = sinon.stub().callsArgWith(2, null, this.user)
this.req.query.user_id = this.user_id
this.req.query.token = this.token = 'mock-token-123'
})
it('should 404 without a user_id', function(done) {
delete this.req.query.user_id
this.ErrorController.notFound = () => done()
this.UserActivateController.activateAccountPage(this.req, this.res)
})
it('should 404 without a token', function(done) {
delete this.req.query.token
this.ErrorController.notFound = () => done()
this.UserActivateController.activateAccountPage(this.req, this.res)
})
it('should 404 without a valid user_id', function(done) {
this.UserGetter.getUser = sinon.stub().callsArgWith(2, null, null)
this.ErrorController.notFound = () => done()
this.UserActivateController.activateAccountPage(this.req, this.res)
})
it('should 403 for complex user_id', function(done) {
this.ErrorController.forbidden = () => done()
this.req.query.user_id = { first_name: 'X' }
this.UserActivateController.activateAccountPage(this.req, this.res)
})
it('should redirect activated users to login', function(done) {
this.user.loginCount = 1
this.res.redirect = url => {
this.UserGetter.getUser.calledWith(this.user_id).should.equal(true)
url.should.equal('/login')
return done()
}
this.UserActivateController.activateAccountPage(this.req, this.res)
})
it('render the activation page if the user has not logged in before', function(done) {
this.user.loginCount = 0
this.res.render = (page, opts) => {
page.should.equal(VIEW_PATH)
opts.email.should.equal(this.user.email)
opts.token.should.equal(this.token)
return done()
}
this.UserActivateController.activateAccountPage(this.req, this.res)
})
})
})