mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-07-16 19:30:13 -04:00
Merge pull request #219 from alphagov/accept-invite-part-2
[WIP] New user can now accept invite and will be made to
This commit is contained in:
@@ -8,7 +8,8 @@ from wtforms import (
|
||||
TextAreaField,
|
||||
FileField,
|
||||
RadioField,
|
||||
BooleanField
|
||||
BooleanField,
|
||||
HiddenField
|
||||
)
|
||||
from wtforms.fields.html5 import EmailField, TelField
|
||||
from wtforms.validators import DataRequired, Email, Length, Regexp
|
||||
@@ -97,6 +98,15 @@ class RegisterUserForm(Form):
|
||||
password = password()
|
||||
|
||||
|
||||
class RegisterUserFromInviteForm(Form):
|
||||
name = StringField('Full name',
|
||||
validators=[DataRequired(message='Name can not be empty')])
|
||||
mobile_number = mobile_number()
|
||||
password = password()
|
||||
service = HiddenField('service')
|
||||
email_address = HiddenField('email_address')
|
||||
|
||||
|
||||
class InviteUserForm(Form):
|
||||
email_address = email_address('Their email address')
|
||||
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
import re
|
||||
from flask import (
|
||||
render_template,
|
||||
redirect,
|
||||
session,
|
||||
url_for
|
||||
)
|
||||
|
||||
from flask_login import login_required
|
||||
|
||||
from flask import render_template, request, redirect, session, url_for
|
||||
from flask_login import login_required, current_user
|
||||
from app.main import main
|
||||
from app.main.dao import services_dao, users_dao
|
||||
from app.main.forms import AddServiceForm
|
||||
from app import user_api_client
|
||||
|
||||
|
||||
@main.route("/add-service", methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def add_service():
|
||||
|
||||
invited_user = session.get('invited_user')
|
||||
if invited_user:
|
||||
# if invited user add to service and redirect to dashboard
|
||||
user = users_dao.get_user_by_id(session['user_id'])
|
||||
service_id = invited_user['service']
|
||||
user_api_client.add_user_to_service(service_id, user.id)
|
||||
session.pop('invited_user', None)
|
||||
return redirect(url_for('main.service_dashboard', service_id=service_id))
|
||||
|
||||
form = AddServiceForm(services_dao.find_all_service_names)
|
||||
heading = 'Which service do you want to set up notifications for?'
|
||||
if form.validate_on_submit():
|
||||
|
||||
@@ -12,12 +12,6 @@ def index():
|
||||
return render_template('views/signedout.html')
|
||||
|
||||
|
||||
@main.route("/register-from-invite")
|
||||
@login_required
|
||||
def register_from_invite():
|
||||
return render_template('views/register-from-invite.html')
|
||||
|
||||
|
||||
@main.route("/verify-mobile")
|
||||
@login_required
|
||||
def verify_mobile():
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from flask import (
|
||||
redirect,
|
||||
url_for,
|
||||
abort
|
||||
abort,
|
||||
session
|
||||
)
|
||||
|
||||
from notifications_python_client.errors import HTTPError
|
||||
@@ -15,6 +16,7 @@ from app import (
|
||||
|
||||
@main.route("/invitation/<token>")
|
||||
def accept_invite(token):
|
||||
|
||||
try:
|
||||
invited_user = invite_api_client.accept_invite(token)
|
||||
existing_user = user_api_client.get_user_by_email(invited_user.email_address)
|
||||
@@ -22,11 +24,11 @@ def accept_invite(token):
|
||||
if existing_user:
|
||||
user_api_client.add_user_to_service(invited_user.service,
|
||||
existing_user.id)
|
||||
|
||||
return redirect(url_for('main.service_dashboard', service_id=invited_user.service))
|
||||
else:
|
||||
# TODO implement registration flow for new users
|
||||
abort(404)
|
||||
session['invited_user'] = invited_user.serialize()
|
||||
return redirect(url_for('main.register_from_invite'))
|
||||
|
||||
except HTTPError as e:
|
||||
if e.status_code == 404:
|
||||
abort(404)
|
||||
|
||||
@@ -15,7 +15,10 @@ from notifications_python_client.errors import HTTPError
|
||||
|
||||
from app.main import main
|
||||
from app.main.dao import users_dao
|
||||
from app.main.forms import RegisterUserForm
|
||||
from app.main.forms import (
|
||||
RegisterUserForm,
|
||||
RegisterUserFromInviteForm
|
||||
)
|
||||
|
||||
from app import user_api_client
|
||||
|
||||
@@ -27,29 +30,53 @@ def register():
|
||||
|
||||
form = RegisterUserForm()
|
||||
if form.validate_on_submit():
|
||||
if users_dao.is_email_unique(form.email_address.data):
|
||||
try:
|
||||
user = user_api_client.register_user(form.name.data,
|
||||
form.email_address.data,
|
||||
form.mobile_number.data,
|
||||
form.password.data)
|
||||
except HTTPError as e:
|
||||
if e.status_code == 404:
|
||||
abort(404)
|
||||
else:
|
||||
raise e
|
||||
return _do_registration(form)
|
||||
else:
|
||||
return render_template('views/register.html', form=form)
|
||||
|
||||
# TODO possibly there should be some exception handling
|
||||
# for sending sms and email codes.
|
||||
# How do we report to the user there is a problem with
|
||||
# sending codes apart from service unavailable?
|
||||
# at the moment i believe http 500 is fine.
|
||||
users_dao.send_verify_code(user.id, 'sms', user.mobile_number)
|
||||
users_dao.send_verify_code(user.id, 'email', user.email_address)
|
||||
session['expiry_date'] = str(datetime.now() + timedelta(hours=1))
|
||||
session['user_details'] = {"email": user.email_address, "id": user.id}
|
||||
return redirect(url_for('main.verify'))
|
||||
else:
|
||||
flash('There was an error registering your account')
|
||||
|
||||
return render_template('views/register.html', form=form)
|
||||
@main.route('/register-from-invite', methods=['GET', 'POST'])
|
||||
def register_from_invite():
|
||||
|
||||
form = RegisterUserFromInviteForm()
|
||||
invited_user = session.get('invited_user')
|
||||
if not invited_user:
|
||||
abort(404)
|
||||
|
||||
if form.validate_on_submit():
|
||||
if form.service.data != invited_user['service'] or form.email_address.data != invited_user['email_address']:
|
||||
abort(400)
|
||||
return _do_registration(form)
|
||||
|
||||
form.service.data = invited_user['service']
|
||||
form.email_address.data = invited_user['email_address']
|
||||
|
||||
return render_template('views/register-from-invite.html', form=form)
|
||||
|
||||
|
||||
def _do_registration(form, service=None):
|
||||
if users_dao.is_email_unique(form.email_address.data):
|
||||
try:
|
||||
user = user_api_client.register_user(form.name.data,
|
||||
form.email_address.data,
|
||||
form.mobile_number.data,
|
||||
form.password.data)
|
||||
|
||||
except HTTPError as e:
|
||||
if e.status_code == 404:
|
||||
abort(404)
|
||||
else:
|
||||
raise e
|
||||
|
||||
# TODO possibly there should be some exception handling
|
||||
# for sending sms and email codes.
|
||||
# How do we report to the user there is a problem with
|
||||
# sending codes apart from service unavailable?
|
||||
# at the moment i believe http 500 is fine.
|
||||
users_dao.send_verify_code(user.id, 'sms', user.mobile_number)
|
||||
users_dao.send_verify_code(user.id, 'email', user.email_address)
|
||||
session['expiry_date'] = str(datetime.now() + timedelta(hours=1))
|
||||
session['user_details'] = {"email": user.email_address, "id": user.id}
|
||||
return redirect(url_for('main.verify'))
|
||||
else:
|
||||
flash('There was an error registering your account')
|
||||
|
||||
@@ -17,6 +17,7 @@ from app.main.forms import VerifyForm
|
||||
|
||||
@main.route('/verify', methods=['GET', 'POST'])
|
||||
def verify():
|
||||
|
||||
# TODO there needs to be a way to regenerate a session id
|
||||
# or handle gracefully.
|
||||
user_id = session['user_details']['id']
|
||||
|
||||
@@ -125,3 +125,12 @@ class InvitedUser(object):
|
||||
|
||||
def has_permissions(self, permission):
|
||||
return permission in self.permissions
|
||||
|
||||
def serialize(self):
|
||||
return {'id': self.id,
|
||||
'service': self.service,
|
||||
'from_user': self.from_user,
|
||||
'email_address': self.email_address,
|
||||
'permissions': self.permissions,
|
||||
'status': self.status
|
||||
}
|
||||
|
||||
@@ -34,8 +34,14 @@ class UserApiClient(BaseAPIClient):
|
||||
return User(user_data['data'], max_failed_login_count=self.max_failed_login_count)
|
||||
|
||||
def get_user_by_email(self, email_address):
|
||||
params = {'email': email_address}
|
||||
user_data = self.get('/user/email', params=params)
|
||||
try:
|
||||
params = {'email': email_address}
|
||||
user_data = self.get('/user/email', params=params)
|
||||
except HTTPError as e:
|
||||
if e.status_code == 404:
|
||||
return None
|
||||
else:
|
||||
raise e
|
||||
return User(user_data['data'], max_failed_login_count=self.max_failed_login_count)
|
||||
|
||||
def get_users(self):
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{% extends "withoutnav_template.html" %}
|
||||
{% from "components/textbox.html" import textbox %}
|
||||
{% from "components/page-footer.html" import page_footer %}
|
||||
|
||||
{% block page_title %}
|
||||
Create an account – GOV.UK Notify
|
||||
@@ -9,26 +11,14 @@ Create an account – GOV.UK Notify
|
||||
<div class="grid-row">
|
||||
<div class="column-two-thirds">
|
||||
<h1 class="heading-large">Create an account</h1>
|
||||
|
||||
<p>If you've used GOV.UK Notify before, <a href="">sign in to your account</a>.</p>
|
||||
|
||||
<p>
|
||||
<label class="form-label" for="name-f1">Full name</label>
|
||||
<input class="form-control-2-3" id="name-f1" type="text">
|
||||
</p>
|
||||
<p>
|
||||
<label class="form-label" for="mobile">Mobile phone number</label>
|
||||
<input class="form-control-1-4" id="mobile" type="text">
|
||||
</p>
|
||||
<p>
|
||||
<label class="form-label" for="password">Create a password</label>
|
||||
<input class="form-control-1-4" id="password" type="password"><br>
|
||||
<span class="font-xsmall">Your password must have at least 10 characters</span></label>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a class="button" href="verify-mobile" role="button">Continue</a>
|
||||
</p>
|
||||
<form method="post" autocomplete="nope">
|
||||
{{ textbox(form.name, width='3-4') }}
|
||||
{{ textbox(form.mobile_number, width='3-4') }}
|
||||
{{ textbox(form.password, hint="Your password must have at least 10 characters", width='3-4') }}
|
||||
{{ page_footer("Continue") }}
|
||||
{{form.service}}
|
||||
{{form.email_address}}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
from flask import url_for
|
||||
|
||||
from app.notify_client.models import InvitedUser
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
import pytest
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
def test_existing_user_accept_invite_calls_api_and_redirects_to_dashboard(app_,
|
||||
@@ -28,3 +25,142 @@ def test_existing_user_accept_invite_calls_api_and_redirects_to_dashboard(app_,
|
||||
|
||||
assert response.status_code == 302
|
||||
assert response.location == expected_redirect_location
|
||||
|
||||
|
||||
def test_existing_signed_out_user_accept_invite_redirects_to_sign_in(app_,
|
||||
service_one,
|
||||
api_user_active,
|
||||
sample_invite,
|
||||
mock_accept_invite,
|
||||
mock_get_user_by_email,
|
||||
mock_add_user_to_service):
|
||||
|
||||
expected_service = service_one['id']
|
||||
expected_redirect_location = 'http://localhost/services/{}/dashboard'.format(expected_service)
|
||||
|
||||
with app_.test_request_context():
|
||||
with app_.test_client() as client:
|
||||
|
||||
response = client.get(url_for('main.accept_invite', token='thisisnotarealtoken'), follow_redirects=True)
|
||||
|
||||
mock_accept_invite.assert_called_with('thisisnotarealtoken')
|
||||
mock_get_user_by_email.assert_called_with('invited_user@test.gov.uk')
|
||||
mock_add_user_to_service.assert_called_with(expected_service, api_user_active.id)
|
||||
|
||||
assert response.status_code == 200
|
||||
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
|
||||
assert page.h1.string.strip() == 'Sign in'
|
||||
|
||||
|
||||
def test_new_user_accept_invite_calls_api_and_redirects_to_registration(app_,
|
||||
service_one,
|
||||
sample_invite,
|
||||
mock_accept_invite,
|
||||
mock_dont_get_user_by_email,
|
||||
mock_add_user_to_service):
|
||||
|
||||
expected_service = service_one['id']
|
||||
expected_redirect_location = 'http://localhost/register-from-invite'
|
||||
|
||||
with app_.test_request_context():
|
||||
with app_.test_client() as client:
|
||||
|
||||
response = client.get(url_for('main.accept_invite', token='thisisnotarealtoken'))
|
||||
|
||||
mock_accept_invite.assert_called_with('thisisnotarealtoken')
|
||||
mock_dont_get_user_by_email.assert_called_with('invited_user@test.gov.uk')
|
||||
|
||||
assert response.status_code == 302
|
||||
assert response.location == expected_redirect_location
|
||||
|
||||
|
||||
def test_new_user_accept_invite_completes_new_registration_redirects_to_verify(app_,
|
||||
service_one,
|
||||
sample_invite,
|
||||
mock_accept_invite,
|
||||
mock_dont_get_user_by_email,
|
||||
mock_register_user,
|
||||
mock_send_verify_code,
|
||||
mock_add_user_to_service):
|
||||
|
||||
expected_service = service_one['id']
|
||||
expected_email = sample_invite['email_address']
|
||||
expected_from_user = service_one['users'][0]
|
||||
expected_redirect_location = 'http://localhost/register-from-invite'
|
||||
|
||||
with app_.test_request_context():
|
||||
with app_.test_client() as client:
|
||||
response = client.get(url_for('main.accept_invite', token='thisisnotarealtoken'))
|
||||
with client.session_transaction() as session:
|
||||
assert response.status_code == 302
|
||||
assert response.location == expected_redirect_location
|
||||
invited_user = session.get('invited_user')
|
||||
assert invited_user
|
||||
assert expected_service == invited_user['service']
|
||||
assert expected_email == invited_user['email_address']
|
||||
assert expected_from_user == invited_user['from_user']
|
||||
|
||||
data = {'service': invited_user['service'],
|
||||
'email_address': invited_user['email_address'],
|
||||
'from_user': invited_user['from_user'],
|
||||
'password': 'longpassword',
|
||||
'mobile_number': '+447890123456',
|
||||
'name': 'Invited User'
|
||||
}
|
||||
|
||||
expected_redirect_location = 'http://localhost/verify'
|
||||
response = client.post(url_for('main.register_from_invite'), data=data)
|
||||
assert response.status_code == 302
|
||||
assert response.location == expected_redirect_location
|
||||
|
||||
mock_register_user.assert_called_with(data['name'],
|
||||
data['email_address'],
|
||||
data['mobile_number'],
|
||||
data['password'])
|
||||
|
||||
|
||||
def test_new_invited_user_verifies_and_added_to_service(app_,
|
||||
service_one,
|
||||
sample_invite,
|
||||
mock_accept_invite,
|
||||
mock_dont_get_user_by_email,
|
||||
mock_register_user,
|
||||
mock_send_verify_code,
|
||||
mock_check_verify_code,
|
||||
mock_get_user,
|
||||
mock_update_user,
|
||||
mock_add_user_to_service,
|
||||
mock_get_service,
|
||||
mock_get_service_templates,
|
||||
mock_get_jobs):
|
||||
|
||||
with app_.test_request_context():
|
||||
with app_.test_client() as client:
|
||||
# visit accept token page
|
||||
response = client.get(url_for('main.accept_invite', token='thisisnotarealtoken'))
|
||||
data = {'service': sample_invite['service'],
|
||||
'email_address': sample_invite['email_address'],
|
||||
'from_user': sample_invite['from_user'],
|
||||
'password': 'longpassword',
|
||||
'mobile_number': '+447890123456',
|
||||
'name': 'Invited User'
|
||||
}
|
||||
|
||||
# get redirected to register from invite
|
||||
response = client.post(url_for('main.register_from_invite'), data=data)
|
||||
|
||||
# that sends user on to verify
|
||||
response = client.post(url_for('main.verify'), data={'sms_code': '12345', 'email_code': '23456'},
|
||||
follow_redirects=True)
|
||||
|
||||
# when they post codes back to admin user should be added to
|
||||
# service and sent on to dash board
|
||||
with client.session_transaction() as session:
|
||||
new_user_id = session['user_id']
|
||||
mock_add_user_to_service.assert_called_with(data['service'], new_user_id)
|
||||
|
||||
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
|
||||
element = page.find('h2', class_='navigation-service-name').find('a')
|
||||
assert element.text == 'Test Service'
|
||||
service_link = element.attrs['href']
|
||||
assert service_link == '/services/{}/dashboard'.format(service_one['id'])
|
||||
|
||||
@@ -252,7 +252,6 @@ def test_should_redirect_after_mobile_number_confirm(app_,
|
||||
response = client.post(
|
||||
url_for('main.user_profile_mobile_number_confirm'),
|
||||
data=data)
|
||||
print(response.get_data(as_text=True))
|
||||
assert response.status_code == 302
|
||||
assert response.location == url_for(
|
||||
'main.user_profile', _external=True)
|
||||
@@ -287,7 +286,6 @@ def test_should_redirect_after_password_change(app_,
|
||||
url_for('main.user_profile_password'),
|
||||
data=data)
|
||||
|
||||
print(response.get_data(as_text=True))
|
||||
assert response.status_code == 302
|
||||
assert response.location == url_for(
|
||||
'main.user_profile', _external=True)
|
||||
|
||||
@@ -572,7 +572,7 @@ def sample_invite(mocker, service_one):
|
||||
email_address = 'invited_user@test.gov.uk'
|
||||
service_id = service_one['id']
|
||||
permissions = 'send_messages,manage_service,manage_api_keys'
|
||||
created_at = datetime.datetime.now()
|
||||
created_at = str(datetime.datetime.now())
|
||||
return invite_json(id, from_user, service_id, email_address, permissions, created_at)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user