mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-19 14:09:20 -04:00
Merge branch 'master' of https://github.com/alphagov/notifications-admin into vb-free-sms-history
This commit is contained in:
@@ -43,6 +43,7 @@ class Config(object):
|
||||
}
|
||||
EMAIL_EXPIRY_SECONDS = 3600 # 1 hour
|
||||
INVITATION_EXPIRY_SECONDS = 3600 * 24 * 2 # 2 days - also set on api
|
||||
EMAIL_2FA_EXPIRY_SECONDS = 1800 # 30 Minutes
|
||||
HEADER_COLOUR = '#FFBF47' # $yellow
|
||||
HTTP_PROTOCOL = 'http'
|
||||
MAX_FAILED_LOGIN_COUNT = 10
|
||||
|
||||
@@ -31,8 +31,7 @@ def check_and_resend_text_code():
|
||||
form = TextNotReceivedForm(mobile_number=user.mobile_number)
|
||||
if form.validate_on_submit():
|
||||
user_api_client.send_verify_code(user.id, 'sms', to=form.mobile_number.data)
|
||||
user.mobile_number = form.mobile_number.data
|
||||
user_api_client.update_user(user)
|
||||
user = user_api_client.update_user_attribute(user.id, mobile_number=form.mobile_number.data)
|
||||
return redirect(url_for('.verify'))
|
||||
|
||||
return render_template('views/text-not-received.html', form=form)
|
||||
@@ -47,3 +46,17 @@ def check_and_resend_verification_code():
|
||||
return redirect(url_for('main.verify'))
|
||||
else:
|
||||
return redirect(url_for('main.two_factor'))
|
||||
|
||||
|
||||
@main.route('/email-not-received', methods=['GET'])
|
||||
@redirect_to_sign_in
|
||||
def email_not_received():
|
||||
return render_template('views/email-not-received.html')
|
||||
|
||||
|
||||
@main.route('/send-new-email-token', methods=['GET'])
|
||||
@redirect_to_sign_in
|
||||
def resend_email_link():
|
||||
user_api_client.send_verify_code(session['user_details']['id'], 'email', None)
|
||||
session.pop('user_details')
|
||||
return redirect(url_for('main.two_factor_email_sent', email_resent=True))
|
||||
|
||||
@@ -217,18 +217,18 @@ def set_sender(service_id, template_id):
|
||||
def get_sender_context(sender_details, template_type):
|
||||
context = {
|
||||
'email': {
|
||||
'title': "Choose where to send replies",
|
||||
'description': "Select an email address that recipients can reply to",
|
||||
'title': 'Send to one recipient',
|
||||
'description': 'Where should replies go?',
|
||||
'field_name': 'email_address'
|
||||
},
|
||||
'letter': {
|
||||
'title': 'Choose sender address',
|
||||
'description': 'Select an address that recipients can reply to',
|
||||
'title': 'Send to one recipient',
|
||||
'description': 'What should appear in the top right of the letter?',
|
||||
'field_name': 'contact_block'
|
||||
},
|
||||
'sms': {
|
||||
'title': 'Chose text message sender',
|
||||
'description': 'Select a text message sender that the recipients can reply to',
|
||||
'title': 'Send to one recipient',
|
||||
'description': 'Who should the message come from?',
|
||||
'field_name': 'sms_sender'
|
||||
}
|
||||
}[template_type]
|
||||
@@ -410,7 +410,8 @@ def send_test_step(service_id, template_id, step_index):
|
||||
if (
|
||||
request.endpoint == 'main.send_one_off_step' and
|
||||
step_index == 0 and
|
||||
template.template_type != 'letter'
|
||||
template.template_type != 'letter' and
|
||||
not (template.template_type == 'sms' and current_user.mobile_number is None)
|
||||
):
|
||||
skip_link = (
|
||||
'Use my {}'.format(first_column_headings[template.template_type][0]),
|
||||
|
||||
@@ -432,28 +432,6 @@ def service_edit_email_reply_to(service_id, reply_to_email_id):
|
||||
reply_to_email_address_id=reply_to_email_address['id'])
|
||||
|
||||
|
||||
@main.route("/services/<service_id>/service-settings/set-sms-sender", methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@user_has_permissions('manage_settings', admin_override=True)
|
||||
def service_set_sms_sender(service_id):
|
||||
form = ServiceSmsSenderForm()
|
||||
if form.validate_on_submit():
|
||||
if 'inbound_sms' in current_service['permissions']:
|
||||
abort(403)
|
||||
service_api_client.update_service(
|
||||
current_service['id'],
|
||||
sms_sender=form.sms_sender.data or None
|
||||
)
|
||||
return redirect(url_for('.service_settings', service_id=service_id))
|
||||
|
||||
if request.method == 'GET':
|
||||
form.sms_sender.data = current_service.get('sms_sender')
|
||||
|
||||
return render_template(
|
||||
'views/service-settings/set-sms-sender.html',
|
||||
form=form)
|
||||
|
||||
|
||||
@main.route("/services/<service_id>/service-settings/set-inbound-number", methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@user_has_permissions('manage_settings', admin_override=True)
|
||||
|
||||
@@ -48,11 +48,11 @@ def sign_in():
|
||||
if user:
|
||||
session['user_details'] = {"email": user.email_address, "id": user.id}
|
||||
if user.is_active:
|
||||
user_api_client.send_verify_code(user.id, 'sms', user.mobile_number)
|
||||
if request.args.get('next'):
|
||||
return redirect(url_for('.two_factor', next=request.args.get('next')))
|
||||
if user.auth_type == "email_auth":
|
||||
return sign_in_email(user.id, user.email_address)
|
||||
else:
|
||||
return redirect(url_for('.two_factor'))
|
||||
return sign_in_sms(user.id, user.mobile_number)
|
||||
|
||||
# Vague error message for login in case of user not known, locked, inactive or password not verified
|
||||
flash(Markup(
|
||||
(
|
||||
@@ -70,6 +70,22 @@ def sign_in():
|
||||
)
|
||||
|
||||
|
||||
def sign_in_email(user_id, to):
|
||||
if request.args.get('next'):
|
||||
user_api_client.send_verify_code(user_id, 'email', None, request.args.get('next'))
|
||||
else:
|
||||
user_api_client.send_verify_code(user_id, 'email', None)
|
||||
return redirect(url_for('.two_factor_email_sent'))
|
||||
|
||||
|
||||
def sign_in_sms(user_id, to):
|
||||
user_api_client.send_verify_code(user_id, 'sms', to)
|
||||
if request.args.get('next'):
|
||||
return redirect(url_for('.two_factor', next=request.args.get('next')))
|
||||
else:
|
||||
return redirect(url_for('.two_factor'))
|
||||
|
||||
|
||||
@login_manager.unauthorized_handler
|
||||
def sign_in_again():
|
||||
return redirect(
|
||||
|
||||
@@ -1,16 +1,65 @@
|
||||
import json
|
||||
from flask import (
|
||||
render_template,
|
||||
redirect,
|
||||
session,
|
||||
url_for,
|
||||
request
|
||||
request,
|
||||
current_app,
|
||||
flash
|
||||
)
|
||||
|
||||
from flask_login import login_user, current_user
|
||||
from app.main import main
|
||||
from app.main.forms import TwoFactorForm
|
||||
from app import service_api_client, user_api_client
|
||||
from app.utils import redirect_to_sign_in
|
||||
from notifications_utils.url_safe_token import check_token
|
||||
from itsdangerous import SignatureExpired
|
||||
|
||||
|
||||
@main.route('/two-factor-email-sent', methods=['GET'])
|
||||
def two_factor_email_sent():
|
||||
title = 'Email resent' if request.args.get('email_resent') else 'Check your email'
|
||||
return render_template(
|
||||
'views/two-factor-email.html',
|
||||
title=title
|
||||
)
|
||||
|
||||
|
||||
@main.route('/email-auth/<token>', methods=['GET'])
|
||||
def two_factor_email(token):
|
||||
if current_user.is_authenticated:
|
||||
return redirect_when_logged_in(current_user.id)
|
||||
|
||||
# checks url is valid, and hasn't timed out
|
||||
try:
|
||||
token_data = json.loads(check_token(
|
||||
token,
|
||||
current_app.config['SECRET_KEY'],
|
||||
current_app.config['DANGEROUS_SALT'],
|
||||
current_app.config['EMAIL_2FA_EXPIRY_SECONDS']
|
||||
))
|
||||
except SignatureExpired as exc:
|
||||
# lets decode again, without the expiry, to get the user id out
|
||||
orig_data = json.loads(check_token(
|
||||
token,
|
||||
current_app.config['SECRET_KEY'],
|
||||
current_app.config['DANGEROUS_SALT'],
|
||||
None
|
||||
))
|
||||
session['user_details'] = {'id': orig_data['user_id']}
|
||||
flash("The link in the email we sent you has expired. We’ve sent you a new one.")
|
||||
return redirect(url_for('.resend_email_link'))
|
||||
|
||||
user_id = token_data['user_id']
|
||||
# checks if code was already used
|
||||
logged_in, msg = user_api_client.check_verify_code(user_id, token_data['secret_code'], "email")
|
||||
|
||||
if not logged_in:
|
||||
flash("This link has already been used")
|
||||
session['user_details'] = {'id': user_id}
|
||||
return redirect(url_for('.resend_email_link'))
|
||||
return log_in_user(user_id)
|
||||
|
||||
|
||||
@main.route('/two-factor', methods=['GET', 'POST'])
|
||||
@@ -24,29 +73,7 @@ def two_factor():
|
||||
form = TwoFactorForm(_check_code)
|
||||
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
user = user_api_client.get_user(user_id)
|
||||
# the user will have a new current_session_id set by the API - store it in the cookie for future requests
|
||||
session['current_session_id'] = user.current_session_id
|
||||
services = service_api_client.get_active_services({'user_id': str(user_id)}).get('data', [])
|
||||
# Check if coming from new password page
|
||||
if 'password' in session['user_details']:
|
||||
user = user_api_client.update_password(user.id, password=session['user_details']['password'])
|
||||
activated_user = user_api_client.activate_user(user)
|
||||
login_user(activated_user)
|
||||
finally:
|
||||
del session['user_details']
|
||||
|
||||
next_url = request.args.get('next')
|
||||
if next_url and _is_safe_redirect_url(next_url):
|
||||
return redirect(next_url)
|
||||
|
||||
if current_user.platform_admin:
|
||||
return redirect(url_for('main.platform_admin'))
|
||||
if len(services) == 1:
|
||||
return redirect(url_for('main.service_dashboard', service_id=services[0]['id']))
|
||||
else:
|
||||
return redirect(url_for('main.choose_service'))
|
||||
return log_in_user(user_id)
|
||||
|
||||
return render_template('views/two-factor.html', form=form)
|
||||
|
||||
@@ -58,3 +85,34 @@ def _is_safe_redirect_url(target):
|
||||
redirect_url = urlparse(urljoin(request.host_url, target))
|
||||
return redirect_url.scheme in ('http', 'https') and \
|
||||
host_url.netloc == redirect_url.netloc
|
||||
|
||||
|
||||
def log_in_user(user_id):
|
||||
try:
|
||||
user = user_api_client.get_user(user_id)
|
||||
# the user will have a new current_session_id set by the API - store it in the cookie for future requests
|
||||
session['current_session_id'] = user.current_session_id
|
||||
# Check if coming from new password page
|
||||
if 'password' in session.get('user_details', {}):
|
||||
user = user_api_client.update_password(user.id, password=session['user_details']['password'])
|
||||
activated_user = user_api_client.activate_user(user)
|
||||
login_user(activated_user)
|
||||
finally:
|
||||
session.pop("user_details", None)
|
||||
|
||||
return redirect_when_logged_in(user_id)
|
||||
|
||||
|
||||
def redirect_when_logged_in(user_id):
|
||||
next_url = request.args.get('next')
|
||||
if next_url and _is_safe_redirect_url(next_url):
|
||||
return redirect(next_url)
|
||||
if current_user.platform_admin:
|
||||
return redirect(url_for('main.platform_admin'))
|
||||
|
||||
services = service_api_client.get_active_services({'user_id': str(user_id)}).get('data', [])
|
||||
|
||||
if len(services) == 1:
|
||||
return redirect(url_for('main.service_dashboard', service_id=services[0]['id']))
|
||||
else:
|
||||
return redirect(url_for('main.choose_service'))
|
||||
|
||||
@@ -19,6 +19,3 @@ class InboundNumberClient(NotifyAdminAPIClient):
|
||||
|
||||
def get_inbound_sms_number_for_service(self, service_id):
|
||||
return self.get('/inbound-number/service/{}'.format(service_id))
|
||||
|
||||
def activate_inbound_sms_service(self, service_id):
|
||||
return self.post(url='/inbound-number/service/{}'.format(service_id), data={})
|
||||
|
||||
@@ -54,12 +54,6 @@ class UserApiClient(NotifyAdminAPIClient):
|
||||
users.append(User(user, max_failed_login_count=self.max_failed_login_count))
|
||||
return users
|
||||
|
||||
def update_user(self, user):
|
||||
data = user.serialize()
|
||||
url = "/user/{}".format(user.id)
|
||||
user_data = self.put(url, data=data)
|
||||
return User(user_data['data'], max_failed_login_count=self.max_failed_login_count)
|
||||
|
||||
def update_user_attribute(self, user_id, **kwargs):
|
||||
data = dict(kwargs)
|
||||
disallowed_attributes = set(data.keys()) - ALLOWED_ATTRIBUTES
|
||||
@@ -94,8 +88,10 @@ class UserApiClient(NotifyAdminAPIClient):
|
||||
if e.status_code == 400 or e.status_code == 404:
|
||||
return False
|
||||
|
||||
def send_verify_code(self, user_id, code_type, to):
|
||||
def send_verify_code(self, user_id, code_type, to, next_string=None):
|
||||
data = {'to': to}
|
||||
if next_string:
|
||||
data['next'] = next_string
|
||||
endpoint = '/user/{0}/{1}-code'.format(user_id, code_type)
|
||||
self.post(endpoint, data=data)
|
||||
|
||||
@@ -154,8 +150,9 @@ class UserApiClient(NotifyAdminAPIClient):
|
||||
|
||||
def activate_user(self, user):
|
||||
if user.state == 'pending':
|
||||
user.state = 'active'
|
||||
return self.update_user(user)
|
||||
url = "/user/{}/activate".format(user.id)
|
||||
user_data = self.post(url, data=None)
|
||||
return User(user_data['data'], max_failed_login_count=self.max_failed_login_count)
|
||||
else:
|
||||
return user
|
||||
|
||||
|
||||
24
app/templates/views/email-not-received.html
Normal file
24
app/templates/views/email-not-received.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% extends "withoutnav_template.html" %}
|
||||
{% from "components/page-footer.html" import page_footer %}
|
||||
|
||||
{% block per_page_title %}
|
||||
Resend email link
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
<div class="grid-row">
|
||||
<div class="column-two-thirds">
|
||||
<h1 class="heading-large">Resend email link</h1>
|
||||
|
||||
<p> Email messages sometimes take a few minutes to arrive. If you do not recieve the email, you can resend it.</p>
|
||||
<p> If you no longer have access to the email address you registered for this service, speak to your service manager to reset the email.</p>
|
||||
|
||||
<p>
|
||||
<a class="button" href="{{url_for('main.resend_email_link')}}" role="button">Resend email link</a>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,32 +0,0 @@
|
||||
{% extends "withnav_template.html" %}
|
||||
{% from "components/textbox.html" import textbox %}
|
||||
{% from "components/page-footer.html" import page_footer %}
|
||||
|
||||
{% block service_page_title %}
|
||||
Text message sender
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
<h1 class="heading-large">Text message sender</h1>
|
||||
<p>
|
||||
This appears instead of a phone number when a user receives a
|
||||
text message from your service.
|
||||
<p>
|
||||
If you set this to ‘GOVUK’ each message will begin with
|
||||
‘{{ current_service.name }}:’.
|
||||
</p>
|
||||
<form method="post">
|
||||
{{ textbox(
|
||||
form.sms_sender,
|
||||
width='1-4',
|
||||
hint='Up to 11 characters, letters, numbers and spaces only'
|
||||
) }}
|
||||
{{ page_footer(
|
||||
'Save',
|
||||
back_link=url_for('.service_settings', service_id=current_service.id),
|
||||
back_link_text='Back to settings'
|
||||
) }}
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
21
app/templates/views/two-factor-email.html
Normal file
21
app/templates/views/two-factor-email.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% extends "withoutnav_template.html" %}
|
||||
{% from "components/page-footer.html" import page_footer %}
|
||||
|
||||
{% block per_page_title %}
|
||||
Email verification
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
<div class="grid-row">
|
||||
<div class="column-two-thirds">
|
||||
<h1 class="heading-large">{{ title }}</h1>
|
||||
<p> We’ve sent you an email with your login link</p>
|
||||
{{ page_footer(
|
||||
secondary_link=url_for('main.email_not_received'),
|
||||
secondary_link_text='Not received an email?'
|
||||
) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user