diff --git a/backend/src/backend/.env b/backend/src/backend/.env index 6bff5c7..3fcde65 100644 --- a/backend/src/backend/.env +++ b/backend/src/backend/.env @@ -1,4 +1,5 @@ BACKEND_URL="http://localhost:8000" FRONTEND_URL="http://localhost:4321" SECRET="IamALocalSecretAndMustBeReplacedInProduction" -ENVIRONMENT="local" \ No newline at end of file +ENVIRONMENT="local" +MAILTRAP_API_TOKEN="FakedyFake" \ No newline at end of file diff --git a/backend/src/backend/__pycache__/config.cpython-314.pyc b/backend/src/backend/__pycache__/config.cpython-314.pyc index d9ad864..25663f5 100644 Binary files a/backend/src/backend/__pycache__/config.cpython-314.pyc and b/backend/src/backend/__pycache__/config.cpython-314.pyc differ diff --git a/backend/src/backend/config.py b/backend/src/backend/config.py index e2ab97c..b902cdc 100644 --- a/backend/src/backend/config.py +++ b/backend/src/backend/config.py @@ -4,3 +4,4 @@ ENVIRONMENT = os.environ["ENVIRONMENT"] SECRET_KEY = os.environ["SECRET"] FRONTEND_URL = os.environ["FRONTEND_URL"] BACKEND_URL = os.environ["BACKEND_URL"] +MAILTRAP_API_TOKEN = os.environ["MAILTRAP_API_TOKEN"] \ No newline at end of file diff --git a/backend/src/backend/internal/__pycache__/authentication.cpython-314.pyc b/backend/src/backend/internal/__pycache__/authentication.cpython-314.pyc index 6c8df75..22f024c 100644 Binary files a/backend/src/backend/internal/__pycache__/authentication.cpython-314.pyc and b/backend/src/backend/internal/__pycache__/authentication.cpython-314.pyc differ diff --git a/backend/src/backend/internal/__pycache__/mail_util.cpython-314.pyc b/backend/src/backend/internal/__pycache__/mail_util.cpython-314.pyc index e56b7ef..6f0aaaf 100644 Binary files a/backend/src/backend/internal/__pycache__/mail_util.cpython-314.pyc and b/backend/src/backend/internal/__pycache__/mail_util.cpython-314.pyc differ diff --git a/backend/src/backend/internal/__pycache__/rss_reader.cpython-314.pyc b/backend/src/backend/internal/__pycache__/rss_reader.cpython-314.pyc index 3fe9348..e1288e6 100644 Binary files a/backend/src/backend/internal/__pycache__/rss_reader.cpython-314.pyc and b/backend/src/backend/internal/__pycache__/rss_reader.cpython-314.pyc differ diff --git a/backend/src/backend/internal/authentication.py b/backend/src/backend/internal/authentication.py index 478e03e..ab3c901 100644 --- a/backend/src/backend/internal/authentication.py +++ b/backend/src/backend/internal/authentication.py @@ -1,3 +1,4 @@ +from typing import Any from backend.config import SECRET_KEY import os from fastapi.security import HTTPBasicCredentials @@ -34,10 +35,13 @@ def get_token(credentials = Security(security)) -> str: return parts[1] raise HTTPException(status_code=403, detail="Invalid authorization format") +def decode_token(token: str)-> str | None: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + return payload.get("sub") + async def get_current_user(token: str = Depends(get_token), session: Session = Depends(get_session)): try: - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - user_code: str | None = payload.get("sub") + user_code: str | None = decode_token(token) if user_code is None: raise HTTPException(status_code=401, detail="Invalid token") except PyJWTError: diff --git a/backend/src/backend/internal/jinja_templates/welcome.jinja b/backend/src/backend/internal/jinja_templates/welcome.jinja index d6ad6f6..6322cca 100644 --- a/backend/src/backend/internal/jinja_templates/welcome.jinja +++ b/backend/src/backend/internal/jinja_templates/welcome.jinja @@ -12,7 +12,7 @@

Dein persönlicher Code, um alle Funktionen nutzen zu können: {{data.user_code}}

