added lots of stuff

This commit is contained in:
ebby 2026-09-27 19:59:59 +02:00
parent c4d64f6ac7
commit ab1497b488
14 changed files with 101 additions and 33 deletions

View file

@ -1,4 +1,5 @@
BACKEND_URL="http://localhost:8000" BACKEND_URL="http://localhost:8000"
FRONTEND_URL="http://localhost:4321" FRONTEND_URL="http://localhost:4321"
SECRET="IamALocalSecretAndMustBeReplacedInProduction" SECRET="IamALocalSecretAndMustBeReplacedInProduction"
ENVIRONMENT="local" ENVIRONMENT="local"
MAILTRAP_API_TOKEN="FakedyFake"

View file

@ -4,3 +4,4 @@ ENVIRONMENT = os.environ["ENVIRONMENT"]
SECRET_KEY = os.environ["SECRET"] SECRET_KEY = os.environ["SECRET"]
FRONTEND_URL = os.environ["FRONTEND_URL"] FRONTEND_URL = os.environ["FRONTEND_URL"]
BACKEND_URL = os.environ["BACKEND_URL"] BACKEND_URL = os.environ["BACKEND_URL"]
MAILTRAP_API_TOKEN = os.environ["MAILTRAP_API_TOKEN"]

View file

@ -1,3 +1,4 @@
from typing import Any
from backend.config import SECRET_KEY from backend.config import SECRET_KEY
import os import os
from fastapi.security import HTTPBasicCredentials from fastapi.security import HTTPBasicCredentials
@ -34,10 +35,13 @@ def get_token(credentials = Security(security)) -> str:
return parts[1] return parts[1]
raise HTTPException(status_code=403, detail="Invalid authorization format") 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)): async def get_current_user(token: str = Depends(get_token), session: Session = Depends(get_session)):
try: try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) user_code: str | None = decode_token(token)
user_code: str | None = payload.get("sub")
if user_code is None: if user_code is None:
raise HTTPException(status_code=401, detail="Invalid token") raise HTTPException(status_code=401, detail="Invalid token")
except PyJWTError: except PyJWTError:

View file

@ -12,7 +12,7 @@
<p>Dein persönlicher Code, um alle Funktionen nutzen zu können: {{data.user_code}}</p> <p>Dein persönlicher Code, um alle Funktionen nutzen zu können: {{data.user_code}}</p>
<a href="https://backend.blauwasser-mausis.de/bootsuche-england"> <a href="https://blauwasser-mausis.de/">
<span>Daten löschen</span> <span>Daten löschen</span>
</a> </a>
</div> </div>

View file

@ -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 backend.config import ENVIRONMENT
from mailtrap import Mail from mailtrap import Mail
import mailtrap as mt import mailtrap as mt
@ -22,30 +25,44 @@ class AdminApprovalData(ArticleData):
secret: str 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')) env = Environment(loader = FileSystemLoader('src/backend/internal/jinja_templates'))
template = env.get_template('approval.jinja') template = env.get_template(f"{template_name}.jinja")
output = template.render(data=data) return template.render(data=data)
def send_newsletter_admin_approval(data: ArticleData ):
html = create_html_from_template("approval", data)
mail = mt.Mail( mail = mt.Mail(
sender=mt.Address(email="newsletter@blauwasser-mausis.de", name="Flaschenpost"), sender=mt.Address(email="newsletter@blauwasser-mausis.de", name="Flaschenpost"),
to=[mt.Address(email="blauwasser-mausis@proton.me")], to=[mt.Address(email="blauwasser-mausis@proton.me")],
subject=f"[Approval] {data.title}", subject=f"[Approval] {data.title}",
text=output, html=html,
) )
send_mail(mail) send_mail(mail)
def send_mail(mail: Mail): def send_mail(mail: Mail):
if ENVIRONMENT == "local": if ENVIRONMENT == "local":
print(f"Mail sent: {mail.subject}") print(f"Mail sent: {mail.subject}")
print(mail.text)
else: else:
client = mt.MailtrapClient(token="<YOUR_API_TOKEN>") client = mt.MailtrapClient(token=MAILTRAP_API_TOKEN)
client.send(mail) client.send(mail)
def send_batch_mail(): def send_batch_mail(mails: list[BatchEmailRequest]):
mt.BatchMail( if ENVIRONMENT == "local":
sender=mt.Address(email="newsletter@blauwasser-mausis.de", name="Flaschenpost"), for mail in mails:
subject="Flaschenpost | Blauwasser-Mausis" 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)

