Add a link to the newspaper advertising A Study in Scarlett
This commit is contained in:
parent
5ba29b509e
commit
a4b8e4fee3
5 changed files with 146 additions and 1 deletions
|
|
@ -14,7 +14,7 @@ Acceptable Date Justifications:
|
|||
- Unverified Trusted Source (UTS): Something like a bibliography or a physical book being used as a justification which I haven't independently verified.
|
||||
|
||||
### A Study in Scarlet
|
||||
November 21st 1887: [ENA](./evidence/a-study-in-scarlet-ad.png)
|
||||
November 21st 1887: [ENA](./evidence/a-study-in-scarlet-ad.png) from [The Standard November 21st 1887 Edition](https://www.britishnewspaperarchive.com/image-viewer?issue=BL%2F0000183%2F18871121&page=8)
|
||||
This was released on November 21st 1887 in [Beeton's Christmas Annual](https://archive.org/details/beetons-christmas-annual-nov-1887/Beetons-Christmas-Annual-1887/page/1/mode/1up).
|
||||
|
||||
### The Sign of the Four
|
||||
|
|
|
|||
5
src/services/publications/__init__.py
Normal file
5
src/services/publications/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from .data import User, UserProfile, UserDTO, SignupError, LoginError
|
||||
from .service import UserService
|
||||
from .repo import UserRepo
|
||||
|
||||
__all__ = ['User', 'UserProfile', 'UserDTO', 'SignupError', 'LoginError', 'UserService', 'UserRepo']
|
||||
6
src/services/publications/data.py
Normal file
6
src/services/publications/data.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Publication:
|
||||
id = int
|
||||
26
src/services/publications/repo.py
Normal file
26
src/services/publications/repo.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import abc
|
||||
from src.config.email import EmailAddress
|
||||
|
||||
from .data import User, UserDTO
|
||||
|
||||
|
||||
class UserRepo(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def create_user(self, user: UserDTO) -> User:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_user_by_id(self, user_id: int) -> User | None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_user_by_email(self, email: EmailAddress) -> User | None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_user(self, user: User):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def auth_as_user(self, user: UserDTO) -> User | None:
|
||||
pass
|
||||
108
src/services/publications/service.py
Normal file
108
src/services/publications/service.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
from email.headerregistry import Address
|
||||
from typing import TypedDict, Unpack
|
||||
|
||||
from jinja2 import Environment, PackageLoader, select_autoescape
|
||||
|
||||
from src.constants.routes import ROUTES
|
||||
from src.services.auth import AuthService, EmailConfirmationClaim, JWT
|
||||
from src.services.router import Router
|
||||
from src.services.email import EmailService, EmailDTO
|
||||
|
||||
from .data import (
|
||||
UserDTO,
|
||||
User,
|
||||
UserProfile,
|
||||
SignupError,
|
||||
LoginError,
|
||||
EmailConfirmationTokenInvalid,
|
||||
)
|
||||
from .repo import UserRepo
|
||||
|
||||
|
||||
def is_valid_email(email: str) -> bool:
|
||||
try:
|
||||
parsed = Address(addr_spec=email)
|
||||
return bool(parsed.username and parsed.domain and '.' in parsed.domain)
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
|
||||
|
||||
def is_valid_password(password: str) -> bool:
|
||||
# TODO: Enforce other or saner rules?
|
||||
long_enough = len(password) > 8
|
||||
short_enough = len(password) < 32
|
||||
has_symbol = any([s in password for s in list('@#$%^&*!?/')])
|
||||
return long_enough and short_enough and has_symbol
|
||||
|
||||
|
||||
class ServiceDeps(TypedDict):
|
||||
repo: UserRepo
|
||||
email_service: EmailService
|
||||
auth_service: AuthService
|
||||
router: Router
|
||||
|
||||
|
||||
class UserService:
|
||||
def __init__(self, **kwargs: Unpack[ServiceDeps]):
|
||||
self._repo = kwargs['repo']
|
||||
self._email_service = kwargs['email_service']
|
||||
self._auth_service = kwargs['auth_service']
|
||||
self._router = kwargs['router']
|
||||
|
||||
def login(self, user: UserDTO) -> UserProfile:
|
||||
full_user = self._repo.auth_as_user(user)
|
||||
if full_user:
|
||||
return full_user.to_profile()
|
||||
else:
|
||||
raise LoginError('No user found for that email or password')
|
||||
|
||||
def confirm_email_for_user(self, user_id: int, confirmation_token: JWT):
|
||||
"""
|
||||
Attempt to confirm that the user at the given ID has confirmed their email by returning the JWT that was
|
||||
minted for this purpose.
|
||||
"""
|
||||
claim = self._auth_service.validate_token(EmailConfirmationClaim, confirmation_token)
|
||||
if not claim or claim.sub != user_id:
|
||||
raise EmailConfirmationTokenInvalid
|
||||
|
||||
user = self._repo.get_user_by_id(user_id)
|
||||
# If there is no user or the users emails don't match or the user is already confirmed then consider
|
||||
# the token invalid for this request.
|
||||
if not user or user.email != claim.email or user.email_confirmed:
|
||||
raise EmailConfirmationTokenInvalid
|
||||
|
||||
user.email_confirmed = True
|
||||
self._repo.update_user(user)
|
||||
|
||||
def _send_confirmation_email(self, user: User):
|
||||
env = Environment(loader=PackageLoader('src'), autoescape=select_autoescape())
|
||||
text_template = env.get_template('mail/confirmation_email.txt')
|
||||
html_template = env.get_template('mail/confirmation_email.html')
|
||||
token = self._auth_service.mint_claim_from_user(EmailConfirmationClaim, user)
|
||||
|
||||
url = self._router.get_url(ROUTES['confirm_email'], token=token)
|
||||
text_content = text_template.render(confirmation_link=url)
|
||||
html_content = html_template.render(confirmation_link=url)
|
||||
|
||||
email = EmailDTO(to=user.email, subject='Confirm Your Email Address', text=text_content, html=html_content)
|
||||
|
||||
self._email_service.send_email(email)
|
||||
|
||||
def signup(self, user: UserDTO) -> UserProfile:
|
||||
if not is_valid_email(user.email):
|
||||
raise SignupError(f'{user.email} was not an acceptable email address')
|
||||
|
||||
if not is_valid_password(user.raw_password.expose_secret()):
|
||||
raise SignupError('The given password was not acceptable')
|
||||
|
||||
# Create a user in persistence
|
||||
created_user = self._repo.create_user(user)
|
||||
# send a confirmation email
|
||||
self._send_confirmation_email(created_user)
|
||||
|
||||
return created_user.to_profile()
|
||||
|
||||
def get_user_by_id(self, user_id: int) -> UserProfile | None:
|
||||
user = self._repo.get_user_by_id(user_id)
|
||||
if user:
|
||||
return user.to_profile()
|
||||
Loading…
Add table
Add a link
Reference in a new issue