From 51be12e59c7722fbc21b623f70e27861783f5208 Mon Sep 17 00:00:00 2001 From: Ewen Le Bihan Date: Fri, 28 Aug 2020 12:35:20 +0200 Subject: [PATCH 01/15] =?UTF-8?q?=E2=9E=95=20Depend=20on=20chevron?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 14 +++++++++++++- pyproject.toml | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 6052fac..4743196 100644 --- a/poetry.lock +++ b/poetry.lock @@ -137,6 +137,14 @@ optional = false python-versions = "*" version = "3.0.4" +[[package]] +category = "main" +description = "Mustache templating language renderer" +name = "chevron" +optional = false +python-versions = "*" +version = "0.13.1" + [[package]] category = "main" description = "Composable command line interface toolkit" @@ -1028,7 +1036,7 @@ python-versions = "*" version = "4.4.28" [metadata] -content-hash = "1f318bc4c8d62b545037d45083cc00373624eed3ded4f31ed9baf6b8570c9a4c" +content-hash = "ca94af47a1d55897b7f967220b787c27e3cc09c9b233be6dee2bde27d8cc2c03" lock-version = "1.1" python-versions = "^3.8" @@ -1124,6 +1132,10 @@ chardet = [ {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, ] +chevron = [ + {file = "chevron-0.13.1-py3-none-any.whl", hash = "sha256:95b0a055ef0ada5eb061d60be64a7f70670b53372ccd221d1b88adf1c41a9094"}, + {file = "chevron-0.13.1.tar.gz", hash = "sha256:f95054a8b303268ebf3efd6bdfc8c1b428d3fc92327913b4e236d062ec61c989"}, +] click = [ {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, diff --git a/pyproject.toml b/pyproject.toml index fdcfe81..dc2df09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ nanoid = "^2.0.0" python-slugify = "^4.0.1" fastapi_utils = "^0.2.1" fastapi-etag = "^0.2.2" +chevron = "^0.13.1" [tool.poetry.dev-dependencies] black = "^19.10b0" From e5f58115562803c5142f5c7d9b255948db59358c Mon Sep 17 00:00:00 2001 From: Ewen Le Bihan Date: Fri, 28 Aug 2020 12:38:53 +0200 Subject: [PATCH 02/15] =?UTF-8?q?=F0=9F=9A=A7=20Create=20`EmailConfirmedAc?= =?UTF-8?q?tion`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- schoolsyst_api/email_confirmed_action.py | 85 ++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 schoolsyst_api/email_confirmed_action.py diff --git a/schoolsyst_api/email_confirmed_action.py b/schoolsyst_api/email_confirmed_action.py new file mode 100644 index 0000000..35e8aa4 --- /dev/null +++ b/schoolsyst_api/email_confirmed_action.py @@ -0,0 +1,85 @@ +from datetime import timedelta +from pathlib import Path +from typing import Callable, Literal, Union + +import chevron +from fastapi import BackgroundTasks, HTTPException, status +from schoolsyst_api.accounts import create_jwt_token, verify_jwt_token +from schoolsyst_api.accounts.models import User + + +class EmailConfirmedAction: + name: str + callback_url: str + token_valid_for: timedelta + + def __init__( + self, name: str, callback_url: str, token_valid_for: timedelta, + ) -> None: + """ + `callback_url` is used as the link to click on in the email. + All occurences of `{}` will be replaced with the generated token. + """ + self.name = name + self.callback_url = callback_url + self.token_valid_for = token_valid_for + + @property + def jwt_sub_format(self) -> str: + return f"{self.name}:{{}}" + + @property + def mail_template_path(self) -> str: + """ + The mail template (.html compiled from .mjml file)'s path, + assuming the project root as the current directory. + The extension still needs to be added to get either the HTML version (.html) + or the plain-text one (.txt) + """ + return str(Path("static") / "mail_templates" / "dist" / self.name) + + @property + def _send_mail_function(self) -> Callable[[str], bool]: + def send_mail(token: str): + pass + + return send_mail + + def _action_url(self, token: str) -> str: + return self.callback_url.format(token) + + def _render_mail_template( + self, + token: str, + fmt: Union[Literal["html"], Literal["txt"]], + current_user: User, + ) -> str: + """ + Renders the mail template (at `self.mail_template_path()`) + and returns the HTML/plaintext string + """ + return chevron.render( + Path(f"{self.mail_template_path}.{fmt}").read_text(), + { + "url": self._action_url(token), + "token": token, + "username": current_user.username, + "email": current_user.email, + }, + ) + + def send_request(self, current_user: User, tasks: BackgroundTasks) -> None: + token = create_jwt_token( + self.jwt_sub_format, current_user.username, self.token_valid_for + ) + + tasks.add_task(self._send_mail_function, token) + + def handle_token_verification(self, input_token: str, current_user: User): + if not verify_jwt_token( + input_token, self.jwt_sub_format, current_user.username + ): + raise HTTPException( + status.HTTP_403_FORBIDDEN, + detail="The token is either corrupted, emitted by another user or expired", + ) From 4827aa41ff5afe913152beba1a8ddc0496947f0e Mon Sep 17 00:00:00 2001 From: Ewen Le Bihan Date: Fri, 28 Aug 2020 12:43:32 +0200 Subject: [PATCH 03/15] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Make=20use=20of=20`E?= =?UTF-8?q?mailConfirmedAction`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- schoolsyst_api/accounts/email_confirmation.py | 102 ++++-------------- schoolsyst_api/accounts/password_reset.py | 54 ++-------- 2 files changed, 29 insertions(+), 127 deletions(-) diff --git a/schoolsyst_api/accounts/email_confirmation.py b/schoolsyst_api/accounts/email_confirmation.py index 8d2c40c..f42e595 100644 --- a/schoolsyst_api/accounts/email_confirmation.py +++ b/schoolsyst_api/accounts/email_confirmation.py @@ -1,102 +1,42 @@ import os -from datetime import datetime, timedelta +from datetime import timedelta from arango.database import StandardDatabase from dotenv import load_dotenv -from fastapi import BackgroundTasks, Depends, HTTPException, status -from jose import jwt -from pydantic import BaseModel +from fastapi import BackgroundTasks, Depends, status from schoolsyst_api import database -from schoolsyst_api.accounts import ( - JWT_SIGN_ALGORITHM, - create_jwt_token, - router, - verify_jwt_token, -) -from schoolsyst_api.accounts.models import User, UserKey -from schoolsyst_api.accounts.password_reset import VALID_FOR +from schoolsyst_api.accounts import router +from schoolsyst_api.accounts.models import User from schoolsyst_api.accounts.users import get_current_user +from schoolsyst_api.email_confirmed_action import EmailConfirmedAction load_dotenv(".env") SECRET_KEY = os.getenv("SECRET_KEY") -TOKEN_VALID_FOR = timedelta(hours=24) -JWT_SUB_FORMAT = "email-confirmation:{}" - - -def send_email_confirmation_email( - to_email: str, to_username: str, email_confirm_request_token: str -) -> bool: - email = f"""\ -From: schoolsyst password reset system -To: {to_username} <{to_email}> -Subject: Confirm your email address - -Go to https://app.schoolsyst.com/email-confirm/{email_confirm_request_token} to confirm your email address. -If you didn't request an email confirmation or don't know what schoolsyst is, -this means that someone tried to register an account and/or confirm an email address using -yours instead of theirs. Just ignore this and your email address won't be confirmed. -If you have any reason to think that this person has access to your mailbox, you may change your -email account's password. -""" - print("[fake] sending email:") - print("---") - print(email) - print("---") - # host = SMTP("email.schoolsyst.com", SMTP_SSL_PORT, "localhost") - # host.sendmail( - # from_addr="reset-password@schoolsyst.com", - # to_addrs=[to_email], - # msg=email, - # ) - return True - - -class EmailConfirmationRequest(BaseModel): - created_at: datetime - token: str - emitted_by_key: UserKey +VALID_FOR = timedelta(hours=24) - -def create_email_confirmation_request_token( - username: str, request_valid_for: timedelta -) -> str: - return jwt.encode( - { - "sub": JWT_SUB_FORMAT.format(username), - "exp": datetime.utcnow() + request_valid_for, - }, - SECRET_KEY, - algorithm=JWT_SIGN_ALGORITHM, - ) +helper = EmailConfirmedAction( + name="email_confirmation", + callback_url="https://app.schoolsyst.com/confirm_email/{}/", + token_valid_for=VALID_FOR, +) @router.post( - "/users/email-confirmation-request", + "/email_confirmation/request", summary="Request an email confirmation", description=f""" Sends an email to the logged-in user's email address, and creates a `EmailConfirmationRequest` with a temporary token, The email can then be confirmed with `POST /users/email-confirmation/`. -The token is considered expired {VALID_FOR.total_seconds() / 3600} hours after creation, -and the `EmailConfirmationRequest` is destroyed once an associated -`POST /users/email-confirmation` is made. - -In other words, the request is valid for up to {VALID_FOR.total_seconds() / 3600} hours, -and can only be used once. +The token is considered expired {VALID_FOR.total_seconds() / 3600} hours after creation. """.strip(), status_code=status.HTTP_202_ACCEPTED, ) async def post_users_password_reset_request( - tasks: BackgroundTasks, - user: User = Depends(get_current_user), - db: StandardDatabase = Depends(database.get), + tasks: BackgroundTasks, user: User = Depends(get_current_user), ): - # create a token - token = create_jwt_token(JWT_SUB_FORMAT, user.username, VALID_FOR) - - # send an email - tasks.add_task(send_email_confirmation_email, user.email, user.username, token) + helper.send_request(current_user=user, tasks=tasks) return @@ -108,7 +48,7 @@ async def post_users_password_reset_request( @router.post( - "/users/email-confirmation", + "/email_confirmation", summary="Confirm an email", responses=post_users_email_confirmation_responses, ) @@ -118,18 +58,14 @@ async def post_users_password_reset( db: StandardDatabase = Depends(database.get), ): """ - Confirms the user's email address given a `request_token`. + Confirms the user's email address given a `token`. - The `request_token` is a token sent to the user's email address as a link + The `token` is a token sent to the user's email address as a link (something like `https://app.schoolsyst.com/email-confirmation/{request_token}`) after performing a `POST /users/email-confirmation-request`. """ # verify the token - if not verify_jwt_token(token, JWT_SUB_FORMAT, user.username): - raise HTTPException( - status.HTTP_403_FORBIDDEN, - detail=post_users_email_confirmation_responses[403]["description"], - ) + helper.handle_token_verification(token, user) # confirm the email db.collection("users").update({"_key": user.key, "email_is_confirmed": True}) return {} diff --git a/schoolsyst_api/accounts/password_reset.py b/schoolsyst_api/accounts/password_reset.py index 8327aec..f4ea3f7 100644 --- a/schoolsyst_api/accounts/password_reset.py +++ b/schoolsyst_api/accounts/password_reset.py @@ -1,12 +1,12 @@ import os -from datetime import datetime, timedelta +from datetime import timedelta from arango.database import StandardDatabase from dotenv import load_dotenv from fastapi import BackgroundTasks, Depends, HTTPException, status from pydantic import BaseModel from schoolsyst_api import database -from schoolsyst_api.accounts import create_jwt_token, router, verify_jwt_token +from schoolsyst_api.accounts import router from schoolsyst_api.accounts.auth import ( get_password_analysis, hash_password, @@ -14,42 +14,15 @@ ) from schoolsyst_api.accounts.models import User from schoolsyst_api.accounts.users import get_current_confirmed_user -from schoolsyst_api.models import UserKey +from schoolsyst_api.email_confirmed_action import EmailConfirmedAction load_dotenv(".env") SECRET_KEY = os.getenv("SECRET_KEY") VALID_FOR = timedelta(minutes=30) -JWT_SUB_FORMAT = "password-reset:{}" - -class PasswordResetRequest(BaseModel): - created_at: datetime - token: str - emitted_by_key: UserKey - - -def send_password_reset_email( - to_email: str, to_username: str, password_reset_request_token: str -) -> bool: - email = f"""\ -From: schoolsyst password reset system -To: {to_username} <{to_email}> -Subject: Reset your schoolsyst password - -Go to https://app.schoolsyst.com/reset-password/{password_reset_request_token} to reset it. -If you didn't request a password reset, just ignore this, and your password won't be modified. -""" - print("[fake] sending email:") - print("---") - print(email) - print("---") - # host = SMTP("email.schoolsyst.com", SMTP_SSL_PORT, "localhost") - # host.sendmail( - # from_addr="reset-password@schoolsyst.com", - # to_addrs=[to_email], - # msg=email, - # ) - return True +helper = EmailConfirmedAction( + name="password_reset", callback_url="/reset-password", token_valid_for=VALID_FOR, +) @router.post( @@ -72,11 +45,7 @@ def send_password_reset_email( async def post_users_password_reset_request( tasks: BackgroundTasks, user: User = Depends(get_current_confirmed_user), ): - # create a request - token = create_jwt_token(JWT_SUB_FORMAT, user.username, VALID_FOR) - - # send an email - tasks.add_task(send_password_reset_email, user.email, user.username, token) + helper.send_request(current_user=user, tasks=tasks) return @@ -110,6 +79,9 @@ async def post_users_password_reset( (something like `https://app.schoolsyst.com/password-reset/{request_token}`) after performing a `POST /users/password-reset-request`. """ + helper.handle_token_verification( + input_token=change_data.request_token, current_user=user + ) analysis = get_password_analysis( change_data.new_password, user.email, user.username ) @@ -119,12 +91,6 @@ async def post_users_password_reset( detail="This password is not strong enough", headers={"X-See": "GET /password_analysis/"}, ) - # validate the token - if not verify_jwt_token(change_data.request_token, JWT_SUB_FORMAT, user.username): - return HTTPException( - status.HTTP_403_FORBIDDEN, - detail=post_users_password_reset_responses[403]["description"], - ) # change the password db.collection("users").update( {"_key": user.key, "password_hash": hash_password(change_data.new_password)} From 4c89f93244e07f4ee99470ff72b0eec8acacc1eb Mon Sep 17 00:00:00 2001 From: Ewen Le Bihan Date: Fri, 28 Aug 2020 12:45:08 +0200 Subject: [PATCH 04/15] =?UTF-8?q?=F0=9F=8D=B1=20=F0=9F=9A=A7=20Add=20email?= =?UTF-8?q?=20template=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/email_templates/_base.mjml | 0 static/email_templates/data_archive.mjml | 0 static/email_templates/data_archive.txt | 0 static/email_templates/email_confirmation.mjml | 0 static/email_templates/email_confirmation.txt | 0 static/email_templates/password_reset.mjml | 0 static/email_templates/password_reset.txt | 0 7 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 static/email_templates/_base.mjml create mode 100644 static/email_templates/data_archive.mjml create mode 100644 static/email_templates/data_archive.txt create mode 100644 static/email_templates/email_confirmation.mjml create mode 100644 static/email_templates/email_confirmation.txt create mode 100644 static/email_templates/password_reset.mjml create mode 100644 static/email_templates/password_reset.txt diff --git a/static/email_templates/_base.mjml b/static/email_templates/_base.mjml new file mode 100644 index 0000000..e69de29 diff --git a/static/email_templates/data_archive.mjml b/static/email_templates/data_archive.mjml new file mode 100644 index 0000000..e69de29 diff --git a/static/email_templates/data_archive.txt b/static/email_templates/data_archive.txt new file mode 100644 index 0000000..e69de29 diff --git a/static/email_templates/email_confirmation.mjml b/static/email_templates/email_confirmation.mjml new file mode 100644 index 0000000..e69de29 diff --git a/static/email_templates/email_confirmation.txt b/static/email_templates/email_confirmation.txt new file mode 100644 index 0000000..e69de29 diff --git a/static/email_templates/password_reset.mjml b/static/email_templates/password_reset.mjml new file mode 100644 index 0000000..e69de29 diff --git a/static/email_templates/password_reset.txt b/static/email_templates/password_reset.txt new file mode 100644 index 0000000..e69de29 From 139659edaff5d69b01f7c7ccc0b1409072174e63 Mon Sep 17 00:00:00 2001 From: Ewen Le Bihan Date: Sat, 29 Aug 2020 01:02:53 +0200 Subject: [PATCH 05/15] =?UTF-8?q?=E2=9E=95=20Depend=20on=20yagmail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 67 +++++++++++++++++++++++++++----------------- pyproject.toml | 1 + requirements.txt | 49 +++++++++++++++++++------------- requirements_dev.txt | 55 +++++++++++++++++++++--------------- 4 files changed, 104 insertions(+), 68 deletions(-) diff --git a/poetry.lock b/poetry.lock index 4743196..6004abe 100644 --- a/poetry.lock +++ b/poetry.lock @@ -190,7 +190,7 @@ description = "cryptography is a package which provides cryptographic recipes an name = "cryptography" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" -version = "3.0" +version = "3.1" [package.dependencies] cffi = ">=1.8,<1.11.3 || >1.11.3" @@ -199,7 +199,6 @@ six = ">=1.4.1" [package.extras] docs = ["sphinx (>=1.6.5,<1.8.0 || >1.8.0,<3.1.0 || >3.1.0,<3.1.1 || >3.1.1)", "sphinx-rtd-theme"] docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] -idna = ["idna (>=2.1)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] ssh = ["bcrypt (>=3.1.5)"] test = ["pytest (>=3.6.0,<3.9.0 || >3.9.0,<3.9.1 || >3.9.1,<3.9.2 || >3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,<3.79.2 || >3.79.2)"] @@ -481,7 +480,7 @@ description = "More routines for operating on iterables, beyond itertools" name = "more-itertools" optional = false python-versions = ">=3.5" -version = "8.4.0" +version = "8.5.0" [[package]] category = "main" @@ -1027,6 +1026,17 @@ optional = false python-versions = ">=3.6.1" version = "8.1" +[[package]] +category = "main" +description = "Yet Another GMAIL client" +name = "yagmail" +optional = false +python-versions = "*" +version = "0.11.224" + +[package.extras] +all = ["keyring"] + [[package]] category = "main" description = "" @@ -1036,7 +1046,7 @@ python-versions = "*" version = "4.4.28" [metadata] -content-hash = "ca94af47a1d55897b7f967220b787c27e3cc09c9b233be6dee2bde27d8cc2c03" +content-hash = "5b49d6cdeff80a1ba3a3469b932111728e15650be683fcfe751bca5b6aa6cad4" lock-version = "1.1" python-versions = "^3.8" @@ -1186,25 +1196,28 @@ coverage = [ {file = "coverage-5.2.1.tar.gz", hash = "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b"}, ] cryptography = [ - {file = "cryptography-3.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ab49edd5bea8d8b39a44b3db618e4783ef84c19c8b47286bf05dfdb3efb01c83"}, - {file = "cryptography-3.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:124af7255ffc8e964d9ff26971b3a6153e1a8a220b9a685dc407976ecb27a06a"}, - {file = "cryptography-3.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:51e40123083d2f946794f9fe4adeeee2922b581fa3602128ce85ff813d85b81f"}, - {file = "cryptography-3.0-cp27-cp27m-win32.whl", hash = "sha256:dea0ba7fe6f9461d244679efa968d215ea1f989b9c1957d7f10c21e5c7c09ad6"}, - {file = "cryptography-3.0-cp27-cp27m-win_amd64.whl", hash = "sha256:8ecf9400d0893836ff41b6f977a33972145a855b6efeb605b49ee273c5e6469f"}, - {file = "cryptography-3.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c608ff4d4adad9e39b5057de43657515c7da1ccb1807c3a27d4cf31fc923b4b"}, - {file = "cryptography-3.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:bec7568c6970b865f2bcebbe84d547c52bb2abadf74cefce396ba07571109c67"}, - {file = "cryptography-3.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:0cbfed8ea74631fe4de00630f4bb592dad564d57f73150d6f6796a24e76c76cd"}, - {file = "cryptography-3.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:a09fd9c1cca9a46b6ad4bea0a1f86ab1de3c0c932364dbcf9a6c2a5eeb44fa77"}, - {file = "cryptography-3.0-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:ce82cc06588e5cbc2a7df3c8a9c778f2cb722f56835a23a68b5a7264726bb00c"}, - {file = "cryptography-3.0-cp35-cp35m-win32.whl", hash = "sha256:9367d00e14dee8d02134c6c9524bb4bd39d4c162456343d07191e2a0b5ec8b3b"}, - {file = "cryptography-3.0-cp35-cp35m-win_amd64.whl", hash = "sha256:384d7c681b1ab904fff3400a6909261cae1d0939cc483a68bdedab282fb89a07"}, - {file = "cryptography-3.0-cp36-cp36m-win32.whl", hash = "sha256:4d355f2aee4a29063c10164b032d9fa8a82e2c30768737a2fd56d256146ad559"}, - {file = "cryptography-3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:45741f5499150593178fc98d2c1a9c6722df88b99c821ad6ae298eff0ba1ae71"}, - {file = "cryptography-3.0-cp37-cp37m-win32.whl", hash = "sha256:8ecef21ac982aa78309bb6f092d1677812927e8b5ef204a10c326fc29f1367e2"}, - {file = "cryptography-3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4b9303507254ccb1181d1803a2080a798910ba89b1a3c9f53639885c90f7a756"}, - {file = "cryptography-3.0-cp38-cp38-win32.whl", hash = "sha256:8713ddb888119b0d2a1462357d5946b8911be01ddbf31451e1d07eaa5077a261"}, - {file = "cryptography-3.0-cp38-cp38-win_amd64.whl", hash = "sha256:bea0b0468f89cdea625bb3f692cd7a4222d80a6bdafd6fb923963f2b9da0e15f"}, - {file = "cryptography-3.0.tar.gz", hash = "sha256:8e924dbc025206e97756e8903039662aa58aa9ba357d8e1d8fc29e3092322053"}, + {file = "cryptography-3.1-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:969ae512a250f869c1738ca63be843488ff5cc031987d302c1f59c7dbe1b225f"}, + {file = "cryptography-3.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:b45ab1c6ece7c471f01c56f5d19818ca797c34541f0b2351635a5c9fe09ac2e0"}, + {file = "cryptography-3.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:247df238bc05c7d2e934a761243bfdc67db03f339948b1e2e80c75d41fc7cc36"}, + {file = "cryptography-3.1-cp27-cp27m-win32.whl", hash = "sha256:10c9775a3f31610cf6b694d1fe598f2183441de81cedcf1814451ae53d71b13a"}, + {file = "cryptography-3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:9f734423eb9c2ea85000aa2476e0d7a58e021bc34f0a373ac52a5454cd52f791"}, + {file = "cryptography-3.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e7563eb7bc5c7e75a213281715155248cceba88b11cb4b22957ad45b85903761"}, + {file = "cryptography-3.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:94191501e4b4009642be21dde2a78bd3c2701a81ee57d3d3d02f1d99f8b64a9e"}, + {file = "cryptography-3.1-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:dc3f437ca6353979aace181f1b790f0fc79e446235b14306241633ab7d61b8f8"}, + {file = "cryptography-3.1-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:725875681afe50b41aee7fdd629cedbc4720bab350142b12c55c0a4d17c7416c"}, + {file = "cryptography-3.1-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:321761d55fb7cb256b771ee4ed78e69486a7336be9143b90c52be59d7657f50f"}, + {file = "cryptography-3.1-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:2a27615c965173c4c88f2961cf18115c08fedfb8bdc121347f26e8458dc6d237"}, + {file = "cryptography-3.1-cp35-cp35m-win32.whl", hash = "sha256:e7dad66a9e5684a40f270bd4aee1906878193ae50a4831922e454a2a457f1716"}, + {file = "cryptography-3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4005b38cd86fc51c955db40b0f0e52ff65340874495af72efabb1bb8ca881695"}, + {file = "cryptography-3.1-cp36-abi3-win32.whl", hash = "sha256:cc6096c86ec0de26e2263c228fb25ee01c3ff1346d3cfc219d67d49f303585af"}, + {file = "cryptography-3.1-cp36-abi3-win_amd64.whl", hash = "sha256:2e26223ac636ca216e855748e7d435a1bf846809ed12ed898179587d0cf74618"}, + {file = "cryptography-3.1-cp36-cp36m-win32.whl", hash = "sha256:7a63e97355f3cd77c94bd98c59cb85fe0efd76ea7ef904c9b0316b5bbfde6ed1"}, + {file = "cryptography-3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:4b9e96543d0784acebb70991ebc2dbd99aa287f6217546bb993df22dd361d41c"}, + {file = "cryptography-3.1-cp37-cp37m-win32.whl", hash = "sha256:eb80a288e3cfc08f679f95da72d2ef90cb74f6d8a8ba69d2f215c5e110b2ca32"}, + {file = "cryptography-3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:180c9f855a8ea280e72a5d61cf05681b230c2dce804c48e9b2983f491ecc44ed"}, + {file = "cryptography-3.1-cp38-cp38-win32.whl", hash = "sha256:fa7fbcc40e2210aca26c7ac8a39467eae444d90a2c346cbcffd9133a166bcc67"}, + {file = "cryptography-3.1-cp38-cp38-win_amd64.whl", hash = "sha256:548b0818e88792318dc137d8b1ec82a0ab0af96c7f0603a00bb94f896fbf5e10"}, + {file = "cryptography-3.1.tar.gz", hash = "sha256:26409a473cc6278e4c90f782cd5968ebad04d3911ed1c402fc86908c17633e08"}, ] distlib = [ {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, @@ -1363,8 +1376,8 @@ mccabe = [ {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] more-itertools = [ - {file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"}, - {file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"}, + {file = "more-itertools-8.5.0.tar.gz", hash = "sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20"}, + {file = "more_itertools-8.5.0-py3-none-any.whl", hash = "sha256:9b30f12df9393f0d28af9210ff8efe48d10c94f73e5daf886f10c4b0b0b4f03c"}, ] nanoid = [ {file = "nanoid-2.0.0-py3-none-any.whl", hash = "sha256:90aefa650e328cffb0893bbd4c236cfd44c48bc1f2d0b525ecc53c3187b653bb"}, @@ -1704,6 +1717,10 @@ websockets = [ {file = "websockets-8.1-cp38-cp38-win_amd64.whl", hash = "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"}, {file = "websockets-8.1.tar.gz", hash = "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f"}, ] +yagmail = [ + {file = "yagmail-0.11.224-py2.py3-none-any.whl", hash = "sha256:a055d78eca8846bee8329b1bd337599b0b09462d4d24471fba44db30dbfec03f"}, + {file = "yagmail-0.11.224.tar.gz", hash = "sha256:ca5ff7eee3c190eedd7cbdf05bff69c5c0ae357aef64f89dcc42e4872c6ebe7c"}, +] zxcvbn = [ {file = "zxcvbn-4.4.28.tar.gz", hash = "sha256:151bd816817e645e9064c354b13544f85137ea3320ca3be1fb6873ea75ef7dc1"}, ] diff --git a/pyproject.toml b/pyproject.toml index dc2df09..8eb751f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ python-slugify = "^4.0.1" fastapi_utils = "^0.2.1" fastapi-etag = "^0.2.2" chevron = "^0.13.1" +yagmail = "^0.11.224" [tool.poetry.dev-dependencies] black = "^19.10b0" diff --git a/requirements.txt b/requirements.txt index 028de56..6166412 100644 --- a/requirements.txt +++ b/requirements.txt @@ -56,29 +56,35 @@ cffi==1.14.2 \ chardet==3.0.4 \ --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 \ --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae +chevron==0.13.1 \ + --hash=sha256:95b0a055ef0ada5eb061d60be64a7f70670b53372ccd221d1b88adf1c41a9094 \ + --hash=sha256:f95054a8b303268ebf3efd6bdfc8c1b428d3fc92327913b4e236d062ec61c989 click==7.1.2 \ --hash=sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc \ --hash=sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a -cryptography==3.0 \ - --hash=sha256:ab49edd5bea8d8b39a44b3db618e4783ef84c19c8b47286bf05dfdb3efb01c83 \ - --hash=sha256:124af7255ffc8e964d9ff26971b3a6153e1a8a220b9a685dc407976ecb27a06a \ - --hash=sha256:51e40123083d2f946794f9fe4adeeee2922b581fa3602128ce85ff813d85b81f \ - --hash=sha256:dea0ba7fe6f9461d244679efa968d215ea1f989b9c1957d7f10c21e5c7c09ad6 \ - --hash=sha256:8ecf9400d0893836ff41b6f977a33972145a855b6efeb605b49ee273c5e6469f \ - --hash=sha256:0c608ff4d4adad9e39b5057de43657515c7da1ccb1807c3a27d4cf31fc923b4b \ - --hash=sha256:bec7568c6970b865f2bcebbe84d547c52bb2abadf74cefce396ba07571109c67 \ - --hash=sha256:0cbfed8ea74631fe4de00630f4bb592dad564d57f73150d6f6796a24e76c76cd \ - --hash=sha256:a09fd9c1cca9a46b6ad4bea0a1f86ab1de3c0c932364dbcf9a6c2a5eeb44fa77 \ - --hash=sha256:ce82cc06588e5cbc2a7df3c8a9c778f2cb722f56835a23a68b5a7264726bb00c \ - --hash=sha256:9367d00e14dee8d02134c6c9524bb4bd39d4c162456343d07191e2a0b5ec8b3b \ - --hash=sha256:384d7c681b1ab904fff3400a6909261cae1d0939cc483a68bdedab282fb89a07 \ - --hash=sha256:4d355f2aee4a29063c10164b032d9fa8a82e2c30768737a2fd56d256146ad559 \ - --hash=sha256:45741f5499150593178fc98d2c1a9c6722df88b99c821ad6ae298eff0ba1ae71 \ - --hash=sha256:8ecef21ac982aa78309bb6f092d1677812927e8b5ef204a10c326fc29f1367e2 \ - --hash=sha256:4b9303507254ccb1181d1803a2080a798910ba89b1a3c9f53639885c90f7a756 \ - --hash=sha256:8713ddb888119b0d2a1462357d5946b8911be01ddbf31451e1d07eaa5077a261 \ - --hash=sha256:bea0b0468f89cdea625bb3f692cd7a4222d80a6bdafd6fb923963f2b9da0e15f \ - --hash=sha256:8e924dbc025206e97756e8903039662aa58aa9ba357d8e1d8fc29e3092322053 +cryptography==3.1 \ + --hash=sha256:969ae512a250f869c1738ca63be843488ff5cc031987d302c1f59c7dbe1b225f \ + --hash=sha256:b45ab1c6ece7c471f01c56f5d19818ca797c34541f0b2351635a5c9fe09ac2e0 \ + --hash=sha256:247df238bc05c7d2e934a761243bfdc67db03f339948b1e2e80c75d41fc7cc36 \ + --hash=sha256:10c9775a3f31610cf6b694d1fe598f2183441de81cedcf1814451ae53d71b13a \ + --hash=sha256:9f734423eb9c2ea85000aa2476e0d7a58e021bc34f0a373ac52a5454cd52f791 \ + --hash=sha256:e7563eb7bc5c7e75a213281715155248cceba88b11cb4b22957ad45b85903761 \ + --hash=sha256:94191501e4b4009642be21dde2a78bd3c2701a81ee57d3d3d02f1d99f8b64a9e \ + --hash=sha256:dc3f437ca6353979aace181f1b790f0fc79e446235b14306241633ab7d61b8f8 \ + --hash=sha256:725875681afe50b41aee7fdd629cedbc4720bab350142b12c55c0a4d17c7416c \ + --hash=sha256:321761d55fb7cb256b771ee4ed78e69486a7336be9143b90c52be59d7657f50f \ + --hash=sha256:2a27615c965173c4c88f2961cf18115c08fedfb8bdc121347f26e8458dc6d237 \ + --hash=sha256:e7dad66a9e5684a40f270bd4aee1906878193ae50a4831922e454a2a457f1716 \ + --hash=sha256:4005b38cd86fc51c955db40b0f0e52ff65340874495af72efabb1bb8ca881695 \ + --hash=sha256:cc6096c86ec0de26e2263c228fb25ee01c3ff1346d3cfc219d67d49f303585af \ + --hash=sha256:2e26223ac636ca216e855748e7d435a1bf846809ed12ed898179587d0cf74618 \ + --hash=sha256:7a63e97355f3cd77c94bd98c59cb85fe0efd76ea7ef904c9b0316b5bbfde6ed1 \ + --hash=sha256:4b9e96543d0784acebb70991ebc2dbd99aa287f6217546bb993df22dd361d41c \ + --hash=sha256:eb80a288e3cfc08f679f95da72d2ef90cb74f6d8a8ba69d2f215c5e110b2ca32 \ + --hash=sha256:180c9f855a8ea280e72a5d61cf05681b230c2dce804c48e9b2983f491ecc44ed \ + --hash=sha256:fa7fbcc40e2210aca26c7ac8a39467eae444d90a2c346cbcffd9133a166bcc67 \ + --hash=sha256:548b0818e88792318dc137d8b1ec82a0ab0af96c7f0603a00bb94f896fbf5e10 \ + --hash=sha256:26409a473cc6278e4c90f782cd5968ebad04d3911ed1c402fc86908c17633e08 dnspython==2.0.0 \ --hash=sha256:40bb3c24b9d4ec12500f0124288a65df232a3aa749bb0c39734b782873a2544d \ --hash=sha256:044af09374469c3a39eeea1a146e8cac27daec951f1f1f157b1962fc7cb9d1b7 @@ -357,5 +363,8 @@ websockets==8.1 \ --hash=sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36 \ --hash=sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b \ --hash=sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f +yagmail==0.11.224 \ + --hash=sha256:a055d78eca8846bee8329b1bd337599b0b09462d4d24471fba44db30dbfec03f \ + --hash=sha256:ca5ff7eee3c190eedd7cbdf05bff69c5c0ae357aef64f89dcc42e4872c6ebe7c zxcvbn==4.4.28 \ --hash=sha256:151bd816817e645e9064c354b13544f85137ea3320ca3be1fb6873ea75ef7dc1 diff --git a/requirements_dev.txt b/requirements_dev.txt index b25294b..e309dc7 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -76,6 +76,9 @@ cfgv==3.2.0 \ chardet==3.0.4 \ --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 \ --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae +chevron==0.13.1 \ + --hash=sha256:95b0a055ef0ada5eb061d60be64a7f70670b53372ccd221d1b88adf1c41a9094 \ + --hash=sha256:f95054a8b303268ebf3efd6bdfc8c1b428d3fc92327913b4e236d062ec61c989 click==7.1.2 \ --hash=sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc \ --hash=sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a @@ -121,26 +124,29 @@ coverage==5.2.1 \ --hash=sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89 \ --hash=sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b \ --hash=sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b -cryptography==3.0 \ - --hash=sha256:ab49edd5bea8d8b39a44b3db618e4783ef84c19c8b47286bf05dfdb3efb01c83 \ - --hash=sha256:124af7255ffc8e964d9ff26971b3a6153e1a8a220b9a685dc407976ecb27a06a \ - --hash=sha256:51e40123083d2f946794f9fe4adeeee2922b581fa3602128ce85ff813d85b81f \ - --hash=sha256:dea0ba7fe6f9461d244679efa968d215ea1f989b9c1957d7f10c21e5c7c09ad6 \ - --hash=sha256:8ecf9400d0893836ff41b6f977a33972145a855b6efeb605b49ee273c5e6469f \ - --hash=sha256:0c608ff4d4adad9e39b5057de43657515c7da1ccb1807c3a27d4cf31fc923b4b \ - --hash=sha256:bec7568c6970b865f2bcebbe84d547c52bb2abadf74cefce396ba07571109c67 \ - --hash=sha256:0cbfed8ea74631fe4de00630f4bb592dad564d57f73150d6f6796a24e76c76cd \ - --hash=sha256:a09fd9c1cca9a46b6ad4bea0a1f86ab1de3c0c932364dbcf9a6c2a5eeb44fa77 \ - --hash=sha256:ce82cc06588e5cbc2a7df3c8a9c778f2cb722f56835a23a68b5a7264726bb00c \ - --hash=sha256:9367d00e14dee8d02134c6c9524bb4bd39d4c162456343d07191e2a0b5ec8b3b \ - --hash=sha256:384d7c681b1ab904fff3400a6909261cae1d0939cc483a68bdedab282fb89a07 \ - --hash=sha256:4d355f2aee4a29063c10164b032d9fa8a82e2c30768737a2fd56d256146ad559 \ - --hash=sha256:45741f5499150593178fc98d2c1a9c6722df88b99c821ad6ae298eff0ba1ae71 \ - --hash=sha256:8ecef21ac982aa78309bb6f092d1677812927e8b5ef204a10c326fc29f1367e2 \ - --hash=sha256:4b9303507254ccb1181d1803a2080a798910ba89b1a3c9f53639885c90f7a756 \ - --hash=sha256:8713ddb888119b0d2a1462357d5946b8911be01ddbf31451e1d07eaa5077a261 \ - --hash=sha256:bea0b0468f89cdea625bb3f692cd7a4222d80a6bdafd6fb923963f2b9da0e15f \ - --hash=sha256:8e924dbc025206e97756e8903039662aa58aa9ba357d8e1d8fc29e3092322053 +cryptography==3.1 \ + --hash=sha256:969ae512a250f869c1738ca63be843488ff5cc031987d302c1f59c7dbe1b225f \ + --hash=sha256:b45ab1c6ece7c471f01c56f5d19818ca797c34541f0b2351635a5c9fe09ac2e0 \ + --hash=sha256:247df238bc05c7d2e934a761243bfdc67db03f339948b1e2e80c75d41fc7cc36 \ + --hash=sha256:10c9775a3f31610cf6b694d1fe598f2183441de81cedcf1814451ae53d71b13a \ + --hash=sha256:9f734423eb9c2ea85000aa2476e0d7a58e021bc34f0a373ac52a5454cd52f791 \ + --hash=sha256:e7563eb7bc5c7e75a213281715155248cceba88b11cb4b22957ad45b85903761 \ + --hash=sha256:94191501e4b4009642be21dde2a78bd3c2701a81ee57d3d3d02f1d99f8b64a9e \ + --hash=sha256:dc3f437ca6353979aace181f1b790f0fc79e446235b14306241633ab7d61b8f8 \ + --hash=sha256:725875681afe50b41aee7fdd629cedbc4720bab350142b12c55c0a4d17c7416c \ + --hash=sha256:321761d55fb7cb256b771ee4ed78e69486a7336be9143b90c52be59d7657f50f \ + --hash=sha256:2a27615c965173c4c88f2961cf18115c08fedfb8bdc121347f26e8458dc6d237 \ + --hash=sha256:e7dad66a9e5684a40f270bd4aee1906878193ae50a4831922e454a2a457f1716 \ + --hash=sha256:4005b38cd86fc51c955db40b0f0e52ff65340874495af72efabb1bb8ca881695 \ + --hash=sha256:cc6096c86ec0de26e2263c228fb25ee01c3ff1346d3cfc219d67d49f303585af \ + --hash=sha256:2e26223ac636ca216e855748e7d435a1bf846809ed12ed898179587d0cf74618 \ + --hash=sha256:7a63e97355f3cd77c94bd98c59cb85fe0efd76ea7ef904c9b0316b5bbfde6ed1 \ + --hash=sha256:4b9e96543d0784acebb70991ebc2dbd99aa287f6217546bb993df22dd361d41c \ + --hash=sha256:eb80a288e3cfc08f679f95da72d2ef90cb74f6d8a8ba69d2f215c5e110b2ca32 \ + --hash=sha256:180c9f855a8ea280e72a5d61cf05681b230c2dce804c48e9b2983f491ecc44ed \ + --hash=sha256:fa7fbcc40e2210aca26c7ac8a39467eae444d90a2c346cbcffd9133a166bcc67 \ + --hash=sha256:548b0818e88792318dc137d8b1ec82a0ab0af96c7f0603a00bb94f896fbf5e10 \ + --hash=sha256:26409a473cc6278e4c90f782cd5968ebad04d3911ed1c402fc86908c17633e08 distlib==0.3.1 \ --hash=sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb \ --hash=sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1 @@ -273,9 +279,9 @@ markupsafe==1.1.1 \ mccabe==0.6.1 \ --hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \ --hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f -more-itertools==8.4.0 \ - --hash=sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5 \ - --hash=sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2 +more-itertools==8.5.0 \ + --hash=sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20 \ + --hash=sha256:9b30f12df9393f0d28af9210ff8efe48d10c94f73e5daf886f10c4b0b0b4f03c nanoid==2.0.0 \ --hash=sha256:90aefa650e328cffb0893bbd4c236cfd44c48bc1f2d0b525ecc53c3187b653bb \ --hash=sha256:5a80cad5e9c6e9ae3a41fa2fb34ae189f7cb420b2a5d8f82bd9d23466e4efa68 @@ -567,5 +573,8 @@ websockets==8.1 \ --hash=sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36 \ --hash=sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b \ --hash=sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f +yagmail==0.11.224 \ + --hash=sha256:a055d78eca8846bee8329b1bd337599b0b09462d4d24471fba44db30dbfec03f \ + --hash=sha256:ca5ff7eee3c190eedd7cbdf05bff69c5c0ae357aef64f89dcc42e4872c6ebe7c zxcvbn==4.4.28 \ --hash=sha256:151bd816817e645e9064c354b13544f85137ea3320ca3be1fb6873ea75ef7dc1 From d1d49b6ecf083d2118e24649e50da12d5043a3da Mon Sep 17 00:00:00 2001 From: Ewen Le Bihan Date: Sat, 29 Aug 2020 01:29:36 +0200 Subject: [PATCH 06/15] =?UTF-8?q?=F0=9F=9A=A7=20Define=20.env=20entries=20?= =?UTF-8?q?for=20gmail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 6 ++++++ schoolsyst_api/env.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/.env.example b/.env.example index a2f0b0e..cce1231 100644 --- a/.env.example +++ b/.env.example @@ -2,10 +2,16 @@ # salt for JWTs export SECRET_KEY='' + # github export GITHUB_TOKEN='' export GITHUB_USERNAME='schoolsyst-issues-manager' + # database export ARANGODB_HOST="http://localhost:8529" export ARANGODB_USERNAME="root" export ARANGO_ROOT_PASSWORD="openSesame" + +# send mails (test) +export GMAIL_USERNAME="" +export GMAIL_PASSWORD="" diff --git a/schoolsyst_api/env.py b/schoolsyst_api/env.py index 84b80ce..4fc534a 100644 --- a/schoolsyst_api/env.py +++ b/schoolsyst_api/env.py @@ -8,3 +8,5 @@ class EnvironmentVariables(BaseModel): ARANGODB_USERNAME: str ARANGODB_HOST: AnyHttpUrl ARANGO_ROOT_PASSWORD: str + GMAIL_USERNAME: str + GMAIL_PASSWORD: str From 641cdbf5f3d1612449f2e623faf51ce7f8cc2694 Mon Sep 17 00:00:00 2001 From: Ewen Le Bihan Date: Sat, 29 Aug 2020 01:39:11 +0200 Subject: [PATCH 07/15] =?UTF-8?q?=F0=9F=9A=A7=20Implement=20send=5Fmail=20?= =?UTF-8?q?using=20yagmail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not sure if this'll work specially in the long term but oh well --- schoolsyst_api/email_confirmed_action.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/schoolsyst_api/email_confirmed_action.py b/schoolsyst_api/email_confirmed_action.py index 35e8aa4..a024b53 100644 --- a/schoolsyst_api/email_confirmed_action.py +++ b/schoolsyst_api/email_confirmed_action.py @@ -1,8 +1,10 @@ from datetime import timedelta +from os import getenv from pathlib import Path from typing import Callable, Literal, Union import chevron +import yagmail from fastapi import BackgroundTasks, HTTPException, status from schoolsyst_api.accounts import create_jwt_token, verify_jwt_token from schoolsyst_api.accounts.models import User @@ -12,9 +14,14 @@ class EmailConfirmedAction: name: str callback_url: str token_valid_for: timedelta + email_subject: str def __init__( - self, name: str, callback_url: str, token_valid_for: timedelta, + self, + name: str, + callback_url: str, + token_valid_for: timedelta, + email_subject: str, ) -> None: """ `callback_url` is used as the link to click on in the email. @@ -23,6 +30,7 @@ def __init__( self.name = name self.callback_url = callback_url self.token_valid_for = token_valid_for + self.email_subject = email_subject @property def jwt_sub_format(self) -> str: @@ -40,8 +48,15 @@ def mail_template_path(self) -> str: @property def _send_mail_function(self) -> Callable[[str], bool]: - def send_mail(token: str): - pass + def send_mail(token: str, current_user: User): + yagmail.SMTP(getenv("GMAIL_USERNAME"), getenv("GMAIL_PASSWORD")).send( + to=current_user.email, + subject=self.email_subject.format(username=current_user.username), + contents=[ + self._render_mail_template(token, "html", current_user), + self._render_mail_template(token, "txt", current_user), + ], + ) return send_mail @@ -72,8 +87,7 @@ def send_request(self, current_user: User, tasks: BackgroundTasks) -> None: token = create_jwt_token( self.jwt_sub_format, current_user.username, self.token_valid_for ) - - tasks.add_task(self._send_mail_function, token) + tasks.add_task(self._send_mail_function, token, current_user) def handle_token_verification(self, input_token: str, current_user: User): if not verify_jwt_token( From bc0e9a9a5681889d262e0b4adb39e325c9f8664f Mon Sep 17 00:00:00 2001 From: Ewen Le Bihan Date: Sat, 29 Aug 2020 01:45:23 +0200 Subject: [PATCH 08/15] =?UTF-8?q?=E2=9C=85=20Remove=20email=20confirmation?= =?UTF-8?q?=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/test_accounts_email_confirmation.py | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 tests/unit/test_accounts_email_confirmation.py diff --git a/tests/unit/test_accounts_email_confirmation.py b/tests/unit/test_accounts_email_confirmation.py deleted file mode 100644 index f0d9b5a..0000000 --- a/tests/unit/test_accounts_email_confirmation.py +++ /dev/null @@ -1,16 +0,0 @@ -import os -from datetime import timedelta - -from jose import jwt -from schoolsyst_api.accounts.email_confirmation import ( - JWT_SUB_FORMAT, - create_email_confirmation_request_token, -) - - -def test_create_email_confirmation_request_token(): - token = create_email_confirmation_request_token("ewen-lbh", timedelta(hours=24)) - decoded = jwt.decode( - token, key=os.getenv("SECRET_KEY"), subject=JWT_SUB_FORMAT.format("ewen-lbh") - ) - assert decoded From c958a99145cf5f5aa2cf6536ac7ee343284f20ef Mon Sep 17 00:00:00 2001 From: Ewen Le Bihan Date: Sat, 29 Aug 2020 01:46:39 +0200 Subject: [PATCH 09/15] =?UTF-8?q?=F0=9F=90=9B=20Fix=20`EmailConfirmedActio?= =?UTF-8?q?n`=20initialization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- schoolsyst_api/accounts/email_confirmation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/schoolsyst_api/accounts/email_confirmation.py b/schoolsyst_api/accounts/email_confirmation.py index f42e595..2a13c0b 100644 --- a/schoolsyst_api/accounts/email_confirmation.py +++ b/schoolsyst_api/accounts/email_confirmation.py @@ -18,6 +18,7 @@ name="email_confirmation", callback_url="https://app.schoolsyst.com/confirm_email/{}/", token_valid_for=VALID_FOR, + email_subject="schoolsyst: Confirmation d'email - {}", ) From 2834cf8d9c476ad0b83aa138eda6874665fdd42f Mon Sep 17 00:00:00 2001 From: Ewen Le Bihan Date: Mon, 31 Aug 2020 13:57:37 +0200 Subject: [PATCH 10/15] =?UTF-8?q?=F0=9F=90=9B=20Fix=20missing=20parameter?= =?UTF-8?q?=20in=20helper=20instatiation=20at=20password=5Freset.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- schoolsyst_api/accounts/password_reset.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/schoolsyst_api/accounts/password_reset.py b/schoolsyst_api/accounts/password_reset.py index f4ea3f7..e023386 100644 --- a/schoolsyst_api/accounts/password_reset.py +++ b/schoolsyst_api/accounts/password_reset.py @@ -21,7 +21,10 @@ VALID_FOR = timedelta(minutes=30) helper = EmailConfirmedAction( - name="password_reset", callback_url="/reset-password", token_valid_for=VALID_FOR, + name="password_reset", + callback_url="/reset-password", + token_valid_for=VALID_FOR, + email_subject="schoolsyst: Réinitialisation de mot de passe - {}", ) From 1cc8bbe9b15e7cb61e970af286cd410dfd73fb70 Mon Sep 17 00:00:00 2001 From: Ewen Le Bihan Date: Mon, 31 Aug 2020 13:58:01 +0200 Subject: [PATCH 11/15] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20EmailConf?= =?UTF-8?q?irmedAction=20a=20bit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- schoolsyst_api/email_confirmed_action.py | 38 +++++++++++++++++++----- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/schoolsyst_api/email_confirmed_action.py b/schoolsyst_api/email_confirmed_action.py index a024b53..8a042d9 100644 --- a/schoolsyst_api/email_confirmed_action.py +++ b/schoolsyst_api/email_confirmed_action.py @@ -31,30 +31,40 @@ def __init__( self.callback_url = callback_url self.token_valid_for = token_valid_for self.email_subject = email_subject + self._templates_directory = ( + Path(__file__).parent.parent / "static" / "email_templates" / "dist" + ) @property def jwt_sub_format(self) -> str: return f"{self.name}:{{}}" @property - def mail_template_path(self) -> str: + def template_pseudo_filepath(self) -> Path: """ The mail template (.html compiled from .mjml file)'s path, assuming the project root as the current directory. The extension still needs to be added to get either the HTML version (.html) or the plain-text one (.txt) """ - return str(Path("static") / "mail_templates" / "dist" / self.name) + return Path(self._templates_directory) / self.name + + def template_filepath(self, fmt: Union[Literal["html"], Literal["txt"]]) -> Path: + return Path(f"{self.template_pseudo_filepath}.{fmt}") @property - def _send_mail_function(self) -> Callable[[str], bool]: + def _send_mail_function(self) -> Callable[[str, User], None]: def send_mail(token: str, current_user: User): yagmail.SMTP(getenv("GMAIL_USERNAME"), getenv("GMAIL_PASSWORD")).send( to=current_user.email, subject=self.email_subject.format(username=current_user.username), contents=[ - self._render_mail_template(token, "html", current_user), - self._render_mail_template(token, "txt", current_user), + self._render_mail_template_file( + "html", current_user=current_user, token=token + ), + self._render_mail_template_file( + "txt", current_user=current_user, token=token + ), ], ) @@ -63,18 +73,30 @@ def send_mail(token: str, current_user: User): def _action_url(self, token: str) -> str: return self.callback_url.format(token) - def _render_mail_template( + def _render_mail_template_file( self, - token: str, fmt: Union[Literal["html"], Literal["txt"]], + token: str, current_user: User, ) -> str: """ Renders the mail template (at `self.mail_template_path()`) and returns the HTML/plaintext string """ + return self._render_mail_template( + template=Path(self.template_filepath(fmt)).read_text(), + token=token, + current_user=current_user, + ) + + def _render_mail_template( + self, template: str, token: str, current_user: User + ) -> str: + """ + Renders the mail template given and returns the rendered string + """ return chevron.render( - Path(f"{self.mail_template_path}.{fmt}").read_text(), + template, { "url": self._action_url(token), "token": token, From b1efce5c865d78056858cf1bfca5f867213c1deb Mon Sep 17 00:00:00 2001 From: Ewen Le Bihan Date: Mon, 31 Aug 2020 13:58:32 +0200 Subject: [PATCH 12/15] =?UTF-8?q?=F0=9F=9A=A7=20=E2=9C=85=20Start=20testin?= =?UTF-8?q?g=20EmailConfirmedAction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/test_email_confirmed_action.py | 83 +++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 tests/unit/test_email_confirmed_action.py diff --git a/tests/unit/test_email_confirmed_action.py b/tests/unit/test_email_confirmed_action.py new file mode 100644 index 0000000..e42e1ff --- /dev/null +++ b/tests/unit/test_email_confirmed_action.py @@ -0,0 +1,83 @@ +from datetime import timedelta +from pathlib import Path + +from schoolsyst_api.email_confirmed_action import EmailConfirmedAction +from tests import mocks + +MOCKS_MAIL_TEMPLATES_DIR = ( + Path(__file__).parent.parent / "mocks" / "email_templates" / "dist" +) + +eca = EmailConfirmedAction( + name="lorem", + callback_url="ipsum://{}", + token_valid_for=timedelta(1), + email_subject="Ut id ea voluptate enim laborum.", +) +eca._templates_directory = MOCKS_MAIL_TEMPLATES_DIR + + +def test_jwt_sub_format(): + assert eca.jwt_sub_format == "lorem:{}" + + +def test_template_pseudo_filepath(): + assert eca.template_pseudo_filepath == MOCKS_MAIL_TEMPLATES_DIR / "lorem" + + +def test_template_filepath(): + assert eca.template_filepath("html") == MOCKS_MAIL_TEMPLATES_DIR / "lorem.html" + assert eca.template_filepath("txt") == MOCKS_MAIL_TEMPLATES_DIR / "lorem.txt" + + +def test_send_mail_function(): + assert callable(eca._send_mail_function) + + +def test_action_url(): + assert eca._action_url("hmmm") == "ipsum://hmmm" + + +def test_render_mail_template(): + assert ( + eca._render_mail_template( + template="""\ +

Hello, {{username}}.

+Please click on here +Token: {{token}} +Sent to your email +""", + current_user=mocks.users.alice, + token="hmmm", + ) + == f"""\ +

Hello, {mocks.users.alice.username}.

+Please click on here +Token: hmmm +Sent to your email +""" + ) + + +def test_render_mail_template_file(): + assert ( + eca._render_mail_template_file( + "html", token="hmmm", current_user=mocks.users.alice + ) + == f"""\ +

Hello, {mocks.users.alice.username}.

+Please click on here +Token: hmmm +Sent to your email +""" + ) + assert ( + eca._render_mail_template_file( + "txt", token="hmmm", current_user=mocks.users.alice + ) + == f"""\ +Hello, {mocks.users.alice.username}. + +Please click here: ipsum://hmmm Token: hmmm Sent to your email: {mocks.users.alice.email} +""" + ) From 0ad45ed8c50049fffa967db5492d5930be79a0c6 Mon Sep 17 00:00:00 2001 From: Ewen Le Bihan Date: Sat, 21 Nov 2020 23:34:43 +0100 Subject: [PATCH 13/15] =?UTF-8?q?=F0=9F=90=9B=20Get=20rid=20of=20plaintext?= =?UTF-8?q?=20copy=20in=20email=20content?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Was sent along the HTML but not as an alternative This is not a long-term solution though. --- schoolsyst_api/email_confirmed_action.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/schoolsyst_api/email_confirmed_action.py b/schoolsyst_api/email_confirmed_action.py index 8a042d9..00ebf17 100644 --- a/schoolsyst_api/email_confirmed_action.py +++ b/schoolsyst_api/email_confirmed_action.py @@ -62,9 +62,6 @@ def send_mail(token: str, current_user: User): self._render_mail_template_file( "html", current_user=current_user, token=token ), - self._render_mail_template_file( - "txt", current_user=current_user, token=token - ), ], ) @@ -105,7 +102,13 @@ def _render_mail_template( }, ) - def send_request(self, current_user: User, tasks: BackgroundTasks) -> None: + def _send_request(self, current_user: User) -> None: + token = create_jwt_token( + self.jwt_sub_format, current_user.username, self.token_valid_for + ) + self._send_mail_function(token, current_user) + + def queue_request_sending(self, current_user: User, tasks: BackgroundTasks) -> None: token = create_jwt_token( self.jwt_sub_format, current_user.username, self.token_valid_for ) From 9c9ab4cb989e107aa32511ff4614b2b93a6a86cf Mon Sep 17 00:00:00 2001 From: Ewen Le Bihan Date: Sat, 21 Nov 2020 23:36:36 +0100 Subject: [PATCH 14/15] =?UTF-8?q?=E2=9C=85=20=F0=9F=9A=A7=20Test=20`=5Fsen?= =?UTF-8?q?d=5Frequest`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/test_email_confirmed_action.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_email_confirmed_action.py b/tests/unit/test_email_confirmed_action.py index e42e1ff..dfb7905 100644 --- a/tests/unit/test_email_confirmed_action.py +++ b/tests/unit/test_email_confirmed_action.py @@ -1,6 +1,8 @@ -from datetime import timedelta +from datetime import datetime, timedelta +from os import getenv from pathlib import Path +from schoolsyst_api.accounts.models import User from schoolsyst_api.email_confirmed_action import EmailConfirmedAction from tests import mocks @@ -81,3 +83,20 @@ def test_render_mail_template_file(): Please click here: ipsum://hmmm Token: hmmm Sent to your email: {mocks.users.alice.email} """ ) + + +def test_send_request(): + print( + f"""Running with gmail creds: +------------------------- +un {getenv('GMAIL_USERNAME')!r} +pw {getenv('GMAIL_PASSWORD')!r}""" + ) + eca._send_request( + User( + username="ewen-lbh", + email=getenv("GMAIL_PASSWORD"), + email_is_confirmed=True, + joined_at=datetime.now(), + ) + ) From e5db20bdfcf9a6acfffd1367d14150a0d62eaf4a Mon Sep 17 00:00:00 2001 From: Ewen Le Bihan Date: Sat, 21 Nov 2020 23:40:09 +0100 Subject: [PATCH 15/15] =?UTF-8?q?=F0=9F=92=9A=20Add=20GMAIL=5F*=20env=20va?= =?UTF-8?q?riables=20in=20travis=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 6ebffb5..06210a7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,8 @@ before_install: - echo -ne "ARANGO_ROOT_PASSWORD=\"openSesame\"\n" >> .env - echo -ne "GITHUB_USERNAME=\"$GITHUB_USERNAME\"\n" >> .env - echo -ne "GITHUB_TOKEN=\"$GITHUB_TOKEN\"\n" >> .env + - echo -ne "GMAIL_USERNAME=\"$GMAIL_USERNAME\"\n" >> .env + - echo -ne "GMAIL_PASSWORD=\"$GMAIL_PASSWORD\"\n" >> .env - pip install 'poetry>=1.0' - docker run -d -p 8529:8529 -e ARANGO_ROOT_PASSWORD=openSesame arangodb/arangodb:3.6.5