Merge branch 'main' of https://github.com/GSA/notifications-admin into 810-hide-personalization-after-sending

This commit is contained in:
Jonathan Bobel
2024-03-26 11:23:52 -04:00
15 changed files with 287 additions and 45 deletions

View File

@@ -605,6 +605,15 @@ class RegisterUserForm(StripWhitespaceForm):
auth_type = HiddenField("auth_type", default="sms_auth")
class SetupUserProfileForm(StripWhitespaceForm):
name = GovukTextInputField(
"Full name", validators=[DataRequired(message="Cannot be empty")]
)
mobile_number = international_phone_number()
# TODO This should be replaced with a select widget when one is available.
preferred_timezone = HiddenField("preferred_timezone", default="US/Eastern")
class RegisterUserFromInviteForm(RegisterUserForm):
def __init__(self, invited_user):
super().__init__(

View File

@@ -1,3 +1,5 @@
import os
from flask import abort, redirect, render_template, request, url_for
from flask_login import current_user
@@ -8,6 +10,8 @@ from app.main.views.pricing import CURRENT_SMS_RATE
from app.main.views.sub_navigation_dictionaries import features_nav, using_notify_nav
from app.utils.user import user_is_logged_in
login_dot_gov_url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL")
@main.route("/")
def index():
@@ -18,6 +22,7 @@ def index():
"views/signedout.html",
sms_rate=CURRENT_SMS_RATE,
counts=status_api_client.get_count_of_live_services_and_organizations(),
login_dot_gov_url=login_dot_gov_url,
)

View File

@@ -1,14 +1,26 @@
import uuid
from datetime import datetime, timedelta
from flask import abort, redirect, render_template, session, url_for
from flask import (
abort,
current_app,
redirect,
render_template,
request,
session,
url_for,
)
from flask_login import current_user
from app import user_api_client
from app.main import main
from app.main.forms import (
RegisterUserForm,
RegisterUserFromInviteForm,
RegisterUserFromOrgInviteForm,
SetupUserProfileForm,
)
from app.main.views import sign_in
from app.main.views.verify import activate_user
from app.models.user import InvitedOrgUser, InvitedUser, User
from app.utils import hide_from_search_engines
@@ -120,4 +132,44 @@ def _do_registration(form, send_sms=True, send_email=True, organization_id=None)
def registration_continue():
if not session.get("user_details"):
return redirect(url_for(".show_accounts_or_dashboard"))
return render_template("views/registration-continue.html")
else:
raise Exception("Unexpected routing in registration_continue")
@main.route("/set-up-your-profile", methods=["GET", "POST"])
@hide_from_search_engines
def set_up_your_profile():
form = SetupUserProfileForm()
if form.validate_on_submit():
# start login.gov
code = request.args.get("code")
state = request.args.get("state")
login_gov_error = request.args.get("error")
if code and state:
access_token = sign_in._get_access_token(code, state)
user_email, user_uuid = sign_in._get_user_email_and_uuid(access_token)
redirect_url = request.args.get("next")
elif login_gov_error:
current_app.logger.error(f"login.gov error: {login_gov_error}")
raise Exception(f"Could not login with login.gov {login_gov_error}")
# end login.gov
# create the user
# TODO we have to provide something for password until that column goes away
# TODO ideally we would set the user's preferred timezone here as well
user = User.register(
name=form.name.data,
email_address=user_email,
mobile_number=form.mobile_number.data,
password=str(uuid.uuid4()),
auth_type="sms_auth",
)
# activate the user
user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email)
activate_user(user["id"])
return redirect(url_for("main.show_accounts_or_dashboard", next=redirect_url))
return render_template("views/set-up-your-profile.html", form=form)

View File