View file

@ -1,4 +1,5 @@
from feedparser import FeedParserDict
from backend.config import FRONTEND_URL from backend.config import FRONTEND_URL
from backend.internal.authentication import create_access_token from backend.internal.authentication import create_access_token
from backend.internal.mail_util import AdminApprovalData from backend.internal.mail_util import AdminApprovalData
@ -18,15 +19,14 @@ def check_blog_rss_and_send_admin_mail():
description = str(article["summary"]) description = str(article["summary"])
subtitle = str(article["subtitle"]) subtitle = str(article["subtitle"])
link = str(article["link"]) link = str(article["link"])
secret = create_access_token({"sub": title}) secret = create_access_token({"sub": title})
data = AdminApprovalData(title, subtitle, description, link, secret) data = AdminApprovalData(title, subtitle, description, link, secret)
send_newsletter_admin_approval(data) send_newsletter_admin_approval(data)
# TODO: create link def find_article_by_title(title: str) -> FeedParserDict | None:
# TODO: create Template with jinja2 feed = feedparser.parse(f"{FRONTEND_URL}/rss.xml")
# TODO: send mail to blauwasser-mausis@proton.me for article in feed.entries:
if title == str(article["title"]):
return article
return None

View file

@ -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 Form
from fastapi import APIRouter from fastapi import APIRouter
@ -12,5 +25,20 @@ router = APIRouter(
"/approve", "/approve",
status_code=201 status_code=201
) )
def approve_newsletter(secret: str = Form()): def approve_newsletter(secret: str = Form(), session: Session = Depends(get_session)):
pass 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)

View file

@ -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 Duration
from pyrate_limiter import Rate from pyrate_limiter import Rate
from pyrate_limiter import Limiter from pyrate_limiter import Limiter
@ -28,18 +31,26 @@ router = APIRouter(
status_code=201, status_code=201,
dependencies=[Depends(RateLimiter(limiter=Limiter(Rate(1, Duration.MINUTE))))] 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) user = User.model_validate(new_user)
try: try:
session.add(user) session.add(user)
session.commit() session.commit()
session.refresh(user) 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: except IntegrityError as e:
session.rollback() session.rollback()
raise HTTPException(HTTP_400_BAD_REQUEST) raise HTTPException(HTTP_400_BAD_REQUEST)
return user.user_code
@router.post( @router.post(
"/login", "/login",
@ -53,6 +64,7 @@ def login(user_code: str = Form(), session: Session = Depends(get_session)) -> s
raise HTTPException(HTTP_401_UNAUTHORIZED) raise HTTPException(HTTP_401_UNAUTHORIZED)
return create_access_token({"sub": user.user_code}) return create_access_token({"sub": user.user_code})
@router.post( @router.post(
"/forgot", "/forgot",
dependencies=[Depends(RateLimiter(limiter=Limiter(Rate(1, Duration.MINUTE))))] 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: if user is not None:
pass # TODO: send Mail pass # TODO: send Mail
@router.get("/me", status_code=201) @router.get("/me", status_code=201)
def get_current_user_info(current_user: User = Depends(get_current_user)) -> UserInfo: def get_current_user_info(current_user: User = Depends(get_current_user)) -> UserInfo:
return UserInfo.model_validate(current_user) 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.delete(current_user)
session.commit() session.commit()
@router.delete("/me", status_code=201) @router.delete("/me", status_code=201)
def delete(user_code: str = Form(), session: Session = Depends(get_session)) : def delete(user_code: str = Form(), session: Session = Depends(get_session)) :
user = session.exec(select(User).where(User.user_code == user_code)).first() 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.delete(user)
session.commit() session.commit()
@router.put("me/subscribe", status_code=201) @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 current_user.subscription = subscription
session.add(current_user) session.add(current_user)
session.commit() session.commit()
# TODO: # TODO:
# Template für Willkommen mit delete link # change mail <- token, new mail
# change mail <- token, new mail
# delete all deactivated users after 48h