mirror of
https://github.com/overleaf/overleaf.git
synced 2024-12-15 15:00:58 -05:00
Merge pull request #8259 from overleaf/ds-admin-panel-register-new-user
Migrating admin panel register new user to react GitOrigin-RevId: 520fea91cd9e560e4381504de45e5bedf11a7844
This commit is contained in:
parent
eba2fe9a3e
commit
5498d59c6b
8 changed files with 395 additions and 86 deletions
|
@ -23,7 +23,6 @@ import './main/translations'
|
||||||
import './main/subscription-dashboard'
|
import './main/subscription-dashboard'
|
||||||
import './main/new-subscription'
|
import './main/new-subscription'
|
||||||
import './main/annual-upgrade'
|
import './main/annual-upgrade'
|
||||||
import './main/register-users'
|
|
||||||
import './main/subscription/team-invite-controller'
|
import './main/subscription/team-invite-controller'
|
||||||
import './main/subscription/upgrade-subscription'
|
import './main/subscription/upgrade-subscription'
|
||||||
import './main/learn'
|
import './main/learn'
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
import _ from 'lodash'
|
|
||||||
/* eslint-disable
|
|
||||||
max-len,
|
|
||||||
no-return-assign,
|
|
||||||
*/
|
|
||||||
// TODO: This file was created by bulk-decaffeinate.
|
|
||||||
// Fix any style issues and re-enable lint.
|
|
||||||
/*
|
|
||||||
* decaffeinate suggestions:
|
|
||||||
* DS101: Remove unnecessary use of Array.from
|
|
||||||
* DS102: Remove unnecessary code created because of implicit returns
|
|
||||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
||||||
*/
|
|
||||||
import App from '../base'
|
|
||||||
|
|
||||||
export default App.controller(
|
|
||||||
'RegisterUsersController',
|
|
||||||
function ($scope, queuedHttp) {
|
|
||||||
$scope.users = []
|
|
||||||
|
|
||||||
$scope.inputs = { emails: '' }
|
|
||||||
|
|
||||||
const parseEmails = function (emailsString) {
|
|
||||||
const regexBySpaceOrComma = /[\s,]+/
|
|
||||||
let emails = emailsString.split(regexBySpaceOrComma)
|
|
||||||
emails = _.map(emails, email => (email = email.trim()))
|
|
||||||
emails = _.filter(emails, email => email.indexOf('@') !== -1)
|
|
||||||
return emails
|
|
||||||
}
|
|
||||||
|
|
||||||
return ($scope.registerUsers = function () {
|
|
||||||
const emails = parseEmails($scope.inputs.emails)
|
|
||||||
$scope.error = false
|
|
||||||
return Array.from(emails).map(email =>
|
|
||||||
queuedHttp
|
|
||||||
.post('/admin/register', {
|
|
||||||
email,
|
|
||||||
_csrf: window.csrfToken,
|
|
||||||
})
|
|
||||||
.then(function (response) {
|
|
||||||
const { data } = response
|
|
||||||
const user = data
|
|
||||||
$scope.users.push(user)
|
|
||||||
return ($scope.inputs.emails = '')
|
|
||||||
})
|
|
||||||
.catch(() => ($scope.error = true))
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
|
@ -1,40 +1,11 @@
|
||||||
extends ../../../../../app/views/layout
|
extends ../../../../../app/views/layout
|
||||||
|
|
||||||
|
block entrypointVar
|
||||||
|
- entrypoint = 'modules/user-activate/pages/user-activate-page'
|
||||||
|
|
||||||
|
|
||||||
block content
|
block content
|
||||||
.content.content-alt
|
.content.content-alt
|
||||||
.container
|
.container
|
||||||
.row
|
#user-activate-register-container
|
||||||
.col-md-12
|
|
||||||
.card(ng-controller="RegisterUsersController")
|
|
||||||
.page-header
|
|
||||||
h1 Register New Users
|
|
||||||
form.form
|
|
||||||
.row
|
|
||||||
.col-md-4.col-xs-8
|
|
||||||
input.form-control(
|
|
||||||
name="email",
|
|
||||||
type="text",
|
|
||||||
placeholder="jane@example.com, joe@example.com",
|
|
||||||
ng-model="inputs.emails",
|
|
||||||
on-enter="registerUsers()"
|
|
||||||
)
|
|
||||||
.col-md-8.col-xs-4
|
|
||||||
button.btn.btn-primary(ng-click="registerUsers()") #{translate("register")}
|
|
||||||
|
|
||||||
.row-spaced(ng-show="error").ng-cloak.text-danger
|
|
||||||
p Sorry, an error occured
|
|
||||||
|
|
||||||
.row-spaced(ng-show="users.length > 0").ng-cloak.text-success
|
|
||||||
p We've sent out welcome emails to the registered users.
|
|
||||||
p You can also manually send them URLs below to allow them to reset their password and log in for the first time.
|
|
||||||
p (Password reset tokens will expire after one week and the user will need registering again).
|
|
||||||
|
|
||||||
hr(ng-show="users.length > 0").ng-cloak
|
|
||||||
table(ng-show="users.length > 0").table.table-striped.ng-cloak
|
|
||||||
tr
|
|
||||||
th #{translate("email")}
|
|
||||||
th Set Password Url
|
|
||||||
tr(ng-repeat="user in users")
|
|
||||||
td {{ user.email }}
|
|
||||||
td(style="word-break: break-all;") {{ user.setNewPasswordUrl }}
|
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { postJSON } from '../../../../../frontend/js/infrastructure/fetch-json'
|
||||||
|
|
||||||
|
function RegisterForm({
|
||||||
|
setRegistrationSuccess,
|
||||||
|
setEmails,
|
||||||
|
setRegisterError,
|
||||||
|
setFailedEmails,
|
||||||
|
}) {
|
||||||
|
function handleRegister(event) {
|
||||||
|
event.preventDefault()
|
||||||
|
const formData = new FormData(event.target)
|
||||||
|
const formDataAsEntries = formData.entries()
|
||||||
|
const formDataAsObject = Object.fromEntries(formDataAsEntries)
|
||||||
|
const emailString = formDataAsObject.email
|
||||||
|
setRegistrationSuccess(false)
|
||||||
|
setRegisterError(false)
|
||||||
|
setEmails([])
|
||||||
|
registerGivenUsers(parseEmails(emailString))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function registerGivenUsers(emails) {
|
||||||
|
const registeredEmails = []
|
||||||
|
const failingEmails = []
|
||||||
|
for (const email of emails) {
|
||||||
|
try {
|
||||||
|
const result = await registerUser(email)
|
||||||
|
registeredEmails.push(result)
|
||||||
|
} catch {
|
||||||
|
failingEmails.push(email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (registeredEmails.length > 0) setRegistrationSuccess(true)
|
||||||
|
if (failingEmails.length > 0) {
|
||||||
|
setRegisterError(true)
|
||||||
|
setFailedEmails(failingEmails)
|
||||||
|
}
|
||||||
|
setEmails(registeredEmails)
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerUser(email) {
|
||||||
|
const options = { email }
|
||||||
|
const url = `/admin/register`
|
||||||
|
return postJSON(url, { body: options })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleRegister}>
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-md-4 col-xs-8">
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
name="email"
|
||||||
|
type="text"
|
||||||
|
placeholder="jane@example.com, joe@example.com"
|
||||||
|
aria-label="emails to register"
|
||||||
|
aria-describedby="input-details"
|
||||||
|
/>
|
||||||
|
<p id="input-details" className="sr-only">
|
||||||
|
Enter the emails you would like to register and separate them using
|
||||||
|
commas
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-8 col-xs-4">
|
||||||
|
<button className="btn btn-primary">Register</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEmails(emailsText) {
|
||||||
|
const regexBySpaceOrComma = /[\s,]+/
|
||||||
|
let emails = emailsText.split(regexBySpaceOrComma)
|
||||||
|
emails.map(email => email.trim())
|
||||||
|
emails = emails.filter(email => email.indexOf('@') !== -1)
|
||||||
|
return emails
|
||||||
|
}
|
||||||
|
|
||||||
|
RegisterForm.propTypes = {
|
||||||
|
setRegistrationSuccess: PropTypes.func,
|
||||||
|
setEmails: PropTypes.func,
|
||||||
|
setRegisterError: PropTypes.func,
|
||||||
|
setFailedEmails: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RegisterForm
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import RegisterForm from './register-form'
|
||||||
|
function UserActivateRegister() {
|
||||||
|
const [emails, setEmails] = useState([])
|
||||||
|
const [failedEmails, setFailedEmails] = useState([])
|
||||||
|
const [registerError, setRegisterError] = useState(false)
|
||||||
|
const [registrationSuccess, setRegistrationSuccess] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-md-12">
|
||||||
|
<div className="card">
|
||||||
|
<div className="page-header">
|
||||||
|
<h1> Register New Users</h1>
|
||||||
|
</div>
|
||||||
|
<RegisterForm
|
||||||
|
setRegistrationSuccess={setRegistrationSuccess}
|
||||||
|
setEmails={setEmails}
|
||||||
|
setRegisterError={setRegisterError}
|
||||||
|
setFailedEmails={setFailedEmails}
|
||||||
|
/>
|
||||||
|
{registerError ? (
|
||||||
|
<UserActivateError failedEmails={failedEmails} />
|
||||||
|
) : null}
|
||||||
|
{registrationSuccess ? (
|
||||||
|
<>
|
||||||
|
<SuccessfulRegistrationMessage />
|
||||||
|
<hr />
|
||||||
|
<DisplayEmailsList emails={emails} />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserActivateError({ failedEmails }) {
|
||||||
|
return (
|
||||||
|
<div className="row-spaced text-danger">
|
||||||
|
<p>Sorry, an error occured, failed to register these emails.</p>
|
||||||
|
{failedEmails.map(email => (
|
||||||
|
<p key={email}>{email}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SuccessfulRegistrationMessage() {
|
||||||
|
return (
|
||||||
|
<div className="row-spaced text-success">
|
||||||
|
<p>We've sent out welcome emails to the registered users.</p>
|
||||||
|
<p>
|
||||||
|
You can also manually send them URLs below to allow them to reset their
|
||||||
|
password and log in for the first time.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
(Password reset tokens will expire after one week and the user will need
|
||||||
|
registering again).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DisplayEmailsList({ emails }) {
|
||||||
|
return (
|
||||||
|
<table className="table table-striped ">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Set Password Url</th>
|
||||||
|
</tr>
|
||||||
|
{emails.map(user => (
|
||||||
|
<tr key={user.email}>
|
||||||
|
<td>{user.email}</td>
|
||||||
|
<td style={{ wordBreak: 'break-all' }}>{user.setNewPasswordUrl}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DisplayEmailsList.propTypes = {
|
||||||
|
emails: PropTypes.array,
|
||||||
|
}
|
||||||
|
UserActivateError.propTypes = {
|
||||||
|
failedEmails: PropTypes.array,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserActivateRegister
|
|
@ -0,0 +1,9 @@
|
||||||
|
import '../../../../../frontend/js/marketing'
|
||||||
|
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import UserActivateRegister from '../components/user-activate-register'
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<UserActivateRegister />,
|
||||||
|
document.getElementById('user-activate-register-container')
|
||||||
|
)
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react'
|
||||||
|
import fetchMock from 'fetch-mock'
|
||||||
|
import sinon from 'sinon'
|
||||||
|
import RegisterForm from '../../../../frontend/js/components/register-form'
|
||||||
|
|
||||||
|
describe('RegisterForm', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
fetchMock.reset()
|
||||||
|
})
|
||||||
|
afterEach(function () {
|
||||||
|
fetchMock.reset()
|
||||||
|
})
|
||||||
|
it('should render the register form', async function () {
|
||||||
|
const setRegistrationSuccessStub = sinon.stub()
|
||||||
|
const setEmailsStub = sinon.stub()
|
||||||
|
const setRegisterErrorStub = sinon.stub()
|
||||||
|
const setFailedEmailsStub = sinon.stub()
|
||||||
|
|
||||||
|
render(
|
||||||
|
<RegisterForm
|
||||||
|
setRegistrationSuccess={setRegistrationSuccessStub}
|
||||||
|
setEmails={setEmailsStub}
|
||||||
|
setRegisterError={setRegisterErrorStub}
|
||||||
|
setFailedEmails={setFailedEmailsStub}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
await screen.findByLabelText('emails to register')
|
||||||
|
screen.getByRole('button', { name: /register/i })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should call the fetch request when register button is pressed', async function () {
|
||||||
|
const email = 'abc@gmail.com'
|
||||||
|
const setRegistrationSuccessStub = sinon.stub()
|
||||||
|
const setEmailsStub = sinon.stub()
|
||||||
|
const setRegisterErrorStub = sinon.stub()
|
||||||
|
const setFailedEmailsStub = sinon.stub()
|
||||||
|
|
||||||
|
const endPointResponse = {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
email,
|
||||||
|
setNewPasswordUrl: 'SetNewPasswordURL',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const registerMock = fetchMock.post('/admin/register', endPointResponse)
|
||||||
|
|
||||||
|
render(
|
||||||
|
<RegisterForm
|
||||||
|
setRegistrationSuccess={setRegistrationSuccessStub}
|
||||||
|
setEmails={setEmailsStub}
|
||||||
|
setRegisterError={setRegisterErrorStub}
|
||||||
|
setFailedEmails={setFailedEmailsStub}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
const registerInput = screen.getByLabelText('emails to register')
|
||||||
|
const registerButton = screen.getByRole('button', { name: /register/i })
|
||||||
|
fireEvent.change(registerInput, { target: { value: email } })
|
||||||
|
fireEvent.click(registerButton)
|
||||||
|
expect(registerMock.called()).to.be.true
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,139 @@
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react'
|
||||||
|
import fetchMock from 'fetch-mock'
|
||||||
|
import UserActivateRegister from '../../../../frontend/js/components/user-activate-register'
|
||||||
|
|
||||||
|
describe('UserActivateRegister', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
fetchMock.reset()
|
||||||
|
})
|
||||||
|
afterEach(function () {
|
||||||
|
fetchMock.reset()
|
||||||
|
})
|
||||||
|
it('should display the error message', async function () {
|
||||||
|
const email = 'abc@gmail.com'
|
||||||
|
render(<UserActivateRegister />)
|
||||||
|
const endPointResponse = {
|
||||||
|
status: 500,
|
||||||
|
}
|
||||||
|
const registerMock = fetchMock.post('/admin/register', endPointResponse)
|
||||||
|
const registerInput = screen.getByLabelText('emails to register')
|
||||||
|
const registerButton = screen.getByRole('button', { name: /register/i })
|
||||||
|
|
||||||
|
fireEvent.change(registerInput, { target: { value: email } })
|
||||||
|
fireEvent.click(registerButton)
|
||||||
|
|
||||||
|
expect(registerMock.called()).to.be.true
|
||||||
|
await screen.findByText('Sorry, an error occured', { exact: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display the success message', async function () {
|
||||||
|
const email = 'abc@gmail.com'
|
||||||
|
render(<UserActivateRegister />)
|
||||||
|
const endPointResponse = {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
email,
|
||||||
|
setNewPasswordUrl: 'SetNewPasswordURL',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const registerMock = fetchMock.post('/admin/register', endPointResponse)
|
||||||
|
const registerInput = screen.getByLabelText('emails to register')
|
||||||
|
const registerButton = screen.getByRole('button', { name: /register/i })
|
||||||
|
|
||||||
|
fireEvent.change(registerInput, { target: { value: email } })
|
||||||
|
fireEvent.click(registerButton)
|
||||||
|
|
||||||
|
expect(registerMock.called()).to.be.true
|
||||||
|
await screen.findByText(
|
||||||
|
"We've sent out welcome emails to the registered users."
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display the registered emails', async function () {
|
||||||
|
const email = 'abc@gmail.com, def@gmail.com'
|
||||||
|
render(<UserActivateRegister />)
|
||||||
|
const endPointResponse1 = {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
email: 'abc@gmail.com',
|
||||||
|
setNewPasswordUrl: 'SetNewPasswordURL',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const endPointResponse2 = {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
email: 'def@gmail.com',
|
||||||
|
setNewPasswordUrl: 'SetNewPasswordURL',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const registerMock = fetchMock.post('/admin/register', (path, req) => {
|
||||||
|
const body = JSON.parse(req.body)
|
||||||
|
if (body.email === 'abc@gmail.com') return endPointResponse1
|
||||||
|
else if (body.email === 'def@gmail.com') return endPointResponse2
|
||||||
|
})
|
||||||
|
const registerInput = screen.getByLabelText('emails to register')
|
||||||
|
const registerButton = screen.getByRole('button', { name: /register/i })
|
||||||
|
|
||||||
|
fireEvent.change(registerInput, { target: { value: email } })
|
||||||
|
fireEvent.click(registerButton)
|
||||||
|
|
||||||
|
expect(registerMock.called()).to.be.true
|
||||||
|
await screen.findByText('abc@gmail.com')
|
||||||
|
await screen.findByText('def@gmail.com')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display the failed emails', async function () {
|
||||||
|
const email = 'abc@, def@'
|
||||||
|
render(<UserActivateRegister />)
|
||||||
|
const endPointResponse1 = {
|
||||||
|
status: 500,
|
||||||
|
}
|
||||||
|
const endPointResponse2 = {
|
||||||
|
status: 500,
|
||||||
|
}
|
||||||
|
const registerMock = fetchMock.post('/admin/register', (path, req) => {
|
||||||
|
const body = JSON.parse(req.body)
|
||||||
|
if (body.email === 'abc@') return endPointResponse1
|
||||||
|
else if (body.email === 'def@') return endPointResponse2
|
||||||
|
})
|
||||||
|
const registerInput = screen.getByLabelText('emails to register')
|
||||||
|
const registerButton = screen.getByRole('button', { name: /register/i })
|
||||||
|
|
||||||
|
fireEvent.change(registerInput, { target: { value: email } })
|
||||||
|
fireEvent.click(registerButton)
|
||||||
|
|
||||||
|
expect(registerMock.called()).to.be.true
|
||||||
|
await screen.findByText('abc@')
|
||||||
|
await screen.findByText('def@')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display the registered and failed emails together', async function () {
|
||||||
|
const email = 'abc@gmail.com, def@'
|
||||||
|
render(<UserActivateRegister />)
|
||||||
|
const endPointResponse1 = {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
email: 'abc@gmail.com',
|
||||||
|
setNewPasswordUrl: 'SetNewPasswordURL',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const endPointResponse2 = {
|
||||||
|
status: 500,
|
||||||
|
}
|
||||||
|
const registerMock = fetchMock.post('/admin/register', (path, req) => {
|
||||||
|
const body = JSON.parse(req.body)
|
||||||
|
if (body.email === 'abc@gmail.com') return endPointResponse1
|
||||||
|
else if (body.email === 'def@gmail.com') return endPointResponse2
|
||||||
|
})
|
||||||
|
const registerInput = screen.getByLabelText('emails to register')
|
||||||
|
const registerButton = screen.getByRole('button', { name: /register/i })
|
||||||
|
|
||||||
|
fireEvent.change(registerInput, { target: { value: email } })
|
||||||
|
fireEvent.click(registerButton)
|
||||||
|
|
||||||
|
expect(registerMock.called()).to.be.true
|
||||||
|
await screen.findByText('abc@gmail.com')
|
||||||
|
await screen.findByText('def@')
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in a new issue