diff --git a/Makefile b/Makefile index 83c317c61..71ce1699d 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ NVMSH := $(shell [ -f "$(HOME)/.nvm/nvm.sh" ] && echo "$(HOME)/.nvm/nvm.sh" || e .PHONY: bootstrap bootstrap: generate-version-file ## Set up everything to run the app - poetry install + poetry install --sync poetry run playwright install --with-deps source $(NVMSH) --no-use && nvm install && npm ci --no-audit source $(NVMSH) && npm run build @@ -92,6 +92,18 @@ js-test: ## Run javascript unit tests fix-imports: ## Fix imports using isort poetry run isort ./app ./tests +.PHONY: py-lock +py-lock: ## Syncs dependencies and updates lock file without performing recursive internal updates + poetry lock --no-update + poetry install --sync + +.PHONY: update-utils +update-utils: ## Forces Poetry to pull the latest changes from the notifications-utils repo; requires that you commit the changes to poetry.lock! + poetry update notifications-utils + @echo + @echo !!! PLEASE MAKE SURE TO COMMIT AND PUSH THE UPDATED poetry.lock FILE !!! + @echo + .PHONY: freeze-requirements freeze-requirements: ## create static requirements.txt poetry export --without-hashes --format=requirements.txt > requirements.txt @@ -101,7 +113,7 @@ pip-audit: poetry requirements > requirements.txt poetry requirements --dev > requirements_for_test.txt poetry run pip-audit -r requirements.txt - -poetry run pip-audit -r requirements_for_test.txt + poetry run pip-audit -r requirements_for_test.txt .PHONY: audit audit: npm-audit pip-audit diff --git a/README.md b/README.md index 57245f677..e8a94416c 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,52 @@ The [Notify API](https://github.com/GSA/notifications-api) provides the UI's bac If you are using VS Code, there are also instructions for [running inside Docker](./docs/docker-remote-containers.md) +### Python dependency management + +We're using [`Poetry`](https://python-poetry.org/) for managing our Python +dependencies and local virtual environments. When it comes to managing the +Python dependencies, there are a couple of things to bear in mind. + +For situations where you manually manipulate the `pyproject.toml` file, you +should use the `make py-lock` command to sync the `poetry.lock` file. This will +ensure that you don't inadvertently bring in other transitive dependency updates +that have not been fully tested with the project yet. + +If you're just trying to update a dependency to a newer (or the latest) version, +you should let Poetry take care of that for you by running the following: + +``` +poetry update [...] +``` + +You can specify more than one dependency together. With this command, Poetry +will do the following for you: + +- Find the latest compatible version(s) of the specified dependency/dependencies +- Install the new versions +- Update and sync the `poetry.lock` file + +In either situation, once you are finished and have verified the dependency +changes are working, please be sure to commit both the `pyproject.toml` and +`poetry.lock` files. + +### Keeping the notification-utils dependency up-to-date + +The `notifications-utils` dependency references the other repository we have at +https://github.com/GSA/notifications-utils - this dependency requires a bit of +extra legwork to ensure it stays up-to-date. + +Whenever a PR is merged in the `notifications-utils` repository, we need to make +sure the changes are pulled in here and committed to this repository as well. +You can do this by going through these steps: + +- Make sure your local `main` branch is up-to-date +- Create a new branch to work in +- Run `make update-utils` +- Commit the updated `poetry.lock` file and push the changes +- Make a new PR with the change +- Have the PR get reviewed and merged + ## To test the application From a terminal within the running devcontainer: diff --git a/app/__init__.py b/app/__init__.py index 7c5879a30..1694fe018 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -285,11 +285,22 @@ def init_app(application): @application.context_processor def _attach_current_global_daily_messages(): remaining_global_messages = 0 - if current_app: - global_limit = current_app.config["GLOBAL_SERVICE_MESSAGE_LIMIT"] - global_messages_count = service_api_client.get_global_notification_count() - remaining_global_messages = global_limit - global_messages_count + if request.view_args: + service_id = request.view_args.get( + "service_id", session.get("service_id") + ) + else: + service_id = session.get("service_id") + + if service_id: + global_limit = current_app.config["GLOBAL_SERVICE_MESSAGE_LIMIT"] + global_messages_count = ( + service_api_client.get_global_notification_count(service_id) + ) + remaining_global_messages = global_limit - global_messages_count.get( + "count" + ) return {"daily_global_messages_remaining": remaining_global_messages} @application.before_request @@ -321,7 +332,7 @@ def make_session_permanent(): """ Make sessions permanent. By permanent, we mean "admin app sets when it expires". Normally the cookie would expire whenever you close the browser. With this, the session expiry is set in `config['PERMANENT_SESSION_LIFETIME']` - (20 hours) and is refreshed after every request. IE: you will be logged out after twenty hours of inactivity. + (30 min) and is refreshed after every request. IE: you will be logged out after thirty minutes of inactivity. We don't _need_ to set this every request (it's saved within the cookie itself under the `_permanent` flag), only when you first log in/sign up/get invited/etc, but we do it just to be safe. For more reading, check here: diff --git a/app/assets/javascripts/timeoutPopup.js b/app/assets/javascripts/timeoutPopup.js new file mode 100644 index 000000000..ed33cf903 --- /dev/null +++ b/app/assets/javascripts/timeoutPopup.js @@ -0,0 +1,65 @@ +window.GOVUK = window.GOVUK || {}; +window.GOVUK.Modules = window.GOVUK.Modules || {}; +window.GOVUK.Modules.TimeoutPopup = window.GOVUK.Modules.TimeoutPopup || {}; + +(function(global) { + "use strict"; + + const sessionTimer = document.getElementById("sessionTimer"); + let intervalId = null; + + function checkTimer(timeTillSessionEnd) { + var now = new Date().getTime(); + var difference = timeTillSessionEnd - now; + var minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60)); + var seconds = Math.floor((difference % (1000 * 60)) / 1000); + document.getElementById("timeLeft").innerHTML = + minutes + "m " + seconds + "s"; + showTimer(); + document.getElementById("logOutTimer").addEventListener("click", signoutUser); + document.getElementById("extendSessionTimer").addEventListener("click", extendSession); + if (difference < 0) { + clearInterval(intervalId); + intervalId = null; + closeTimer(); + expireUserSession(); + } + } + + function expireUserSession() { + var signOutLink = '/sign-out?next=' + window.location.pathname; + window.location.href = signOutLink; + + } + + function signoutUser() { + window.location.href = '/sign-out'; + } + + function extendSession() { + window.location.reload(); + } + + function showTimer() { + sessionTimer.showModal(); + } + + function closeTimer() { + sessionTimer.close(); + } + + function setSessionTimer() { + var timeTillSessionEnd = new Date().getTime() + (5 * 60 * 1000); + intervalId = setInterval(checkTimer, 1000, timeTillSessionEnd); + } + + if (document.getElementById("timeLeft") !== null) { + setTimeout(setSessionTimer, 25 * 60 * 1000); + } + + global.GOVUK.Modules.TimeoutPopup.checkTimer = checkTimer; + global.GOVUK.Modules.TimeoutPopup.expireUserSession = expireUserSession; + global.GOVUK.Modules.TimeoutPopup.signoutUser = signoutUser; + global.GOVUK.Modules.TimeoutPopup.extendSession = extendSession; + global.GOVUK.Modules.TimeoutPopup.showTimer = showTimer; + global.GOVUK.Modules.TimeoutPopup.closeTimer = closeTimer; +})(window); diff --git a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss index f99459c5b..9b88d44a7 100644 --- a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss +++ b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss @@ -414,7 +414,6 @@ details form { display: block; box-sizing: border-box; position: relative; - margin: 0; padding: 4px; overflow: hidden; line-height: 1.6; diff --git a/app/config.py b/app/config.py index 6215a744d..f424076fa 100644 --- a/app/config.py +++ b/app/config.py @@ -54,7 +54,7 @@ class Config(object): EMAIL_EXPIRY_SECONDS = 3600 # 1 hour INVITATION_EXPIRY_SECONDS = 3600 * 24 * 2 # 2 days - also set on api EMAIL_2FA_EXPIRY_SECONDS = 1800 # 30 Minutes - PERMANENT_SESSION_LIFETIME = 20 * 60 * 60 # 20 hours + PERMANENT_SESSION_LIFETIME = 1800 # 30 Minutes SEND_FILE_MAX_AGE_DEFAULT = 365 * 24 * 60 * 60 # 1 year REPLY_TO_EMAIL_ADDRESS_VALIDATION_TIMEOUT = 45 ACTIVITY_STATS_LIMIT_DAYS = 7 diff --git a/app/main/views/index.py b/app/main/views/index.py index 9ad92a285..30818db99 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -227,7 +227,7 @@ def terms(): ) -@main.route("/features/using-notify") +@main.route("/features/using_notify") @user_is_logged_in def using_notify(): return ( diff --git a/app/navigation.py b/app/navigation.py index 8fb06f7f5..b96750dc9 100644 --- a/app/navigation.py +++ b/app/navigation.py @@ -61,6 +61,82 @@ class HeaderNavigation(Navigation): "message_status", "guidance_index", }, + "accounts-or-dashboard": { + "conversation", + "inbox", + "monthly", + "service_dashboard", + "template_usage", + "view_notification", + "view_notifications", + "action_blocked", + "add_service_template", + "check_messages", + "check_notification", + "choose_template", + "choose_template_to_copy", + "confirm_redact_template", + "conversation_reply", + "copy_template", + "delete_service_template", + "edit_service_template", + "manage_template_folder", + "send_messages", + "send_one_off", + "send_one_off_step", + "send_one_off_to_myself", + "set_sender", + "set_template_sender", + "view_template", + "view_template_version", + "view_template_versions", + "uploads", + "view_job", + "view_jobs", + "confirm_edit_user_email", + "confirm_edit_user_mobile_number", + "edit_user_email", + "edit_user_mobile_number", + "edit_user_permissions", + "invite_user", + "manage_users", + "remove_user_from_service", + "usage", + "email_branding_govuk", + "email_branding_govuk_and_org", + "email_branding_organization", + "email_branding_request", + "email_branding_something_else", + "estimate_usage", + "link_service_to_organization", + "request_to_go_live", + "service_add_email_reply_to", + "service_add_sms_sender", + "service_confirm_delete_email_reply_to", + "service_confirm_delete_sms_sender", + "service_edit_email_reply_to", + "service_edit_sms_sender", + "service_email_reply_to", + "service_name_change", + "service_preview_email_branding", + "service_set_auth_type", + "service_set_channel", + "send_files_by_email_contact_details", + "service_set_email_branding", + "service_set_inbound_number", + "service_set_inbound_sms", + "service_set_international_sms", + "service_set_reply_to_email", + "service_set_sms_prefix", + "service_verify_reply_to_address", + "service_verify_reply_to_address_updates", + "service_settings", + "service_sms_senders", + "set_free_sms_allowance", + "set_message_limit", + "set_rate_limit", + "submit_request_to_go_live", + }, "pricing": { "how_to_pay", "billing_details", diff --git a/app/notify_client/service_api_client.py b/app/notify_client/service_api_client.py index 3c29111df..434bf8b3b 100644 --- a/app/notify_client/service_api_client.py +++ b/app/notify_client/service_api_client.py @@ -1,7 +1,5 @@ from datetime import datetime -from notifications_utils.clients.redis import daily_total_cache_key - from app.extensions import redis_client from app.notify_client import NotifyAdminAPIClient, _attach_current_user, cache @@ -497,11 +495,8 @@ class ServiceAPIClient(NotifyAdminAPIClient): return int(count) - def get_global_notification_count(self): - # if cache is not set, or not enabled, return 0 - count = redis_client.get(daily_total_cache_key()) or 0 - - return int(count) + def get_global_notification_count(self, service_id): + return self.get("/service/{}/notification-count".format(service_id)) service_api_client = ServiceAPIClient() diff --git a/app/templates/admin_template.html b/app/templates/admin_template.html index 5e3746262..86482f829 100644 --- a/app/templates/admin_template.html +++ b/app/templates/admin_template.html @@ -23,7 +23,7 @@ {% endblock %} - + {% endblock %} {% block pageTitle %} @@ -43,6 +43,11 @@ {% if current_user.is_authenticated %} {% if current_user.platform_admin %} {% set navigation = [ + { + "href": url_for("main.show_accounts_or_dashboard"), + "text": "Current service", + "active": header_navigation.is_selected('accounts-or-dashboard') + }, { "href": url_for('main.get_started'), "text": "Using Notify", @@ -75,6 +80,11 @@ ] %} {% else %} {% set navigation = [ + { + "href": url_for("main.show_accounts_or_dashboard"), + "text": "Current service", + "active": header_navigation.is_selected('accounts-or-dashboard') + }, { "href": url_for('main.get_started'), "text": "Using Notify", @@ -92,7 +102,7 @@ }, { "href": url_for('main.user_profile'), - "text": current_user.name, + "text": "User profile", "active": header_navigation.is_selected('user-profile') }, { @@ -137,7 +147,6 @@ {% endblock %} {% block footer %} - {% if current_service and current_service.research_mode %} {% set meta_suffix = 'Built by the Technology Transformation Servicesresearch mode' %} @@ -216,8 +225,45 @@ "html": meta_suffix } }) }} + + {% if current_user.is_authenticated %} + {% block sessionUserWarning %} + +
+
+

+ Your session will end soon. + Please choose to extend your session or sign out. Your session will expire in 5 minutes or less. +

+
+

You have been inactive for too long. + Your session will expire in . +

+
+ +
+
+
+ {% endblock %} + {% endif %} + {% endblock %} + {% block bodyEnd %} {% block extra_javascripts %} {% endblock %} @@ -225,4 +271,7 @@ + {% endblock %} + + diff --git a/app/templates/components/components/inset-text/template.njk b/app/templates/components/components/inset-text/template.njk index ade008fed..9e0e6fcdf 100644 --- a/app/templates/components/components/inset-text/template.njk +++ b/app/templates/components/components/inset-text/template.njk @@ -1,4 +1,4 @@ -
{{ params.html | safe if params.html else params.text }}
diff --git a/app/templates/error/500.html b/app/templates/error/500.html index 49645e00a..af86388d7 100644 --- a/app/templates/error/500.html +++ b/app/templates/error/500.html @@ -4,16 +4,10 @@

- Sorry, there’s a problem with Notify.gov + Sorry, we can't deliver what you asked for right now.

- Try again later. -

- -

- To report a problem, please email notify-support@gsa.gov. + Please try again later or email us for more information.

diff --git a/app/templates/main_nav.html b/app/templates/main_nav.html index 75ca2a318..7f1730c5c 100644 --- a/app/templates/main_nav.html +++ b/app/templates/main_nav.html @@ -1,13 +1,15 @@ {% if help %} {% include 'partials/tour.html' %} {% else %} -