Skip to content

Commit

Permalink
Merge pull request #1525 from GSA/main
Browse files Browse the repository at this point in the history
Production deploy 5/10/2024
  • Loading branch information
stvnrlly authored May 10, 2024
2 parents 226906f + b6b0909 commit 84ece7d
Show file tree
Hide file tree
Showing 19 changed files with 640 additions and 938 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,30 @@ This will run the local development web server and make the admin site
available at http://localhost:6012; remember to make sure that the Notify.gov
API is running as well!

## Creating a 'First User' in the database

After you have completed all setup steps, you will be unable to log in, because there
will not be a user in the database to link to the login.gov account you are using. So
you will need to create that user in your database using the 'create-test-user' command.

Open two terminals pointing to the api project and then run these commands in the
respective terminals.

(Server 1)
env ALLOW_EXPIRED_API_TOKEN=1 make run-flask

(Server 2)
poetry run flask command create-admin-jwt | tail -n 1 | pbcopy
poetry run flask command create-test-user --admin=True;

Supply your name, email address, mobile number, and password when prompted. Make sure the email address
is the same one you are using in login.gov and make sure your phone number is in the format 5555555555.

If for any reason in the course of development it is necessary for your to delete your db
via the `dropdb` command, you will need to repeat these steps when you recreate your db.



## Git Hooks

We're using [`pre-commit`](https://pre-commit.com/) to manage hooks in order to
Expand Down
3 changes: 2 additions & 1 deletion app/formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
import markdown
import pytz
from bs4 import BeautifulSoup
from flask import Markup, render_template_string, url_for
from flask import render_template_string, url_for
from flask.helpers import get_root_path
from markupsafe import Markup
from notifications_utils.field import Field
from notifications_utils.formatters import make_quotes_smart
from notifications_utils.formatters import nl2br as utils_nl2br
Expand Down
3 changes: 2 additions & 1 deletion app/main/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
from numbers import Number

import pytz
from flask import Markup, render_template, request
from flask import render_template, request
from flask_login import current_user
from flask_wtf import FlaskForm as Form
from flask_wtf.file import FileAllowed
from flask_wtf.file import FileField as FileField_wtf
from flask_wtf.file import FileSize
from markupsafe import Markup
from notifications_utils.formatters import strip_all_whitespace
from notifications_utils.insensitive_dict import InsensitiveDict
from notifications_utils.recipients import InvalidPhoneError, validate_phone_number
Expand Down
3 changes: 2 additions & 1 deletion app/main/views/api_keys.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from flask import Markup, abort, flash, redirect, render_template, request, url_for
from flask import abort, flash, redirect, render_template, request, url_for
from flask_login import current_user
from markupsafe import Markup

