diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 8e91c4cfe..a76948d77 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -163,7 +163,7 @@ jobs: - uses: ./.github/actions/setup-project - name: Create requirements.txt run: poetry export --without-hashes --format=requirements.txt > requirements.txt - - uses: pypa/gh-action-pip-audit@v1.0.6 + - uses: pypa/gh-action-pip-audit@v1.0.8 with: inputs: requirements.txt - name: Run npm audit diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py index 4d5aca14d..8453ef369 100644 --- a/app/main/views/dashboard.py +++ b/app/main/views/dashboard.py @@ -134,11 +134,17 @@ def template_usage(service_id): months=months, stats=stats, most_used_template_count=max( - max( - (template["requested_count"] for template in month["templates_used"]), - default=0, - ) - for month in months + ( + max( + ( + template["requested_count"] + for template in month["templates_used"] + ), + default=0, + ) + for month in months + ), + default=0, ), years=get_tuples_of_financial_years( partial(url_for, ".template_usage", service_id=service_id), @@ -155,31 +161,16 @@ def usage(service_id): year, current_financial_year = requested_and_current_financial_year(request) free_sms_allowance = billing_api_client.get_free_sms_fragment_limit_for_year( - service_id, year + service_id ) + units = billing_api_client.get_monthly_usage_for_service(service_id, year) + yearly_usage = billing_api_client.get_annual_usage_for_service(service_id, year) more_stats = format_monthly_stats_to_list( service_api_client.get_monthly_notification_stats(service_id, year)["data"] ) - if year == current_financial_year: - # This includes Oct, Nov, Dec - # but we don't need next year's data yet - more_stats = [ - month - for month in more_stats - if month["name"] in ["October", "November", "December"] - ] - elif year == (current_financial_year + 1): - # This is all the other months - # and we need last year's data - more_stats = [ - month - for month in more_stats - if month["name"] not in ["October", "November", "December"] - ] - return render_template( "views/usage.html", months=list(get_monthly_usage_breakdown(year, units, more_stats)), @@ -341,8 +332,15 @@ def get_dashboard_partials(service_id): dashboard_totals = (get_dashboard_totals(stats),) free_sms_allowance = billing_api_client.get_free_sms_fragment_limit_for_year( current_service.id, - get_current_financial_year(), ) + # These 2 calls will update the dashboard sms allowance count while in trial mode. + billing_api_client.get_monthly_usage_for_service( + service_id, get_current_financial_year() + ) + billing_api_client.create_or_update_free_sms_fragment_limit( + service_id, free_sms_fragment_limit=free_sms_allowance + ) + yearly_usage = billing_api_client.get_annual_usage_for_service( service_id, get_current_financial_year(), @@ -433,13 +431,7 @@ def aggregate_status_types(counts_dict): def get_months_for_financial_year(year, time_format="%B"): - return [ - month.strftime(time_format) - for month in ( - get_months_for_year(10, 13, year) + get_months_for_year(1, 10, year + 1) - ) - if month < datetime.now() - ] + return [month.strftime(time_format) for month in (get_months_for_year(1, 13, year))] def get_months_for_year(start, end, year): diff --git a/app/main/views/register.py b/app/main/views/register.py index e6350c8f7..ed0b60f41 100644 --- a/app/main/views/register.py +++ b/app/main/views/register.py @@ -1,9 +1,12 @@ +import base64 +import json import uuid from datetime import datetime, timedelta from flask import ( abort, current_app, + flash, redirect, render_template, request, @@ -22,8 +25,9 @@ from app.main.forms import ( ) 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 +from app.utils import hide_from_search_engines, hilite @main.route("/register", methods=["GET", "POST"]) @@ -150,7 +154,26 @@ def set_up_your_profile(): 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") + + 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")) elif login_gov_error: current_app.logger.error(f"login.gov error: {login_gov_error}") @@ -174,5 +197,16 @@ def set_up_your_profile(): # 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)) + usr = User.from_id(user["id"]) + usr.add_to_service( + invited_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}") + ) + return redirect(url_for("main.show_accounts_or_dashboard")) + return render_template("views/set-up-your-profile.html", form=form) diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py index 6de46b023..2a205c9aa 100644 --- a/app/main/views/sign_in.py +++ b/app/main/views/sign_in.py @@ -6,6 +6,7 @@ import jwt import requests from flask import ( Markup, + Response, abort, current_app, flash, @@ -26,6 +27,7 @@ from app.main.views.verify import activate_user from app.models.user import InvitedUser, 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 def _reformat_keystring(orig): @@ -100,20 +102,47 @@ def _do_login_dot_gov(): user_email, user_uuid = _get_user_email_and_uuid(access_token) redirect_url = request.args.get("next") user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email) - activate_user(user["id"]) + + # Check if the email needs to be revalidated + is_fresh_email = is_less_than_days_ago( + user["email_access_validated_at"], 90 + ) + if not is_fresh_email: + return verify_email(user, redirect_url) + + usr = User.from_email_address(user["email_address"]) + activate_user(usr.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)) # end login.gov +def verify_email(user, redirect_url): + user_api_client.send_verify_code(user["id"], "email", None, redirect_url) + title = "Email resent" if request.args.get("email_resent") else "Check your email" + redirect_url = request.args.get("next") + return render_template( + "views/re-validate-email-sent.html", title=title, redirect_url=redirect_url + ) + + @main.route("/sign-in", methods=(["GET", "POST"])) @hide_from_search_engines def sign_in(): - _do_login_dot_gov() + # If we have to revalidated the email, send the message + # via email and redirect to the "verify your email page" + # and don't proceed further with login + email_verify_template = _do_login_dot_gov() + if ( + email_verify_template + and not isinstance(email_verify_template, Response) + and "Check your email" in email_verify_template + ): + return email_verify_template + redirect_url = request.args.get("next") if os.getenv("NOTIFY_E2E_TEST_EMAIL"): @@ -191,13 +220,11 @@ def sign_in(): if url is not None: url = url.replace("NONCE", token) url = url.replace("STATE", token) - return render_template( "views/signin.html", form=form, again=bool(redirect_url), other_device=other_device, - login_gov_enabled=True, password_reset_url=password_reset_url, initial_signin_url=url, ) diff --git a/app/main/views/verify.py b/app/main/views/verify.py index d87c9c6dc..114e95ddd 100644 --- a/app/main/views/verify.py +++ b/app/main/views/verify.py @@ -5,10 +5,9 @@ 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 +from app.models.user import User from app.utils.login import redirect_to_sign_in @@ -66,59 +65,19 @@ def verify_email(token): def activate_user(user_id): user = User.from_id(user_id) - # 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.raw_get( - f"organization-invite-{user.email_address}" - ) - user_api_client.add_user_to_organization( - organization_id.decode("utf8"), user_id - ) + # TODO add org invites back in the new way + # organization_id = redis_client.raw_get( + # f"organization-invite-{user.email_address}" + # ) + # user_api_client.add_user_to_organization( + # organization_id.decode("utf8"), user_id + # ) + organization_id = None if organization_id: return redirect(url_for("main.organization_dashboard", org_id=organization_id)) else: + activated_user = user.activate() + activated_user.login() + return redirect(url_for("main.add_service", first="first")) - - -def _add_invited_user_to_service(invitation): - user = User.from_id(session["user_id"]) - service_id = invitation.service - user.add_to_service( - service_id, - invitation.permissions, - invitation.folder_permissions, - invitation.from_user.id, - ) - return service_id diff --git a/app/notify_client/service_api_client.py b/app/notify_client/service_api_client.py index fe1a6aff5..d34516b8b 100644 --- a/app/notify_client/service_api_client.py +++ b/app/notify_client/service_api_client.py @@ -497,5 +497,18 @@ class ServiceAPIClient(NotifyAdminAPIClient): def get_global_notification_count(self, service_id): return self.get("/service/{}/notification-count".format(service_id)) + def get_service_invite_data(self, redis_key): + """ + Retrieve service invite_data. + """ + return self.get("/service/invite/redis/{0}".format(redis_key)) + service_api_client = ServiceAPIClient() + + +# TODO, if we try to call get_service_invite_data directly +# from verify, app complains the method is not defined +# If we wrap it like this, the app can find it. +def retrieve_service_invite_data(redis_key): + return service_api_client.get_service_invite_data(redis_key) diff --git a/app/templates/error/500.html b/app/templates/error/500.html index af86388d7..aca8332df 100644 --- a/app/templates/error/500.html +++ b/app/templates/error/500.html @@ -7,7 +7,7 @@ Sorry, we can't deliver what you asked for right now.
+ Please try again later or email us for more information. diff --git a/app/templates/new/components/org_nav.html b/app/templates/new/components/org_nav.html index f7f7a5b93..7d156635c 100644 --- a/app/templates/new/components/org_nav.html +++ b/app/templates/new/components/org_nav.html @@ -1,11 +1,11 @@ -