From 0c0df083e7931ecad174030b0d156a585c4e3e1c Mon Sep 17 00:00:00 2001 From: Nikita Melkozerov Date: Fri, 12 Jan 2024 10:17:17 +0000 Subject: [PATCH] add the html templates --- fixbackend/auth/user_verifier.py | 16 +- fixbackend/notification/email_service.py | 6 +- fixbackend/notification/messages.py | 91 +++++ fixbackend/notification/templates/invite.html | 349 +++++++++++++++++ fixbackend/notification/templates/signup.html | 354 ++++++++++++++++++ .../notification/templates/verify_email.html | 349 +++++++++++++++++ fixbackend/workspaces/invitation_service.py | 10 +- .../workspaces/invitation_service_test.py | 34 +- 8 files changed, 1174 insertions(+), 35 deletions(-) create mode 100644 fixbackend/notification/messages.py create mode 100644 fixbackend/notification/templates/invite.html create mode 100644 fixbackend/notification/templates/signup.html create mode 100644 fixbackend/notification/templates/verify_email.html diff --git a/fixbackend/auth/user_verifier.py b/fixbackend/auth/user_verifier.py index c72193e8..5b6b9fd1 100644 --- a/fixbackend/auth/user_verifier.py +++ b/fixbackend/auth/user_verifier.py @@ -19,10 +19,11 @@ from fixbackend.auth.models import User from fixbackend.notification.email_service import EmailService, EmailServiceDependency +from fixbackend.notification.messages import VerifyEmail class UserVerifier(ABC): - def plaintext_email_content(self, request: Request, token: str) -> str: + def email_content(self, *, request: Request, user_email: str, token: str) -> VerifyEmail: # redirect is defined by the UI - use / as safe fallback redirect_url = request.query_params.get("redirectUrl", "/") verification_link = request.base_url @@ -30,9 +31,7 @@ def plaintext_email_content(self, request: Request, token: str) -> str: path="/auth/verify-email", query=f"token={token}&redirectUrl={redirect_url}" ) - body_text = f"Hello fellow FIX user, click this link to verify your email. {verification_link}" - - return body_text + return VerifyEmail(recipient=user_email, verification_link=str(verification_link)) @abstractmethod async def verify(self, user: User, token: str, request: Optional[Request]) -> None: @@ -45,14 +44,9 @@ def __init__(self, email_service: EmailService) -> None: async def verify(self, user: User, token: str, request: Optional[Request]) -> None: assert request - body_text = self.plaintext_email_content(request, token) + message = self.email_content(request=request, user_email=user.email, token=token) - await self.email_service.send_email( - to=user.email, - subject="FIX: verify your e-mail address", - text=body_text, - html=None, - ) + await self.email_service.send_message(message=message) def get_user_verifier(email_service: EmailServiceDependency) -> UserVerifier: diff --git a/fixbackend/notification/email_service.py b/fixbackend/notification/email_service.py index 655a0357..4783f87b 100644 --- a/fixbackend/notification/email_service.py +++ b/fixbackend/notification/email_service.py @@ -21,6 +21,7 @@ from fastapi import Depends from fixbackend.config import Config, ConfigDependency +from fixbackend.notification.messages import EmailMessage class EmailService(ABC): @@ -36,6 +37,9 @@ async def send_email( """Send an email to the given address.""" raise NotImplementedError() + async def send_message(self, *, message: EmailMessage) -> None: + await self.send_email(to=message.recipient, subject=message.subject(), text=message.text(), html=message.html()) + class ConsoleEmailService(EmailService): async def send_email( @@ -48,7 +52,7 @@ async def send_email( print(f"Sending email to {to} with subject {subject}") print(f"text: {text}") if html: - print(f"html: {html}") + print(f"html (first 100 chars): {html[:100]}") class EmailServiceImpl(EmailService): diff --git a/fixbackend/notification/messages.py b/fixbackend/notification/messages.py new file mode 100644 index 00000000..30f98903 --- /dev/null +++ b/fixbackend/notification/messages.py @@ -0,0 +1,91 @@ +# Copyright (c) 2023. Some Engineering +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from typing import Any, Union +from attrs import frozen + +from pathlib import Path +from jinja2 import Environment, FileSystemLoader +from functools import lru_cache + + +@lru_cache(maxsize=1) +def get_env() -> Environment: + return Environment(loader=FileSystemLoader(Path(__file__).parent / "templates")) + + +def render(template_name: str, **kwargs: Any) -> str: + template = get_env().get_template(template_name) + return template.render(**kwargs) + + +@frozen(kw_only=True) +class Signup: + recipient: str + + def subject(self) -> str: + return "Welcome to fix!" + + def text(self) -> str: + return f"Welcome to fix, {self.recipient}!" + + def html(self) -> str: + return render("signup.html", title=self.subject(), email=self.recipient) + + +@frozen(kw_only=True) +class Invite: + inviter: str + invitation_link: str + recipient: str + + def subject(self) -> str: + return "You've been invited to join fix!" + + def text(self) -> str: + text = ( + f"{self.inviter} has invited you to join their workspace. " + "Please click on the link below to accept the invitation. \n\n" + f"{self.invitation_link}" + ) + return text + + def html(self) -> str: + return render( + "invite.html", + title=self.subject(), + inviter=self.inviter, + invitation_link=self.invitation_link, + email=self.recipient, + ) + + +@frozen(kw_only=True) +class VerifyEmail: + recipient: str + verification_link: str + + def subject(self) -> str: + return "FIX: verify your e-mail address" + + def text(self) -> str: + return f"Hello fellow FIX user, click this link to verify your email. {self.verification_link}" + + def html(self) -> str: + return render( + "verify_email.html", title=self.subject(), email=self.recipient, verification_link=self.verification_link + ) + + +EmailMessage = Union[Signup, Invite, VerifyEmail] diff --git a/fixbackend/notification/templates/invite.html b/fixbackend/notification/templates/invite.html new file mode 100644 index 00000000..028fdfe1 --- /dev/null +++ b/fixbackend/notification/templates/invite.html @@ -0,0 +1,349 @@ +{# templates/invite.html #} + + + + + + {{ title }} + + + + + + + +
+
+ +
+ Welcome + to FIX! +
+
+
+
+ + {{ inviter }} invited you to join their workspace + +
+
+ + Great news! You've been invited to join Fix, the innovative all-in-one dashboard tailored for security + engineers. Fix streamlines your security management by prioritizing top risks, integrating user, + resource, and configuration data for a comprehensive overview. + +
+ + + +
+ If you have any questions, drop our support team a message at + + support@fix.tt + +
+
+ + This email is intended to: + + + {{ email }} + +
+ + For more information please visit us at + + + fix.tt + +
+ + © 2023 Some Engineering Inc. All rights reserved. + +
+
+ + + + \ No newline at end of file diff --git a/fixbackend/notification/templates/signup.html b/fixbackend/notification/templates/signup.html new file mode 100644 index 00000000..b0873b79 --- /dev/null +++ b/fixbackend/notification/templates/signup.html @@ -0,0 +1,354 @@ +{# templates/signup.html #} + + + + + + {{ title }} + + + + + + + +
+
+ +
+ Thank You + for Signing Up! +
+
+
+
+ + We are glad to see you, {{ email }} + +
+ + You chose FIX to Detect, prioritize, and remediate critical cloud risks. + We're setting up your dashboard and it might take a while. In the + meantime, here are more ways to get in touch with us: + +
+ +
+ + Visit our Blog: + + Ready industry related news, in depth posts of new feature launches and + other related content. +
+ + Connect with FIX on LinkedIn: + + We have a LinkedIn page that we post updates to including new features, + invitations to tech talks and general company news. +
+ + Schedule a call with our team: + + We're always happy to hear from users, learn about their infrastructure + and hear product feedback. +
+
+
+ If you have any questions, drop our support team a message at + + support@fix.tt + +
+
+ + This email is intended to: + + + {{ email }} + +
+ + For more information please visit us at + + + fix.tt + +
+ + © 2023 Some Engineering Inc. All rights reserved. + +
+
+ + + + \ No newline at end of file diff --git a/fixbackend/notification/templates/verify_email.html b/fixbackend/notification/templates/verify_email.html new file mode 100644 index 00000000..938dbfb3 --- /dev/null +++ b/fixbackend/notification/templates/verify_email.html @@ -0,0 +1,349 @@ +{# templates/verify_email.html #} + + + + + + {{ title }} + + + + + + + +
+
+ +
+ Welcome + to FIX! +
+
+
+
+
+ + Please verify your email address + +
+
+
+ + Simply click the link below to finish creating your account. + +
+ +
+ +
+
+
+ If you have any questions, drop our support team a message at + + support@fix.tt + +
+
+ + This email is intended to: + + + {{ email }} + +
+ + For more information please visit us at + + + fix.tt + +
+ + © 2023 Some Engineering Inc. All rights reserved. + +
+
+ + + + \ No newline at end of file diff --git a/fixbackend/workspaces/invitation_service.py b/fixbackend/workspaces/invitation_service.py index 44b835fc..d1fa0d67 100644 --- a/fixbackend/workspaces/invitation_service.py +++ b/fixbackend/workspaces/invitation_service.py @@ -31,6 +31,7 @@ from fixbackend.domain_events.publisher import DomainEventPublisher from fixbackend.ids import InvitationId, WorkspaceId from fixbackend.notification.email_service import EmailService, EmailServiceDependency +from fixbackend.notification.messages import Invite from fixbackend.workspaces.invitation_repository import InvitationRepository, InvitationRepositoryDependency from fixbackend.workspaces.models import WorkspaceInvitation from fixbackend.workspaces.repository import WorkspaceRepository, WorkspaceRepositoryDependency @@ -103,14 +104,9 @@ async def invite_user( } token = generate_state_token(state_data, secret=self.config.secret) - subject = f"FIX Cloud {inviter.email} has invited you to FIX workspace" invite_link = f"{accept_invite_base_url}?token={token}" - text = ( - f"{inviter.email} has invited you to join the workspace {workspace.name}. " - "Please click on the link below to accept the invitation. \n\n" - f"{invite_link}" - ) - await self.email_service.send_email(to=invitee_email, subject=subject, text=text, html=None) + message = Invite(inviter=inviter.email, invitation_link=invite_link, recipient=invitee_email) + await self.email_service.send_message(message=message) return invitation, token async def list_invitations(self, workspace_id: WorkspaceId) -> Sequence[WorkspaceInvitation]: diff --git a/tests/fixbackend/workspaces/invitation_service_test.py b/tests/fixbackend/workspaces/invitation_service_test.py index 74e213f3..4c55a503 100644 --- a/tests/fixbackend/workspaces/invitation_service_test.py +++ b/tests/fixbackend/workspaces/invitation_service_test.py @@ -14,9 +14,9 @@ from typing import Optional, List -from attrs import frozen import pytest from fixbackend.domain_events.events import InvitationAccepted, UserJoinedWorkspace +from fixbackend.notification.messages import EmailMessage from fixbackend.workspaces.invitation_service import InvitationService, InvitationServiceImpl @@ -29,20 +29,22 @@ from tests.fixbackend.conftest import InMemoryDomainEventPublisher -@frozen -class NotificationEmail: - to: str - subject: str - text: str - html: Optional[str] - - class InMemoryEmailService(EmailService): def __init__(self) -> None: - self.call_args: List[NotificationEmail] = [] + self.call_args: List[EmailMessage] = [] + + async def send_email( + self, + *, + to: str, + subject: str, + text: str, + html: Optional[str], + ) -> None: + pass - async def send_email(self, *, to: str, subject: str, text: str, html: str | None) -> None: - self.call_args.append(NotificationEmail(to, subject, text, html)) + async def send_message(self, *, message: EmailMessage) -> None: + self.call_args.append(message) @pytest.fixture @@ -98,10 +100,10 @@ async def test_invite_accept_user( # check email email = email_service.call_args[0] - assert email.to == new_user_email - assert email.subject == f"FIX Cloud {user.email} has invited you to FIX workspace" - assert email.text.startswith(f"{user.email} has invited you to join the workspace {workspace.name}.") - assert "https://example.com?token=" in email.text + assert email.recipient == new_user_email + assert email.subject() == "You've been invited to join fix!" + assert email.text().startswith(f"{user.email} has invited you to join their workspace") + assert "https://example.com?token=" in email.text() # existing user existing_user = await user_repository.create(