from app import (
api_key_api_client,
Expand Down
2 changes: 1 addition & 1 deletion app/main/views/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from functools import partial

from flask import (
Markup,
Response,
abort,
jsonify,
Expand All @@ -15,6 +14,7 @@
url_for,
)
from flask_login import current_user
from markupsafe import Markup
from notifications_utils.template import EmailPreviewTemplate, SMSBodyPreviewTemplate

from app import (
Expand Down
119 changes: 58 additions & 61 deletions app/main/views/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,11 @@
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.service import Service
from app.models.user import InvitedOrgUser, InvitedUser, User
from app.utils import hide_from_search_engines, hilite

Expand All @@ -44,35 +42,10 @@ def register():
return render_template("views/register.html", form=form)


@main.route("/register-from-invite", methods=["GET", "POST"])
def register_from_invite():
invited_user = InvitedUser.from_session()
if not invited_user:
abort(404)

form = RegisterUserFromInviteForm(invited_user)

if form.validate_on_submit():
if (
form.service.data != invited_user.service
or form.email_address.data != invited_user.email_address
):
abort(400)
_do_registration(form, send_email=False, send_sms=invited_user.sms_auth)
invited_user.accept_invite()
if invited_user.sms_auth:
return redirect(url_for("main.verify"))
else:
# we've already proven this user has email because they clicked the invite link,
# so just activate them straight away
return activate_user(session["user_details"]["id"])

return render_template(
"views/register-from-invite.html", invited_user=invited_user, form=form
)


@main.route("/register-from-org-invite", methods=["GET", "POST"])
# TODO This is deprecated, we are now handling invites in the
# login.gov workflow. Leaving it here until we write the new
# org registration.
def register_from_org_invite():
invited_org_user = InvitedOrgUser.from_session()
if not invited_org_user:
Expand Down Expand Up @@ -152,38 +125,62 @@ def set_up_your_profile():
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)

invite_data = state.encode("utf8")
invite_data = base64.b64decode(invite_data)
invite_data = json.loads(invite_data)
invited_service = Service.from_id(invite_data["service_id"])
invited_user_id = invite_data["invited_user_id"]
invited_user = InvitedUser.by_id(invited_user_id)

if user_email.lower() != invited_user.email_address.lower():
flash("You cannot accept an invite for another person.")
session.pop("invited_user_id", None)
abort(403)
else:
invited_user.accept_invite()
current_app.logger.debug(
hilite(
f"INVITED USER {invited_user.email_address} to service {invited_service.name}"
)
)
current_app.logger.debug(hilite("ACCEPTED INVITE"))

return _handle_login_dot_gov_invite(code, state, form)
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
return render_template("views/set-up-your-profile.html", form=form)


def get_invited_user_email_address(invited_user_id):
# InvitedUser is an unhashable type and hard to mock in tests
# so this convenience method is a workaround for that
invited_user = InvitedUser.by_id(invited_user_id)
return invited_user.email_address


def invited_user_accept_invite(invited_user_id):
# InvitedUser is an unhashable type and hard to mock in tests
# so this convenience method is a workaround for that
invited_user = InvitedUser.by_id(invited_user_id)
invited_user.accept_invite()


def debug_msg(msg):
current_app.logger.debug(hilite(msg))


def _handle_login_dot_gov_invite(code, state, form):
debug_msg(f"enter _handle_login_dot_gov_invite with code {code} state {state}")
access_token = sign_in._get_access_token(code, state)
debug_msg("Got the access token for login.gov")
user_email, user_uuid = sign_in._get_user_email_and_uuid(access_token)
debug_msg(
f"Got the user_email {user_email} and user_uuid {user_uuid} from login.gov"
)
debug_msg(f"raw state {state}")
invite_data = state.encode("utf8")
debug_msg(f"utf8 encoded state {invite_data}")
invite_data = base64.b64decode(invite_data)
debug_msg(f"b64 decoded state {invite_data}")
invite_data = json.loads(invite_data)
debug_msg(f"final state {invite_data}")
invited_user_id = invite_data["invited_user_id"]
invited_user_email_address = get_invited_user_email_address(invited_user_id)
debug_msg(f"email address from the invite_date is {invited_user_email_address}")
if user_email.lower() != invited_user_email_address.lower():
debug_msg("invited user email did not match expected email, abort(403)")
flash("You cannot accept an invite for another person.")
session.pop("invited_user_id", None)
abort(403)
else:
invited_user_accept_invite(invited_user_id)
debug_msg(
f"invited user {invited_user_email_address} to service {invite_data['service_id']}"
)
debug_msg("accepted invite")
user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email)
if user is None:
user = User.register(
Expand All @@ -193,20 +190,20 @@ def set_up_your_profile():
password=str(uuid.uuid4()),
auth_type="sms_auth",
)
debug_msg(f"registered user {form.name.data} with email {user_email}")