- + Daten löschen diff --git a/backend/src/backend/internal/mail_util.py b/backend/src/backend/internal/mail_util.py index d8bc083..0cfa910 100644 --- a/backend/src/backend/internal/mail_util.py +++ b/backend/src/backend/internal/mail_util.py @@ -1,3 +1,6 @@ +from backend.config import MAILTRAP_API_TOKEN +from mailtrap import BatchEmailRequest +from mailtrap import BatchSendEmailParams from backend.config import ENVIRONMENT from mailtrap import Mail import mailtrap as mt @@ -22,30 +25,44 @@ class AdminApprovalData(ArticleData): secret: str -def send_newsletter_admin_approval(data: ArticleData ): +def create_html_from_template(template_name: str, data): env = Environment(loader = FileSystemLoader('src/backend/internal/jinja_templates')) - template = env.get_template('approval.jinja') - output = template.render(data=data) + template = env.get_template(f"{template_name}.jinja") + return template.render(data=data) + +def send_newsletter_admin_approval(data: ArticleData ): + html = create_html_from_template("approval", data) mail = mt.Mail( sender=mt.Address(email="newsletter@blauwasser-mausis.de", name="Flaschenpost"), to=[mt.Address(email="blauwasser-mausis@proton.me")], subject=f"[Approval] {data.title}", - text=output, + html=html, ) send_mail(mail) - def send_mail(mail: Mail): if ENVIRONMENT == "local": print(f"Mail sent: {mail.subject}") + print(mail.text) else: - client = mt.MailtrapClient(token="") + client = mt.MailtrapClient(token=MAILTRAP_API_TOKEN) client.send(mail) -def send_batch_mail(): - mt.BatchMail( - sender=mt.Address(email="newsletter@blauwasser-mausis.de", name="Flaschenpost"), - subject="Flaschenpost | Blauwasser-Mausis" - +def send_batch_mail(mails: list[BatchEmailRequest]): + if ENVIRONMENT == "local": + for mail in mails: + print(f"Mail sent: {mail.subject} to {mail.to}") + else: + client = mt.MailtrapClient(token=MAILTRAP_API_TOKEN) + params = BatchSendEmailParams( + base={ + "from": { + "name": "Flaschenpost", + "email": "newsletter@blauwasser-mausis.de" + }, + "subject": "Flaschenpost | Blauwasser Mausis", + }, + requests=mails ) + client.batch_send(params) \ No newline at end of file diff --git a/backend/src/backend/internal/rss_reader.py b/backend/src/backend/internal/rss_reader.py index 0e2cb24..c7e00d8 100644 --- a/backend/src/backend/internal/rss_reader.py +++ b/backend/src/backend/internal/rss_reader.py @@ -1,4 +1,5 @@ +from feedparser import FeedParserDict from backend.config import FRONTEND_URL from backend.internal.authentication import create_access_token from backend.internal.mail_util import AdminApprovalData @@ -18,15 +19,14 @@ def check_blog_rss_and_send_admin_mail(): description = str(article["summary"]) subtitle = str(article["subtitle"]) link = str(article["link"]) - secret = create_access_token({"sub": title}) - + data = AdminApprovalData(title, subtitle, description, link, secret) - send_newsletter_admin_approval(data) - - # TODO: create link - # TODO: create Template with jinja2 - # TODO: send mail to blauwasser-mausis@proton.me - - + +def find_article_by_title(title: str) -> FeedParserDict | None: + feed = feedparser.parse(f"{FRONTEND_URL}/rss.xml") + for article in feed.entries: + if title == str(article["title"]): + return article + return None diff --git a/backend/src/backend/routers/__pycache__/newsletters.cpython-314.pyc b/backend/src/backend/routers/__pycache__/newsletters.cpython-314.pyc index 8ae8574..6717443 100644 Binary files a/backend/src/backend/routers/__pycache__/newsletters.cpython-314.pyc and b/backend/src/backend/routers/__pycache__/newsletters.cpython-314.pyc differ diff --git a/backend/src/backend/routers/__pycache__/users.cpython-314.pyc b/backend/src/backend/routers/__pycache__/users.cpython-314.pyc index a18358a..52ed5ef 100644 Binary files a/backend/src/backend/routers/__pycache__/users.cpython-314.pyc and b/backend/src/backend/routers/__pycache__/users.cpython-314.pyc differ diff --git a/backend/src/backend/routers/newsletters.py b/backend/src/backend/routers/newsletters.py index 0b37d7a..170031a 100644 --- a/backend/src/backend/routers/newsletters.py +++ b/backend/src/backend/routers/newsletters.py @@ -1,3 +1,16 @@ +from backend.internal.mail_util import create_html_from_template +from mailtrap import BatchEmailRequest +from mailtrap import BatchMail +from backend.internal.mail_util import send_batch_mail +from starlette.status import HTTP_400_BAD_REQUEST +from fastapi import HTTPException +from sqlmodel import select +from backend.models.user import User +from sqlmodel import Session +from backend.internal.database import get_session +from fastapi import Depends +from backend.internal.rss_reader import find_article_by_title +from backend.internal.authentication import decode_token from fastapi import Form from fastapi import APIRouter @@ -12,5 +25,20 @@ router = APIRouter( "/approve", status_code=201 ) -def approve_newsletter(secret: str = Form()): - pass \ No newline at end of file +def approve_newsletter(secret: str = Form(), session: Session = Depends(get_session)): + title = decode_token(secret) + if title is not None: + article = find_article_by_title(title) + if article is not None: + subscribed_users = session.exec(select(User).where(User.subscription == True)) + mails: list[BatchEmailRequest] = [] + for user in subscribed_users: + html = create_html_from_template("newsletter", user) + mail = BatchEmailRequest(to=user.email,html=html) + mails.append(mail) + send_batch_mail(mails) + + else: + raise HTTPException(HTTP_400_BAD_REQUEST) + + \ No newline at end of file diff --git a/backend/src/backend/routers/users.py b/backend/src/backend/routers/users.py index 00949a4..9d178ab 100644 --- a/backend/src/backend/routers/users.py +++ b/backend/src/backend/routers/users.py @@ -1,3 +1,6 @@ +from backend.internal.mail_util import send_mail +from backend.internal.mail_util import create_html_from_template +from mailtrap import Mail from pyrate_limiter import Duration from pyrate_limiter import Rate from pyrate_limiter import Limiter @@ -28,18 +31,26 @@ router = APIRouter( status_code=201, dependencies=[Depends(RateLimiter(limiter=Limiter(Rate(1, Duration.MINUTE))))] ) -def register_user(new_user: NewUser, session: Session = Depends(get_session)) -> str: +def register_user(new_user: NewUser, session: Session = Depends(get_session)): user = User.model_validate(new_user) try: session.add(user) session.commit() session.refresh(user) - # TODO: send mail + + mail = Mail( + to=user.email, + sender="noreply@blauwasser-mausis.de", + subject=f"[Registrierung] Willkommen {user.name} | Blauwasser Mausis", + html=create_html_from_template("welcome", user) + ) + send_mail(mail) + except IntegrityError as e: session.rollback() raise HTTPException(HTTP_400_BAD_REQUEST) - return user.user_code + @router.post( "/login", @@ -53,6 +64,7 @@ def login(user_code: str = Form(), session: Session = Depends(get_session)) -> s raise HTTPException(HTTP_401_UNAUTHORIZED) return create_access_token({"sub": user.user_code}) + @router.post( "/forgot", dependencies=[Depends(RateLimiter(limiter=Limiter(Rate(1, Duration.MINUTE))))] @@ -62,6 +74,7 @@ def forgot_user_code(email: str = Form(), session: Session = Depends(get_session if user is not None: pass # TODO: send Mail + @router.get("/me", status_code=201) def get_current_user_info(current_user: User = Depends(get_current_user)) -> UserInfo: return UserInfo.model_validate(current_user) @@ -72,6 +85,7 @@ def delete(current_user: User = Depends(get_current_user), session: Session = De session.delete(current_user) session.commit() + @router.delete("/me", status_code=201) def delete(user_code: str = Form(), session: Session = Depends(get_session)) : user = session.exec(select(User).where(User.user_code == user_code)).first() @@ -80,14 +94,17 @@ def delete(user_code: str = Form(), session: Session = Depends(get_session)) : session.delete(user) session.commit() + @router.put("me/subscribe", status_code=201) -def change_subscription(subscription: bool = Form(), current_user: User = Depends(get_current_user), session: Session = Depends(get_session)): +def change_subscription( + subscription: bool = Form(), + current_user: User = Depends(get_current_user), + session: Session = Depends(get_session) +): current_user.subscription = subscription session.add(current_user) session.commit() # TODO: -# Template für Willkommen mit delete link -# change mail <- token, new mail -# delete all deactivated users after 48h \ No newline at end of file +# change mail <- token, new mail \ No newline at end of file