diff --git a/.ds.baseline b/.ds.baseline index c64d22627..56c3afc7d 100644 --- a/.ds.baseline +++ b/.ds.baseline @@ -161,7 +161,7 @@ "filename": "app/config.py", "hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc", "is_verified": false, - "line_number": 125, + "line_number": 123, "is_secret": false } ], @@ -684,5 +684,5 @@ } ] }, - "generated_at": "2024-11-14T15:53:44Z" + "generated_at": "2024-11-21T23:08:45Z" } diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index f002bb3fc..c3ef5dcbb 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -144,6 +144,7 @@ jobs: inputs: requirements.txt ignore-vulns: | PYSEC-2024-60 + PYSEC-2022-43162 - name: Run npm audit run: make npm-audit diff --git a/app/__init__.py b/app/__init__.py index 64580fcc1..4d89cd59e 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,8 +1,9 @@ import os import pathlib +import secrets from functools import partial from time import monotonic -from urllib.parse import urlparse, urlunparse +from urllib.parse import unquote, urlparse, urlunparse import jinja2 from flask import ( @@ -114,6 +115,7 @@ from notifications_utils.formatters import ( get_lines_with_normalised_whitespace, ) from notifications_utils.recipients import format_phone_number_human_readable +from notifications_utils.url_safe_token import generate_token login_manager = LoginManager() csrf = CSRFProtect() @@ -168,10 +170,38 @@ def create_app(application): @application.context_processor def inject_feature_flags(): - feature_best_practices_enabled = application.config[ - "FEATURE_BEST_PRACTICES_ENABLED" - ] - return dict(FEATURE_BEST_PRACTICES_ENABLED=feature_best_practices_enabled) + feature_best_practices_enabled = application.config.get("FEATURE_BEST_PRACTICES_ENABLED", False) + feature_about_page_enabled = application.config.get("FEATURE_ABOUT_PAGE_ENABLED", False) + return dict( + FEATURE_BEST_PRACTICES_ENABLED=feature_best_practices_enabled, + FEATURE_ABOUT_PAGE_ENABLED=feature_about_page_enabled, + ) + + @application.context_processor + def inject_initial_signin_url(): + ttl = 24 * 60 * 60 + + # make and store the state + state = generate_token( + str(request.remote_addr), + current_app.config["SECRET_KEY"], + current_app.config["DANGEROUS_SALT"], + ) + + state_key = f"login-state-{unquote(state)}" + redis_client.set(state_key, state, ex=ttl) + + # make and store the nonce + nonce = secrets.token_urlsafe() + nonce_key = f"login-nonce-{unquote(nonce)}" + redis_client.set(nonce_key, nonce, ex=ttl) + + url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL") + if url is not None: + url = url.replace("NONCE", nonce) + url = url.replace("STATE", state) + + return {'initial_signin_url': url} notify_environment = os.environ["NOTIFY_ENVIRONMENT"] diff --git a/app/assets/images/alarm.svg b/app/assets/images/alarm.svg new file mode 100644 index 000000000..57e6180bc --- /dev/null +++ b/app/assets/images/alarm.svg @@ -0,0 +1 @@ + diff --git a/app/assets/images/alert.svg b/app/assets/images/alert.svg new file mode 100644 index 000000000..d0d516d8f --- /dev/null +++ b/app/assets/images/alert.svg @@ -0,0 +1 @@ + diff --git a/app/assets/images/calendar.svg b/app/assets/images/calendar.svg new file mode 100644 index 000000000..9b755e9fd --- /dev/null +++ b/app/assets/images/calendar.svg @@ -0,0 +1 @@ + diff --git a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss index da5d77bf2..e57146b14 100644 --- a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss +++ b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss @@ -676,28 +676,28 @@ details form { margin-top: 0; } -ol.best-practices-list { +ol.guides-list { counter-reset: item; list-style-type: none; padding-left: 0; } -ol.best-practices-list.set-two { +ol.guides-list.set-two { counter-reset: item 6; } -ol.best-practices-list.set-three { +ol.guides-list.set-three { counter-reset: item 10; } -ol.best-practices-list li { +ol.guides-list li { counter-increment: item; margin-bottom: 15px; position: relative; padding-left: 40px; } -ol.best-practices-list li::before { +ol.guides-list li::before { content: counter(item); background-color: #005ea2; color: white; @@ -713,11 +713,11 @@ ol.best-practices-list li::before { top: 0; } -li.best-practices { +li.guides { padding-bottom: 50px; } -div.best-practices { +div.guides { height: 400px } @@ -820,7 +820,7 @@ $do-dont-top-bar-width: 1; } @media (max-width: 758px) { - .best-practices-flex-container { + .guides-flex-container { flex-direction: column; } } @@ -898,7 +898,7 @@ li.linked-card:hover svg, display: block; } -.about-icon-list { +.icon-list { display: flex; width: 24px; height: 24px; @@ -908,10 +908,6 @@ li.linked-card:hover svg, margin-right: 4px; } -.usa-icon-list__content{ - padding-left: 0; -} - .indented-paragraph { margin-left: calc(24px + 4px); margin-top: 4px; diff --git a/app/config.py b/app/config.py index f40b46dea..146230047 100644 --- a/app/config.py +++ b/app/config.py @@ -91,9 +91,7 @@ class Config(object): getenv("FEATURE_BEST_PRACTICES_ENABLED", "false") == "true" ) - FEATURE_ABOUT_PAGE_ENABLED = ( - getenv("FEATURE_ABOUT_PAGE_ENABLED", "false") == "true" - ) + FEATURE_ABOUT_PAGE_ENABLED = getenv("FEATURE_ABOUT_PAGE_ENABLED", "false") == "true" def _s3_credentials_from_env(bucket_prefix): diff --git a/app/main/views/index.py b/app/main/views/index.py index 5e7bff008..06f07d38c 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -1,11 +1,15 @@ -import os -import secrets -from urllib.parse import unquote - -from flask import abort, current_app, redirect, render_template, request, url_for +from flask import ( + abort, + current_app, + jsonify, + redirect, + render_template, + request, + url_for, +) from flask_login import current_user -from app import redis_client, status_api_client +from app import status_api_client from app.formatters import apply_html_class, convert_markdown_template from app.main import main from app.main.views.pricing import CURRENT_SMS_RATE @@ -16,55 +20,42 @@ from app.main.views.sub_navigation_dictionaries import ( using_notify_nav, ) from app.utils.user import user_is_logged_in -from notifications_utils.url_safe_token import generate_token # Hook to check for feature flags @main.before_request def check_feature_flags(): - if ( - request.path.startswith("/guides/best-practices") - and not current_app.config.get("FEATURE_BEST_PRACTICES_ENABLED", False) + if request.path.startswith("/guides") and not current_app.config.get( + "FEATURE_BEST_PRACTICES_ENABLED", False ): abort(404) - if ( - request.path.startswith("/about") - and not current_app.config.get("FEATURE_ABOUT_PAGE_ENABLED", False) + if request.path.startswith("/about") and not current_app.config.get( + "FEATURE_ABOUT_PAGE_ENABLED", False ): abort(404) +@main.route("/test/feature-flags") +def test_feature_flags(): + return jsonify( + { + "FEATURE_BEST_PRACTICES_ENABLED": current_app.config[ + "FEATURE_BEST_PRACTICES_ENABLED" + ] + } + ) + + @main.route("/") def index(): if current_user and current_user.is_authenticated: return redirect(url_for("main.choose_account")) - ttl = 24 * 60 * 60 - - # make and store the state - state = generate_token( - str(request.remote_addr), - current_app.config["SECRET_KEY"], - current_app.config["DANGEROUS_SALT"], - ) - state_key = f"login-state-{unquote(state)}" - redis_client.set(state_key, state, ex=ttl) - - # make and store the nonce - nonce = secrets.token_urlsafe() - nonce_key = f"login-nonce-{unquote(nonce)}" - redis_client.set(nonce_key, nonce, ex=ttl) - - url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL") - if url is not None: - url = url.replace("NONCE", nonce) - url = url.replace("STATE", state) return render_template( "views/signedout.html", sms_rate=CURRENT_SMS_RATE, - counts=status_api_client.get_count_of_live_services_and_organizations(), - initial_signin_url=url, + counts=status_api_client.get_count_of_live_services_and_organizations() ) @@ -216,61 +207,61 @@ def trial_mode_new(): @user_is_logged_in def best_practices(): return render_template( - "views/best-practices/best-practices.html", + "views/guides/best-practices.html", navigation_links=best_practices_nav(), ) -@main.route("/guides/best-practices/clear-goals") +@main.route("/guides/clear-goals") @user_is_logged_in def clear_goals(): return render_template( - "views/best-practices/clear-goals.html", + "views/guides/clear-goals.html", navigation_links=best_practices_nav(), ) -@main.route("/guides/best-practices/rules-and-regulations") +@main.route("/guides/rules-and-regulations") @user_is_logged_in def rules_and_regulations(): return render_template( - "views/best-practices/rules-and-regulations.html", + "views/guides/rules-and-regulations.html", navigation_links=best_practices_nav(), ) -@main.route("/guides/best-practices/establish-trust") +@main.route("/guides/establish-trust") @user_is_logged_in def establish_trust(): return render_template( - "views/best-practices/establish-trust.html", + "views/guides/establish-trust.html", navigation_links=best_practices_nav(), ) -@main.route("/guides/best-practices/write-for-action") +@main.route("/guides/write-for-action") @user_is_logged_in def write_for_action(): return render_template( - "views/best-practices/write-for-action.html", + "views/guides/write-for-action.html", navigation_links=best_practices_nav(), ) -@main.route("/guides/best-practices/multiple-languages") +@main.route("/guides/multiple-languages") @user_is_logged_in def multiple_languages(): return render_template( - "views/best-practices/multiple-languages.html", + "views/guides/multiple-languages.html", navigation_links=best_practices_nav(), ) -@main.route("/guides/best-practices/benchmark-performance") +@main.route("/guides/benchmark-performance") @user_is_logged_in def benchmark_performance(): return render_template( - "views/best-practices/benchmark-performance.html", + "views/guides/benchmark-performance.html", navigation_links=best_practices_nav(), ) @@ -287,6 +278,13 @@ def guidance_index(): ) +@main.route("/contact") +def contact(): + return render_template( + "views/about/contact.html", +) + + @main.route("/about") def about_notify(): return render_template( @@ -295,10 +293,18 @@ def about_notify(): ) -@main.route("/about/contact") -def contact(): +@main.route("/about/security") +def about_security(): return render_template( - "views/about/contact.html", + "views/about/security.html", + navigation_links=about_notify_nav(), + ) + + +@main.route("/about/why-text-messaging") +def why_text_messaging(): + return render_template( + "views/about/why-text-messaging.html", navigation_links=about_notify_nav(), ) diff --git a/app/main/views/sub_navigation_dictionaries.py b/app/main/views/sub_navigation_dictionaries.py index 56505ca6e..16991297a 100644 --- a/app/main/views/sub_navigation_dictionaries.py +++ b/app/main/views/sub_navigation_dictionaries.py @@ -1,3 +1,6 @@ +from flask import current_app + + def features_nav(): return [ { @@ -22,46 +25,20 @@ def features_nav(): def using_notify_nav(): - return [ - { - "name": "Get started", - "link": "main.get_started", - }, - { - "name": "Guides", - "link": "main.best_practices", - }, - { - "name": "Trial mode", - "link": "main.trial_mode_new", - }, - { - "name": "Tracking usage", - "link": "main.pricing", - }, - { - "name": "Delivery status", - "link": "main.message_status", - }, - { - "name": "Guidance", - "link": "main.guidance_index", - # "sub_navigation_items": [ - # { - # "name": "Formatting", - # "link": "main.edit_and_format_messages", - # }, - # { - # "name": "Send files by email", - # "link": "main.send_files_by_email", - # }, - # ] - # { - # "name": "API documentation", - # "link": "main.documentation", - # }, - }, + nav_items = [ + {"name": "Get started", "link": "main.get_started"}, + {"name": "Guides", "link": "main.best_practices"}, + {"name": "Trial mode", "link": "main.trial_mode_new"}, + {"name": "Tracking usage", "link": "main.pricing"}, + {"name": "Delivery Status", "link": "main.message_status"}, + {"name": "Guidance", "link": "main.guidance_index"}, ] + if not current_app.config.get("FEATURE_BEST_PRACTICES_ENABLED"): + nav_items = [ + item for item in nav_items if item["link"] != "main.best_practices" + ] + + return nav_items def best_practices_nav(): @@ -110,8 +87,32 @@ def best_practices_nav(): def about_notify_nav(): return [ { - "name": "About notify", + "name": "About Notify", "link": "main.about_notify", + "sub_navigation_items": [ + { + "name": "Why text messaging", + "link": "main.why_text_messaging", + "sub_sub_navigation_items": [ + { + "name": "Reach people using a common method", + "link": "main.why_text_messaging#reach-people-using-a-common-method", + }, + { + "name": "Improve customer experience", + "link": "main.why_text_messaging#improve-customer-experience", + }, + { + "name": "What texting is best for", + "link": "main.why_text_messaging#what-texting-is-best-for", + }, + ], + }, + { + "name": "Security", + "link": "main.about_security", + }, + ], }, { "name": "Contact", diff --git a/app/navigation.py b/app/navigation.py index a02df484d..271d6848b 100644 --- a/app/navigation.py +++ b/app/navigation.py @@ -53,7 +53,7 @@ class HeaderNavigation(Navigation): "establish_trust", "write_for_action", "multiple_languages", - "benchmark_performance" + "benchmark_performance", }, "using_notify": { "get_started", diff --git a/app/templates/base.html b/app/templates/base.html index 369e62b81..b2b6639e9 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -66,7 +66,20 @@
diff --git a/app/templates/views/about/about.html b/app/templates/views/about/about.html
index 39dfce671..17180cef7 100644
--- a/app/templates/views/about/about.html
+++ b/app/templates/views/about/about.html
@@ -1,13 +1,12 @@
{% extends "base.html" %}
-{% set page_title = "About notify" %}
+{% set page_title = "About Notify" %}
{% block per_page_title %}
{{page_title}}
{% endblock %}
{% block content_column_content %}
-
Notify.gov is a text messaging service built by and for the government. We help agencies communicate more
@@ -17,8 +16,9 @@
Notify.gov is an easy-to-use, web-based platform. It requires no technical expertise or system integration — users
can create an account and get started within minutes. We take the security and privacy of messaging data seriously
@@ -56,9 +56,9 @@
{{page_title}}