@@ -20,6 +20,7 @@ from flask_login import current_user
from app import login_manager, user_api_client
from app.main import main
from app.main.forms import LoginForm
from app.main.views.index import error
from app.main.views.verify import activate_user
from app.models.user import InvitedUser, User
from app.utils import hide_from_search_engines
@@ -77,9 +78,7 @@ def _get_user_email_and_uuid(access_token):
return user_email, user_uuid
@main.route("/sign-in", methods=(["GET", "POST"]))
@hide_from_search_engines
def sign_in():
def _do_login_dot_gov():
# start login.gov
code = request.args.get("code")
state = request.args.get("state")
@@ -90,8 +89,13 @@ def sign_in():
redirect_url = request.args.get("next")
# activate the user
user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email)
activate_user(user["id"])
try:
user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email)
activate_user(user["id"])
except BaseException as be: # noqa B036
current_app.logger.error(be)
error(401)
return redirect(url_for("main.show_accounts_or_dashboard", next=redirect_url))
elif login_gov_error:
@@ -99,6 +103,11 @@ def sign_in():
raise Exception(f"Could not login with login.gov {login_gov_error}")
# end login.gov
@main.route("/sign-in", methods=(["GET", "POST"]))
@hide_from_search_engines
def sign_in():
_do_login_dot_gov()
redirect_url = request.args.get("next")
if os.getenv("NOTIFY_E2E_TEST_EMAIL"):
@@ -165,31 +174,8 @@ def sign_in():
)
other_device = current_user.logged_in_elsewhere()
notify_env = os.getenv("NOTIFY_ENVIRONMENT")
current_app.logger.info("should render the sign in template")
# TODO REMOVE THIS INFO ONCE STAGING WORKS WITH LOGIN DOT GOV
current_app.logger.info(f"NOTIFY ENV = {notify_env}")
current_app.logger.info(
f"LOGIN_DOT_GOV_CLIENT_ID={os.getenv('LOGIN_DOT_GOV_CLIENT_ID')}"
)
current_app.logger.info(
f"LOGIN_DOT_GOV_USER_INFO_URL={os.getenv('LOGIN_DOT_GOV_USER_INFO_URL')}"
)
current_app.logger.info(
f"LOGIN_DOT_GOV_ACCESS_TOKEN_URL={os.getenv('LOGIN_DOT_GOV_ACCESS_TOKEN_URL')}"
)
current_app.logger.info(
f"LOGIN_DOT_GOV_LOGOUT_URL={os.getenv('LOGIN_DOT_GOV_LOGOUT_URL')}"
)
current_app.logger.info(
f"LOGIN_DOT_GOV_BASE_LOGOUT_URL={os.getenv('LOGIN_DOT_GOV_BASE_LOGOUT_URL')}"
)
current_app.logger.info(
f"LOGIN_DOT_GOV_SIGNOUT_REDIRECT={os.getenv('LOGIN_DOT_GOV_SIGNOUT_REDIRECT')}"
)
initial_signin_url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL")
current_app.logger.info(f"LOGIN_DOT_GOV_INITIAL_SIGNIN_URL={initial_signin_url}")
return render_template(
"views/signin.html",

View File

@@ -6,10 +6,6 @@ from flask_login import current_user
from app.main import main
# ask login.gov if we really need manual logout and what's up with one hour sessions
# ask login.gov how they recommend approaching dev environment
# ask Tim Donaworth the same for #2
def _sign_out_at_login_dot_gov():
base_url = os.getenv("LOGIN_DOT_GOV_BASE_LOGOUT_URL")

View File

@@ -5,6 +5,7 @@ from itsdangerous import SignatureExpired
from notifications_utils.url_safe_token import check_token
from app import user_api_client
from app.extensions import redis_client
from app.main import main
from app.main.forms import TwoFactorForm
from app.models.user import InvitedOrgUser, InvitedUser, User
@@ -64,20 +65,44 @@ def verify_email(token):
def activate_user(user_id):
user = User.from_id(user_id)
# the user will have a new current_session_id set by the API - store it in the cookie for future requests
# This is the login.gov path
login_gov_invite_data = redis_client.get(f"service-invite-{user.email_address}")
if login_gov_invite_data:
login_gov_invite_data = json.loads(login_gov_invite_data.decode("utf8"))
# This is the deprecated path for organization invites where we get id from session
session["current_session_id"] = user.current_session_id
organization_id = session.get("organization_id")
activated_user = user.activate()
activated_user.login()
# TODO when login.gov is mandatory, get rid of the if clause, it is deprecated.
invited_user = InvitedUser.from_session()
if invited_user:
service_id = _add_invited_user_to_service(invited_user)
return redirect(url_for("main.service_dashboard", service_id=service_id))
elif login_gov_invite_data:
service_id = login_gov_invite_data["service_id"]
user.add_to_service(
service_id,
login_gov_invite_data["permissions"],
login_gov_invite_data["folder_permissions"],
login_gov_invite_data["from_user_id"],
)
return redirect(url_for("main.service_dashboard", service_id=service_id))
# TODO when login.gov is mandatory, git rid of the if clause, it is deprecated.
invited_org_user = InvitedOrgUser.from_session()
if invited_org_user:
user_api_client.add_user_to_organization(invited_org_user.organization, user_id)
elif redis_client.get(f"organization-invite-{user.email_address}"):
organization_id = redis_client.get(f"organization-invite-{user.email_address}")
user_api_client.add_user_to_organization(
organization_id.decode("utf8"), user_id
)
if organization_id:
return redirect(url_for("main.organization_dashboard", org_id=organization_id))

View File

@@ -0,0 +1,13 @@
# Select
## Installation
See the [main README quick start guide](https://github.com/alphagov/govuk-frontend#quick-start) for how to install this component.
## Guidance and Examples
Find out when to use the select component in your service in the [GOV.UK Design System](https://design-system.service.gov.uk/components/select/).
## Component options
Use options to customize the appearance, content and behavior of a component when using a macro, for example, changing the text.

View File

@@ -0,0 +1,58 @@
[
{
"name": "id",
"type": "string",
"required": true,
"description": "ID for each select box."
},
{
"name": "name",
"type": "string",
"required": true,
"description": "Name property for the select."
},
{
"name": "items",
"type": "array",
"required": true,
"description": "The items within the select component."
},
{
"name": "value",
"type": "string",
"required": false,
"description": "Value for the option which should be selected. Use this as an alternative to setting the selected option on each individual item."
},
{
"name": "disabled",
"type": "boolean",
"required": false,
"description": "If true, select box will be disabled. Use the disabled option on each individual item to only disable certain options."
},
{
"name": "describedBy",
"type": "string",
"required": false,
"description": "One or more element IDs to add to the `aria-describedby` attribute, used to provide additional descriptive information for screenreader users."
},
{
"name": "label",
"type": "object",
"required": true,
"description": "The label used by the select component.",
"isComponent": true
},
{
"name": "hint",
"type": "object",
"required": false,
"description": "Can be used to add a hint to the select component.",
"isComponent": true
},
{
"name": "classes",
"type": "string",
"required": false,
"description": "Classes to add to the select."
}
]

View File

@@ -0,0 +1,3 @@
{% macro usaSelect(params) %}
{%- include "./template.njk" -%}
{% endmacro %}

View File

@@ -0,0 +1,15 @@
{% set describedBy = params.describedBy if params.describedBy else "" %}
<div class="usa-form-group">
<label class="usa-label" for="{{ params.name }}">{{params.label}}</label>
{% if params.hint %}
<div class="usa-hint" id="{{ params.describedBy }}">{{ params.hint }}</div>
{% endif %}
<select class="usa-select {%- if params.classes %} {{ params.classes }}{% endif %}" id="{{ params.id }}" name="{{ params.name }}"
{%- if params.value %} value="{{ params.value}}"{% endif %}
{%- if describedBy %} aria-describedby="{{ describedBy }}"{% endif %}
{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor -%}>
{% for item in params.items %}
<option value="{{ item.value }}" {%- if item.disabled == true %} selected disabled {% endif %}>{{ item.text }}</option>
{% endfor %}
</select>
</div>

View File

@@ -4,6 +4,7 @@
<div class="grid-row">
<div class="grid-col-8">
<h1>Youre not authorized to see this page</h1>
<p class="usa-body"><a class="usa-link" href="{{ url_for('main.sign_in' )}}">Sign in</a> to Notify.gov and try again.</p>
<p class="usa-body">If you have been invited to join Notify.gov, <a class="usa-link" href="{{ url_for('main.sign_in' )}}">sign in</a> to Notify.gov using your Login.gov account and try again.</p>
</div>
{% endblock %}
e

View File

@@ -0,0 +1,77 @@
{% extends "withoutnav_template.html" %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
{% from "components/components/select/macro.njk" import usaSelect -%}
{% block per_page_title %}
Set up your profile
{% endblock %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="grid-col-8">
<h1 class="font-body-2xl margin-bottom-3">Set up your profile</h1>
{% call form_wrapper(autocomplete=True) %}
{{ form.name(param_extensions={}) }}
<div class="extra-tracking">
{{ form.mobile_number(param_extensions={
"hint": {"text": "We'll send you a security code by text message"},
}) }}
</div>
<!--{{ usaSelect({
"id": "time-zone",
"name": "time-zone",
"label": "Time zone",
"items": [
{
"value": "",
"disabled": true,
"text": "- Select -"
},
{
"value": "America/Puerto_Rico",
"text": "America/Puerto_Rico"
},
{
"value": "US/Eastern",
"text": "US/Eastern"
},
{
"value": "US/Central",
"text": "US/Central"
},
{
"value": "US/Mountain",
"text": "US/Mountain"
},
{
"value": "US/Pacific",
"text": "US/Pacific"
},
{
"value": "US/Alaska",
"text": "US/Alaska"
},
{
"value": "US/Hawaii",
"text": "US/Hawaii"
},
{
"value": "US/Aleutian",
"text": "US/Aleutian"
},
{
"value": "US/Samoa",
"text": "US/Samoa"
},
]
})
}}-->
{{form.auth_type}}
{{ page_footer("Save") }}
{% endcall %}
</div>
</div>
{% endblock %}

View File

@@ -21,7 +21,7 @@ Notify.gov
<h1 class="font-serif-2xl usa-hero__heading">Reach people where they are with government-powered text messages</h1>
<p class="font-sans-lg">Notify.gov is a text message service that helps federal, state, local, tribal and territorial governments more effectively communicate with the people they serve.</p>
<div class="usa-button-group margin-bottom-5">
<a class="usa-button usa-button--big margin-right-2" href="{{ url_for('main.sign_in' )}}">Sign in</a>
<a class="usa-button usa-button--big margin-right-2" href="{{ url_for('main.sign_in' ) }}">Sign in</a>
if you are an existing pilot partner
</div>
<p class="font-sans-md">Currently we are only working with select pilot partners. If you are interested in using Notify.gov in the future, please contact <br><a href="mailto:tts-benefits-studio@gsa.gov">tts-benefits-studio@gsa.gov</a> to learn more.</p>

View File

@@ -30,6 +30,7 @@ def test_logged_in_user_redirects_to_account(
)
@pytest.mark.skip("Deprecated due to change to login-dot-gov-only registration")
@pytest.mark.parametrize(
"phone_number_to_register_with",
[
@@ -75,14 +76,14 @@ def test_register_creates_new_user_and_redirects_to_continue_page(
== "An email has been sent to notfound@example.gsa.gov."
)
mock_send_verify_email.assert_called_with(ANY, user_data["email_address"])
mock_register_user.assert_called_with(
user_data["name"],
user_data["email_address"],
user_data["mobile_number"],
user_data["password"],
user_data["auth_type"],
)
# mock_send_verify_email.assert_called_with(ANY, user_data["email_address"])
# mock_register_user.assert_called_with(
# user_data["name"],
# user_data["email_address"],
# user_data["mobile_number"],
# user_data["password"],
# user_data["auth_type"],
# )
def test_register_continue_handles_missing_session_sensibly(

View File

@@ -166,6 +166,7 @@ EXCLUDED_ENDPOINTS = tuple(
"send_one_off",
"send_one_off_step",
"send_one_off_to_myself",
"set_up_your_profile",
"service_add_email_reply_to",
"service_add_sms_sender",
"service_confirm_delete_email_reply_to",