diff --git a/app/__init__.py b/app/__init__.py index 1267b23d3..3f3a1b6e0 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -3,7 +3,15 @@ import re import dateutil import urllib -from flask import (Flask, session, Markup, escape, render_template, make_response, current_app) +from flask import ( + Flask, + session, + Markup, + escape, + render_template, + make_response, + current_app, + request) from flask._compat import string_types from flask_login import LoginManager from flask_wtf import CsrfProtect @@ -27,6 +35,10 @@ from utils.recipients import validate_phone_number, InvalidPhoneError import app.proxy_fix from config import configs from utils import logging +from werkzeug.local import LocalStack, LocalProxy +from flask.globals import _lookup_req_object +from functools import partial + login_manager = LoginManager() csrf = CsrfProtect() @@ -41,6 +53,9 @@ invite_api_client = InviteApiClient() statistics_api_client = StatisticsApiClient() asset_fingerprinter = AssetFingerprinter() +# The current service attached to the request stack. +current_service = LocalProxy(partial(_lookup_req_object, 'service')) + def create_app(): application = Flask(__name__) @@ -82,6 +97,12 @@ def create_app(): application.add_template_filter(linkable_name) application.after_request(useful_headers_after_request) + application.after_request(save_service_after_request) + application.before_request(load_service_before_request) + + def _attach_current_service(): + return {'current_service': current_service} + application.context_processor(_attach_current_service) register_errorhandlers(application) return application @@ -167,7 +188,26 @@ def load_user(user_id): return user_api_client.get_user(user_id) -# https://www.owasp.org/index.php/List_of_useful_HTTP_headers +def load_service_before_request(): + service_id = request.view_args.get('service_id', None) if request.view_args else None + if service_id: + from flask.globals import _request_ctx_stack + if _request_ctx_stack.top is not None: + setattr( + _request_ctx_stack.top, + 'service', + service_api_client.get_service(service_id)['data']) + + +def save_service_after_request(response): + # Only save the current session if the request is 200 + service_id = request.view_args.get('service_id', None) if request.view_args else None + if response.status_code == 200 and service_id: + session['service_id'] = service_id + return response + + +# https://www.owasp.org/index.php/List_of_useful_HTTP_headers def useful_headers_after_request(response): response.headers.add('X-Frame-Options', 'deny') response.headers.add('X-Content-Type-Options', 'nosniff') diff --git a/app/main/views/add_service.py b/app/main/views/add_service.py index 3ef20c86b..471058d44 100644 --- a/app/main/views/add_service.py +++ b/app/main/views/add_service.py @@ -35,9 +35,8 @@ def add_service(): form = AddServiceForm(service_api_client.find_all_service_email_from) heading = 'Which service do you want to set up notifications for?' if form.validate_on_submit(): - session['service_name'] = form.name.data - email_from = email_safe(session['service_name']) - service_id = service_api_client.create_service(service_name=session['service_name'], + email_from = email_safe(form.name.data) + service_id = service_api_client.create_service(service_name=form.name.data, active=False, limit=current_app.config['DEFAULT_SERVICE_LIMIT'], restricted=True, diff --git a/app/main/views/api_keys.py b/app/main/views/api_keys.py index 0ee7b2220..4d992c249 100644 --- a/app/main/views/api_keys.py +++ b/app/main/views/api_keys.py @@ -17,7 +17,6 @@ def documentation(): def api_keys(service_id): return render_template( 'views/api-keys.html', - service_id=service_id, keys=api_key_api_client.get_api_keys(service_id=service_id)['apiKeys'] ) @@ -32,11 +31,10 @@ def create_api_key(service_id): form = CreateKeyForm(key_names) if form.validate_on_submit(): secret = api_key_api_client.create_api_key(service_id=service_id, key_name=form.key_name.data) - return render_template('views/api-keys/show.html', service_id=service_id, secret=secret, + return render_template('views/api-keys/show.html', secret=secret, key_name=form.key_name.data) return render_template( 'views/api-keys/create.html', - service_id=service_id, key_name=form.key_name ) @@ -49,7 +47,6 @@ def revoke_api_key(service_id, key_id): if request.method == 'GET': return render_template( 'views/api-keys/revoke.html', - service_id=service_id, key_name=key_name ) elif request.method == 'POST': diff --git a/app/main/views/choose_service.py b/app/main/views/choose_service.py index 23a3a80ff..657e6ab22 100644 --- a/app/main/views/choose_service.py +++ b/app/main/views/choose_service.py @@ -23,4 +23,7 @@ def show_all_services_or_dashboard(): if 1 == len(services): return redirect(url_for('.service_dashboard', service_id=services[0]['id'])) else: + service_id = session.get('service_id', None) + if any([service_id == x['id'] for x in services]): + return redirect(url_for('.service_dashboard', service_id=service_id)) return redirect(url_for('.choose_service')) diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py index 5deafee07..2fbb4c04f 100644 --- a/app/main/views/dashboard.py +++ b/app/main/views/dashboard.py @@ -2,13 +2,14 @@ from flask import ( render_template, session, flash, - jsonify + jsonify, + request ) from datetime import date from flask_login import login_required from app.main import main -from app import (job_api_client, statistics_api_client, service_api_client) +from app import (job_api_client, statistics_api_client, service_api_client, current_service) from app.utils import user_has_permissions @@ -19,14 +20,10 @@ def service_dashboard(service_id): templates = service_api_client.get_service_templates(service_id)['data'] jobs = job_api_client.get_job(service_id)['data'] - service = service_api_client.get_service(service_id) - session['service_name'] = service['data']['name'] - session['service_id'] = service['data']['id'] - if session.get('invited_user'): session.pop('invited_user', None) - service_name = service['data']['name'] - message = 'You have successfully accepted your invitation and been added to {}'.format(service_name) + message = 'You have successfully accepted your invitation and been added to {}'.format( + current_service['name']) flash(message, 'default_with_tick') statistics = statistics_api_client.get_statistics_for_service(service_id)['data'] @@ -37,7 +34,6 @@ def service_dashboard(service_id): more_jobs_to_show=(len(jobs) > 5), free_text_messages_remaining='250,000', spent_this_month='0.00', - service=service['data'], statistics=add_rates_to(statistics), templates=templates, service_id=str(service_id)) diff --git a/app/main/views/jobs.py b/app/main/views/jobs.py index 6719664f5..abcb0d85c 100644 --- a/app/main/views/jobs.py +++ b/app/main/views/jobs.py @@ -12,7 +12,11 @@ from flask_login import login_required from werkzeug.datastructures import MultiDict from utils.template import Template -from app import (job_api_client, notification_api_client, service_api_client) +from app import ( + job_api_client, + notification_api_client, + service_api_client, + current_service) from app.main import main from app.utils import ( get_page_from_request, @@ -38,8 +42,7 @@ def view_jobs(service_id): jobs = job_api_client.get_job(service_id)['data'] return render_template( 'views/jobs/jobs.html', - jobs=jobs, - service_id=service_id + jobs=jobs ) @@ -47,7 +50,6 @@ def view_jobs(service_id): @login_required @user_has_permissions('view_activity', admin_override=True) def view_job(service_id, job_id): - service = service_api_client.get_service(service_id)['data'] job = job_api_client.get_job(service_id, job_id)['data'] template = service_api_client.get_service_template(service_id, job['template'])['data'] notifications = notification_api_client.get_notifications_for_service(service_id, job_id) @@ -64,10 +66,8 @@ def view_job(service_id, job_id): uploaded_file_name=job['original_file_name'], template=Template( template, - prefix=service['name'] + prefix=current_service['name'] ), - service_id=service_id, - service=service, job_id=job_id ) @@ -76,7 +76,6 @@ def view_job(service_id, job_id): @login_required @user_has_permissions('view_activity') def view_job_updates(service_id, job_id): - service = service_api_client.get_service(service_id)['data'] job = job_api_client.get_job(service_id, job_id)['data'] notifications = notification_api_client.get_notifications_for_service(service_id, job_id) finished = job['status'] == 'finished' @@ -135,7 +134,6 @@ def view_notifications(service_id): 'page {}'.format(page + 1)) return render_template( 'views/notifications.html', - service_id=service_id, notifications=notifications['notifications'], page=page, prev_page=prev_page, @@ -157,6 +155,5 @@ def view_notification(service_id, job_id, notification_id): ][0], delivered_at=now, uploaded_at=now, - service_id=service_id, job_id=job_id ) diff --git a/app/main/views/manage_users.py b/app/main/views/manage_users.py index ce95e7011..eecaab1e2 100644 --- a/app/main/views/manage_users.py +++ b/app/main/views/manage_users.py @@ -37,7 +37,6 @@ roles = { def manage_users(service_id): return render_template( 'views/manage-users.html', - service_id=service_id, users=user_api_client.get_users_for_service(service_id=service_id), current_user=current_user, invited_users=[ @@ -51,7 +50,6 @@ def manage_users(service_id): @login_required @user_has_permissions('manage_users', admin_override=True) def invite_user(service_id): - service = service_api_client.get_service(service_id)['data'] form = InviteUserForm(invalid_email_address=current_user.email_address) @@ -75,7 +73,6 @@ def invite_user(service_id): return render_template( 'views/invite-user.html', - service_id=service_id, form=form ) @@ -87,7 +84,6 @@ def edit_user_permissions(service_id, user_id): # TODO we should probably using the service id here in the get user # call as well. eg. /user/?&service=service_id user = user_api_client.get_user(user_id) - service = service_api_client.get_service(service_id)['data'] # Need to make the email address read only, or a disabled field? # Do it through the template or the form class? form = PermissionsForm(**{ @@ -106,8 +102,7 @@ def edit_user_permissions(service_id, user_id): return render_template( 'views/edit-user-permissions.html', user=user, - form=form, - service_id=service_id + form=form ) @@ -116,7 +111,6 @@ def edit_user_permissions(service_id, user_id): @user_has_permissions('manage_users', admin_override=True) def remove_user_from_service(service_id, user_id): user = user_api_client.get_user(user_id) - service = service_api_client.get_service(service_id)['data'] # Need to make the email address read only, or a disabled field? # Do it through the template or the form class? form = PermissionsForm(**{ @@ -145,8 +139,7 @@ def remove_user_from_service(service_id, user_id): return render_template( 'views/edit-user-permissions.html', user=user, - form=form, - service_id=service_id + form=form ) diff --git a/app/main/views/send.py b/app/main/views/send.py index 62125a589..c0d0297d3 100644 --- a/app/main/views/send.py +++ b/app/main/views/send.py @@ -25,8 +25,8 @@ from app.main.uploader import ( s3upload, s3download ) -from app import (job_api_client, service_api_client) -from app.utils import user_has_permissions, get_errors_for_csv +from app import (job_api_client, service_api_client, current_service) +from app.utils import (user_has_permissions, get_errors_for_csv) def get_send_button_text(template_type, number_of_messages): @@ -72,9 +72,6 @@ def get_example_csv_rows(template, number_of_rows=2): 'manage_api_keys', admin_override=True, any_=True) def choose_template(service_id, template_type): - - service = service_api_client.get_service(service_id)['data'] - if template_type not in ['email', 'sms']: abort(404) @@ -83,14 +80,12 @@ def choose_template(service_id, template_type): templates=[ Template( template, - prefix=service['name'] + prefix=current_service['name'] ) for template in service_api_client.get_service_templates(service_id)['data'] if template['template_type'] == template_type ], template_type=template_type, - page_heading=get_page_headings(template_type), - service=service, - service_id=service_id + page_heading=get_page_headings(template_type) ) @@ -98,11 +93,9 @@ def choose_template(service_id, template_type): @login_required @user_has_permissions('send_texts', 'send_emails', 'send_letters') def send_messages(service_id, template_id): - - service = service_api_client.get_service(service_id)['data'] template = Template( service_api_client.get_service_template(service_id, template_id)['data'], - prefix=service['name'] + prefix=current_service['name'] ) form = CsvUploadForm() @@ -136,9 +129,7 @@ def send_messages(service_id, template_id): template=template, recipient_column=first_column_heading[template.template_type], example=get_example_csv_rows(template), - form=form, - service=service, - service_id=service_id + form=form ) @@ -179,8 +170,8 @@ def send_message_to_self(service_id, template_id): session['upload_data'] = {"template_id": template_id, "original_file_name": filedata['file_name']} return redirect(url_for('.check_messages', - service_id=service_id, upload_id=upload_id, + service_id=service_id, template_type=template.template_type)) @@ -201,8 +192,7 @@ def send_from_api(service_id, template_id): return render_template( 'views/send-from-api.html', template=template, - payload=json.dumps(payload, indent=4), - service_id=service_id + payload=json.dumps(payload, indent=4) ) @@ -214,8 +204,6 @@ def check_messages(service_id, template_type, upload_id): if not session.get('upload_data'): return redirect(url_for('main.choose_template', service_id=service_id, template_type=template_type)) - service = service_api_client.get_service(service_id)['data'] - contents = s3download(service_id, upload_id) if not contents: flash('There was a problem reading your upload file') @@ -227,7 +215,7 @@ def check_messages(service_id, template_type, upload_id): template = Template( template, - prefix=service['name'] + prefix=current_service['name'] ) recipients = RecipientCSV( @@ -259,8 +247,6 @@ def check_messages(service_id, template_type, upload_id): ), original_file_name=session['upload_data'].get('original_file_name'), send_button_text=get_send_button_text(template.template_type, session['upload_data']['notification_count']), - service_id=service_id, - service=service, upload_id=upload_id, form=CsvUploadForm() ) @@ -272,7 +258,6 @@ def check_messages(service_id, template_type, upload_id): def start_job(service_id, upload_id): upload_data = session['upload_data'] - service = service_api_client.get_service(service_id)['data'] if request.files or not upload_data.get('valid'): # The csv was invalid, validate the csv again @@ -289,5 +274,5 @@ def start_job(service_id, upload_id): ) return redirect( - url_for('main.view_job', service_id=service_id, job_id=upload_id) + url_for('main.view_job', job_id=upload_id, service_id=service_id) ) diff --git a/app/main/views/service_settings.py b/app/main/views/service_settings.py index b02dafacb..13f24c739 100644 --- a/app/main/views/service_settings.py +++ b/app/main/views/service_settings.py @@ -18,27 +18,20 @@ from app.main import main from app.utils import user_has_permissions, email_safe from app.main.forms import ConfirmPasswordForm, ServiceNameForm from app import user_api_client +from app import current_service @main.route("/services//service-settings") @login_required @user_has_permissions('manage_settings', admin_override=True) def service_settings(service_id): - service = service_api_client.get_service(service_id)['data'] - - return render_template( - 'views/service-settings.html', - service=service, - service_id=service_id - ) + return render_template('views/service-settings.html') @main.route("/services//service-settings/name", methods=['GET', 'POST']) @login_required @user_has_permissions('manage_settings', admin_override=True) def service_name_change(service_id): - service = service_api_client.get_service(service_id)['data'] - form = ServiceNameForm(service_api_client.find_all_service_email_from) if form.validate_on_submit(): @@ -47,16 +40,13 @@ def service_name_change(service_id): return render_template( 'views/service-settings/name.html', - service=service, - form=form, - service_id=service_id) + form=form) @main.route("/services//service-settings/name/confirm", methods=['GET', 'POST']) @login_required @user_has_permissions('manage_settings', admin_override=True) def service_name_change_confirm(service_id): - service = service_api_client.get_service(service_id)['data'] # Validate password for form def _check_password(pwd): @@ -64,17 +54,17 @@ def service_name_change_confirm(service_id): form = ConfirmPasswordForm(_check_password) if form.validate_on_submit(): - service['name'] = session['service_name_change'] - service['email_from'] = email_safe(session['service_name_change']) + current_service['name'] = session['service_name_change'] + current_service['email_from'] = email_safe(session['service_name_change']) try: service_api_client.update_service( - service['id'], - service['name'], - service['active'], - service['limit'], - service['restricted'], - service['users'], - service['email_from']) + current_service['id'], + current_service['name'], + current_service['active'], + current_service['limit'], + current_service['restricted'], + current_service['users'], + current_service['email_from']) except HTTPError as e: error_msg = "Duplicate service name '{}'".format(session['service_name_change']) if e.status_code == 400 and error_msg in e.message['name']: @@ -84,26 +74,21 @@ def service_name_change_confirm(service_id): else: raise e else: - session['service_name'] = service['name'] session.pop('service_name_change') return redirect(url_for('.service_settings', service_id=service_id)) return render_template( 'views/service-settings/confirm.html', heading='Change your service name', - form=form, - service_id=service_id) + form=form) @main.route("/services//service-settings/request-to-go-live", methods=['GET', 'POST']) @login_required @user_has_permissions('manage_settings', admin_override=True) def service_request_to_go_live(service_id): - service = service_api_client.get_service(service_id)['data'] if request.method == 'GET': return render_template( - 'views/service-settings/request-to-go-live.html', - service=service, - service_id=service_id + 'views/service-settings/request-to-go-live.html' ) elif request.method == 'POST': flash('Thanks your request to go live is being processed', 'default') @@ -115,13 +100,9 @@ def service_request_to_go_live(service_id): @login_required @user_has_permissions('manage_settings', admin_override=True) def service_status_change(service_id): - service = service_api_client.get_service(service_id)['data'] - if request.method == 'GET': return render_template( - 'views/service-settings/status.html', - service=service, - service_id=service_id + 'views/service-settings/status.html' ) elif request.method == 'POST': return redirect(url_for('.service_status_change_confirm', service_id=service_id)) @@ -131,43 +112,37 @@ def service_status_change(service_id): @login_required @user_has_permissions('manage_settings', admin_override=True) def service_status_change_confirm(service_id): - service = service_api_client.get_service(service_id)['data'] - # Validate password for form def _check_password(pwd): return user_api_client.verify_password(current_user.id, pwd) form = ConfirmPasswordForm(_check_password) if form.validate_on_submit(): - service['active'] = True + current_service['active'] = True service_api_client.update_service( - service['id'], - service['name'], - service['active'], - service['limit'], - service['restricted'], - service['users'], - service['email_from']) + current_service['id'], + current_service['name'], + current_service['active'], + current_service['limit'], + current_service['restricted'], + current_service['users'], + current_service['email_from']) return redirect(url_for('.service_settings', service_id=service_id)) return render_template( 'views/service-settings/confirm.html', heading='Turn off all outgoing notifications', destructive=True, - form=form, - service_id=service_id) + form=form) @main.route("/services//service-settings/delete", methods=['GET', 'POST']) @login_required @user_has_permissions('manage_settings', admin_override=True) def service_delete(service_id): - service = service_api_client.get_service(service_id)['data'] if request.method == 'GET': return render_template( - 'views/service-settings/delete.html', - service=service, - service_id=service_id + 'views/service-settings/delete.html' ) elif request.method == 'POST': return redirect(url_for('.service_delete_confirm', service_id=service_id)) @@ -177,7 +152,6 @@ def service_delete(service_id): @login_required @user_has_permissions('manage_settings', admin_override=True) def service_delete_confirm(service_id): - service = service_api_client.get_service(service_id)['data'] # Validate password for form def _check_password(pwd): @@ -185,12 +159,11 @@ def service_delete_confirm(service_id): form = ConfirmPasswordForm(_check_password) if form.validate_on_submit(): - service = service_api_client.delete_service(service_id) + service_api_client.delete_service(service_id) return redirect(url_for('.choose_service')) return render_template( 'views/service-settings/confirm.html', heading='Delete this service from Notify', destructive=True, - form=form, - service_id=service_id) + form=form) diff --git a/app/main/views/templates.py b/app/main/views/templates.py index aecb8d90a..2e3a76418 100644 --- a/app/main/views/templates.py +++ b/app/main/views/templates.py @@ -23,8 +23,6 @@ page_headings = { @user_has_permissions('manage_templates', admin_override=True) def add_service_template(service_id, template_type): - service = service_api_client.get_service(service_id) - if template_type not in ['sms', 'email']: abort(404) @@ -46,7 +44,6 @@ def add_service_template(service_id, template_type): 'views/edit-{}-template.html'.format(template_type), form=form, template_type=template_type, - service_id=service_id, heading_action='Add' ) @@ -73,7 +70,6 @@ def edit_service_template(service_id, template_id): return render_template( 'views/edit-{}-template.html'.format(template['template_type']), form=form, - service_id=service_id, template_id=template_id, template_type=template['template_type'], heading_action='Edit' @@ -101,5 +97,4 @@ def delete_service_template(service_id, template_id): 'views/edit-{}-template.html'.format(template['template_type']), h1='Edit template', form=form, - service_id=service_id, template_id=template_id) diff --git a/app/main/views/tour.py b/app/main/views/tour.py index 3615b2a9c..1074ce0bd 100644 --- a/app/main/views/tour.py +++ b/app/main/views/tour.py @@ -9,7 +9,6 @@ from app.main import main def tour(service_id, page): return render_template( 'views/tour/{}.html'.format(page), - service_id=service_id, # TODO: fix when Nick’s PR is merged current_page=page, next_page=(page + 1) ) diff --git a/app/templates/main_nav.html b/app/templates/main_nav.html index 418df91a8..b4448f427 100644 --- a/app/templates/main_nav.html +++ b/app/templates/main_nav.html @@ -1,31 +1,31 @@