# activate the user
user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email)
activate_user(user["id"])
debug_msg("activated user")
usr = User.from_id(user["id"])
usr.add_to_service(
invited_service.id,
invite_data["service_id"],
invite_data["permissions"],
invite_data["folder_permissions"],
invite_data["from_user_id"],
)
current_app.logger.debug(
hilite(f"Added user {usr.email_address} to service {invited_service.name}")
debug_msg(
f"Added user {usr.email_address} to service {invite_data['service_id']}"
)
return redirect(url_for("main.show_accounts_or_dashboard"))

return render_template("views/set-up-your-profile.html", form=form)
2 changes: 0 additions & 2 deletions app/main/views/send.py
Original file line number Diff line number Diff line change
Expand Up @@ -1006,8 +1006,6 @@ def send_notification(service_id, template_id):
".view_job",
service_id=service_id,
job_id=upload_id,
from_job=upload_id,
notification_id=notifications["notifications"][0]["id"],
# used to show the final step of the tour (help=3) or not show
# a back link on a just sent one off notification (help=0)
help=request.args.get("help"),
Expand Down
88 changes: 13 additions & 75 deletions app/main/views/sign_in.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,15 @@

import jwt
import requests
from flask import (
Markup,
Response,
abort,
current_app,
flash,
redirect,
render_template,
request,
session,
url_for,
)
from flask import Response, current_app, redirect, render_template, request, url_for
from flask_login import current_user
from notifications_utils.url_safe_token import generate_token

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.models.user import User
from app.utils import hide_from_search_engines
from app.utils.login import is_safe_redirect_url
from app.utils.time import is_less_than_days_ago
Expand Down Expand Up @@ -129,6 +117,16 @@ def verify_email(user, redirect_url):
)


def _handle_e2e_tests(redirect_url):
current_app.logger.warning("E2E TESTS ARE ENABLED.")
current_app.logger.warning(
"If you are getting a 404 on signin, comment out E2E vars in .env file!"
)
user = user_api_client.get_user_by_email(os.getenv("NOTIFY_E2E_TEST_EMAIL"))
activate_user(user["id"])
return redirect(url_for("main.show_accounts_or_dashboard", next=redirect_url))


@main.route("/sign-in", methods=(["GET", "POST"]))
@hide_from_search_engines
def sign_in():
Expand All @@ -146,70 +144,13 @@ def sign_in():
redirect_url = request.args.get("next")

if os.getenv("NOTIFY_E2E_TEST_EMAIL"):
current_app.logger.warning("E2E TESTS ARE ENABLED.")
current_app.logger.warning(
"If you are getting a 404 on signin, comment out E2E vars in .env file!"
)
user = user_api_client.get_user_by_email(os.getenv("NOTIFY_E2E_TEST_EMAIL"))
activate_user(user["id"])
return redirect(url_for("main.show_accounts_or_dashboard", next=redirect_url))
return _handle_e2e_tests(redirect_url)

current_app.logger.info(f"current user is {current_user}")
if current_user and current_user.is_authenticated:
if redirect_url and is_safe_redirect_url(redirect_url):
return redirect(redirect_url)
return redirect(url_for("main.show_accounts_or_dashboard"))

form = LoginForm()
current_app.logger.info("Got the login form")
password_reset_url = url_for(".forgot_password", next=request.args.get("next"))

if form.validate_on_submit():
user = User.from_email_address_and_password_or_none(
form.email_address.data, form.password.data
)

if user:
# add user to session to mark us as in the process of signing the user in
session["user_details"] = {"email": user.email_address, "id": user.id}

if user.state == "pending":
return redirect(
url_for("main.resend_email_verification", next=redirect_url)
)

if user.is_active:
if session.get("invited_user_id"):
invited_user = InvitedUser.from_session()
if user.email_address.lower() != invited_user.email_address.lower():
flash("You cannot accept an invite for another person.")
session.pop("invited_user_id", None)
abort(403)
else:
invited_user.accept_invite()

user.send_login_code()

if user.sms_auth:
return redirect(url_for(".two_factor_sms", next=redirect_url))

if user.email_auth:
return redirect(
url_for(".two_factor_email_sent", next=redirect_url)
)

# Vague error message for login in case of user not known, locked, inactive or password not verified
flash(
Markup(
(
f"The email address or password you entered is incorrect."
f"&ensp;<a href={password_reset_url} class='usa-link'>Forgot your password?</a>"
)
)
)

other_device = current_user.logged_in_elsewhere()

token = generate_token(
str(request.remote_addr),
current_app.config["SECRET_KEY"],
Expand All @@ -222,10 +163,7 @@ def sign_in():
url = url.replace("STATE", token)
return render_template(
"views/signin.html",
form=form,
again=bool(redirect_url),
other_device=other_device,
password_reset_url=password_reset_url,
initial_signin_url=url,
)

Expand Down
Loading

0 comments on commit 84ece7d

Please sign in to comment.