mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-18 05:29:38 -04:00
Merge branch 'main' of https://github.com/GSA/notifications-admin into 810-hide-personalization-after-sending
This commit is contained in:
@@ -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__(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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))
|
||||
|
||||
13
app/templates/components/components/select/README.md
Normal file
13
app/templates/components/components/select/README.md
Normal 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.
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
3
app/templates/components/components/select/macro.njk
Normal file
3
app/templates/components/components/select/macro.njk
Normal file
@@ -0,0 +1,3 @@
|
||||
{% macro usaSelect(params) %}
|
||||
{%- include "./template.njk" -%}
|
||||
{% endmacro %}
|
||||
15
app/templates/components/components/select/template.njk
Normal file
15
app/templates/components/components/select/template.njk
Normal 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>
|
||||
@@ -4,6 +4,7 @@
|
||||
<div class="grid-row">
|
||||
<div class="grid-col-8">
|
||||
<h1>You’re 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
|
||||
|
||||
77
app/templates/views/set-up-your-profile.html
Normal file
77
app/templates/views/set-up-your-profile.html
Normal 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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user