diff --git a/CONTRIB.md b/CONTRIB.md index dafd23881..742a65eb3 100644 --- a/CONTRIB.md +++ b/CONTRIB.md @@ -66,6 +66,14 @@ Example `.env` variable: OO_SERVICE_EMAIL="sample_email@domain.com" ``` +In addition to needing a service account email, you also need an admin email address so that users have someone to reach out to if an action is taken on their account that needs to be reversed or addressed. +For production, save the email address associated with your admin account to a variable named `OO_HELP_EMAIL` in a `.env` file in the base directory of this repository. For development and testing, update the `OO_HELP_EMAIL` variable in the `docker-compose.yml` file. + +Example `.env` variable: +```bash +OO_HELP_EMAIL="sample_admin_email@domain.com" +``` + ## Testing S3 Functionality We use an S3 bucket for image uploads. If you are working on functionality involving image uploads, then you should follow the "S3 Image Hosting" section in [DEPLOY.md](/DEPLOY.md) to make a test S3 bucket diff --git a/OpenOversight/app/auth/views.py b/OpenOversight/app/auth/views.py index 0fd020769..fbd1c02e4 100644 --- a/OpenOversight/app/auth/views.py +++ b/OpenOversight/app/auth/views.py @@ -28,6 +28,7 @@ from OpenOversight.app.models.emails import ( AdministratorApprovalEmail, ChangeEmailAddressEmail, + ChangePasswordEmail, ConfirmAccountEmail, ConfirmedUserEmail, ResetPasswordEmail, @@ -175,6 +176,9 @@ def change_password(): db.session.add(current_user) db.session.commit() flash("Your password has been updated.") + EmailClient.send_email( + ChangePasswordEmail(current_user.email, user=current_user) + ) return redirect(url_for("main.index")) else: flash("Invalid password.") diff --git a/OpenOversight/app/models/config.py b/OpenOversight/app/models/config.py index c44c3f929..f1de76e05 100644 --- a/OpenOversight/app/models/config.py +++ b/OpenOversight/app/models/config.py @@ -37,6 +37,9 @@ def __init__(self): "OO_MAIL_SUBJECT_PREFIX", "[OpenOversight]" ) self.OO_SERVICE_EMAIL = os.environ.get("OO_SERVICE_EMAIL") + # TODO: Remove the default once we are able to update the production .env file + # TODO: Once that is done, we can re-alpha sort these variables. + self.OO_HELP_EMAIL = os.environ.get("OO_HELP_EMAIL", self.OO_SERVICE_EMAIL) # AWS Settings self.AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID") diff --git a/OpenOversight/app/models/emails.py b/OpenOversight/app/models/emails.py index 6ca205c6a..6ddc711e8 100644 --- a/OpenOversight/app/models/emails.py +++ b/OpenOversight/app/models/emails.py @@ -39,6 +39,19 @@ def __init__(self, receiver: str, user, token: str): super().__init__(body, subject, receiver) +class ChangePasswordEmail(Email): + def __init__(self, receiver: str, user): + subject = ( + f"{current_app.config['OO_MAIL_SUBJECT_PREFIX']} Your Password Has Changed" + ) + body = render_template( + "auth/email/change_password.html", + user=user, + help_email=current_app.config["OO_HELP_EMAIL"], + ) + super().__init__(body, subject, receiver) + + class ConfirmAccountEmail(Email): def __init__(self, receiver: str, user, token: str): subject = f"{current_app.config['OO_MAIL_SUBJECT_PREFIX']} Confirm Your Account" diff --git a/OpenOversight/app/templates/auth/email/change_email.html b/OpenOversight/app/templates/auth/email/change_email.html index 300bd295c..681083b70 100644 --- a/OpenOversight/app/templates/auth/email/change_email.html +++ b/OpenOversight/app/templates/auth/email/change_email.html @@ -7,5 +7,5 @@

Sincerely,

The OpenOversight Team

- Note: replies to this email address are not monitored. + Please note that we may not monitor replies to this email address.

diff --git a/OpenOversight/app/templates/auth/email/change_password.html b/OpenOversight/app/templates/auth/email/change_password.html new file mode 100644 index 000000000..8e4987ff0 --- /dev/null +++ b/OpenOversight/app/templates/auth/email/change_password.html @@ -0,0 +1,11 @@ +

Dear {{ user.username }},

+

Your password has just been changed.

+

If you initiated this change to your password, you can ignore this email.

+

+ If you did not reset your password, please contact the OpenOversight help account; they will help you address this issue. +

+

Sincerely,

+

The OpenOversight Team

+

+ Please note that we may not monitor replies to this email address. +

diff --git a/OpenOversight/app/templates/auth/email/confirm.html b/OpenOversight/app/templates/auth/email/confirm.html index 9e09ad756..14a89e677 100644 --- a/OpenOversight/app/templates/auth/email/confirm.html +++ b/OpenOversight/app/templates/auth/email/confirm.html @@ -10,5 +10,5 @@

Sincerely,

The OpenOversight Team

- Note: replies to this email address are not monitored. + Please note that we may not monitor replies to this email address.

diff --git a/OpenOversight/app/templates/auth/email/new_confirmation.html b/OpenOversight/app/templates/auth/email/new_confirmation.html index 6af487b7a..e50475a32 100644 --- a/OpenOversight/app/templates/auth/email/new_confirmation.html +++ b/OpenOversight/app/templates/auth/email/new_confirmation.html @@ -12,5 +12,5 @@

Sincerely,

The OpenOversight Team

- Note: replies to this email address are not monitored. + Please note that we may not monitor replies to this email address.

diff --git a/OpenOversight/app/templates/auth/email/new_registration.html b/OpenOversight/app/templates/auth/email/new_registration.html index 63deb48a4..8128359d7 100644 --- a/OpenOversight/app/templates/auth/email/new_registration.html +++ b/OpenOversight/app/templates/auth/email/new_registration.html @@ -12,5 +12,5 @@

Sincerely,

The OpenOversight Team

- Note: replies to this email address are not monitored. + Please note that we may not monitor replies to this email address.

diff --git a/OpenOversight/app/templates/auth/email/reset_password.html b/OpenOversight/app/templates/auth/email/reset_password.html index 84dd9fd5b..ce67a476c 100644 --- a/OpenOversight/app/templates/auth/email/reset_password.html +++ b/OpenOversight/app/templates/auth/email/reset_password.html @@ -8,5 +8,5 @@

Sincerely,

The OpenOversight Team

- Note: replies to this email address are not monitored. + Please note that we may not monitor replies to this email address.

diff --git a/OpenOversight/tests/routes/test_auth.py b/OpenOversight/tests/routes/test_auth.py index f738299da..a6f5e0d82 100644 --- a/OpenOversight/tests/routes/test_auth.py +++ b/OpenOversight/tests/routes/test_auth.py @@ -1,5 +1,6 @@ # Routing and view tests from http import HTTPStatus +from unittest import TestCase from urllib.parse import urlparse import pytest @@ -141,8 +142,10 @@ def test_user_cannot_register_if_passwords_dont_match(mockdata, client, session) def test_user_can_register_with_legit_credentials(mockdata, client, session): - with current_app.test_request_context(): - diceware_password = "operative hamster perservere verbalize curling" + with current_app.test_request_context(), TestCase.assertLogs( + current_app.logger + ) as log: + diceware_password = "operative hamster persevere verbalize curling" form = RegistrationForm( email="jen@example.com", username="redshiftzero", @@ -154,6 +157,10 @@ def test_user_can_register_with_legit_credentials(mockdata, client, session): ) assert b"A confirmation email has been sent to you." in rv.data + assert ( + f"{current_app.config['OO_MAIL_SUBJECT_PREFIX']} Confirm Your Account" + in str(log.output) + ) def test_user_cannot_register_with_weak_password(mockdata, client, session): @@ -172,16 +179,24 @@ def test_user_cannot_register_with_weak_password(mockdata, client, session): def test_user_can_get_a_confirmation_token_resent(mockdata, client, session): - with current_app.test_request_context(): + with current_app.test_request_context(), TestCase.assertLogs( + current_app.logger + ) as log: login_user(client) rv = client.get(url_for("auth.resend_confirmation"), follow_redirects=True) assert b"A new confirmation email has been sent to you." in rv.data + assert ( + f"{current_app.config['OO_MAIL_SUBJECT_PREFIX']} Confirm Your Account" + in str(log.output) + ) def test_user_can_get_password_reset_token_sent(mockdata, client, session): - with current_app.test_request_context(): + with current_app.test_request_context(), TestCase.assertLogs( + current_app.logger + ) as log: form = PasswordResetRequestForm(email="jen@example.org") rv = client.post( @@ -191,12 +206,18 @@ def test_user_can_get_password_reset_token_sent(mockdata, client, session): ) assert b"An email with instructions to reset your password" in rv.data + assert ( + f"{current_app.config['OO_MAIL_SUBJECT_PREFIX']} Reset Your Password" + in str(log.output) + ) def test_user_can_get_password_reset_token_sent_with_differently_cased_email( mockdata, client, session ): - with current_app.test_request_context(): + with current_app.test_request_context(), TestCase.assertLogs( + current_app.logger + ) as log: form = PasswordResetRequestForm(email="JEN@EXAMPLE.ORG") rv = client.post( @@ -206,6 +227,10 @@ def test_user_can_get_password_reset_token_sent_with_differently_cased_email( ) assert b"An email with instructions to reset your password" in rv.data + assert ( + f"{current_app.config['OO_MAIL_SUBJECT_PREFIX']} Reset Your Password" + in str(log.output) + ) def test_user_can_get_reset_password_with_valid_token(mockdata, client, session): @@ -361,7 +386,9 @@ def test_user_can_not_confirm_account_with_invalid_token(mockdata, client, sessi def test_user_can_change_password_if_they_match(mockdata, client, session): - with current_app.test_request_context(): + with current_app.test_request_context(), TestCase.assertLogs( + current_app.logger + ) as log: login_user(client) form = ChangePasswordForm( old_password="dog", password="validpasswd", password2="validpasswd" @@ -372,6 +399,10 @@ def test_user_can_change_password_if_they_match(mockdata, client, session): ) assert b"Your password has been updated." in rv.data + assert ( + f"{current_app.config['OO_MAIL_SUBJECT_PREFIX']} Your Password Has Changed" + in str(log.output) + ) def test_unconfirmed_user_redirected_to_confirm_account(mockdata, client, session): diff --git a/docker-compose.yml b/docker-compose.yml index a204c82f9..36ce3c23d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,6 +36,7 @@ services: AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY}" ENV: "${ENV:-development}" FLASK_APP: OpenOversight.app + OO_HELP_EMAIL: "info@lucyparsonslabs.com" OO_SERVICE_EMAIL: "openoversightchi@lucyparsonslabs.com" S3_BUCKET_NAME: "${S3_BUCKET_NAME}" volumes: