Compare commits
2 commits
c51908ffeb
...
c4d64f6ac7
| Author | SHA1 | Date | |
|---|---|---|---|
| c4d64f6ac7 | |||
| 91c13307aa |
19 changed files with 132 additions and 46 deletions
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
|
@ -2,7 +2,9 @@
|
||||||
"cSpell.language": "en,de-DE",
|
"cSpell.language": "en,de-DE",
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"Chiara",
|
"Chiara",
|
||||||
|
"dotenv",
|
||||||
"Hobbithöhle",
|
"Hobbithöhle",
|
||||||
|
"mailtrap",
|
||||||
"Masala",
|
"Masala",
|
||||||
"Pomorie",
|
"Pomorie",
|
||||||
"Reinke"
|
"Reinke"
|
||||||
|
|
|
||||||
4
backend/src/backend/.env
Normal file
4
backend/src/backend/.env
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
BACKEND_URL="http://localhost:8000"
|
||||||
|
FRONTEND_URL="http://localhost:4321"
|
||||||
|
SECRET="IamALocalSecretAndMustBeReplacedInProduction"
|
||||||
|
ENVIRONMENT="local"
|
||||||
BIN
backend/src/backend/__pycache__/config.cpython-314.pyc
Normal file
BIN
backend/src/backend/__pycache__/config.cpython-314.pyc
Normal file
Binary file not shown.
Binary file not shown.
6
backend/src/backend/config.py
Normal file
6
backend/src/backend/config.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
ENVIRONMENT = os.environ["ENVIRONMENT"]
|
||||||
|
SECRET_KEY = os.environ["SECRET"]
|
||||||
|
FRONTEND_URL = os.environ["FRONTEND_URL"]
|
||||||
|
BACKEND_URL = os.environ["BACKEND_URL"]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,3 +1,5 @@
|
||||||
|
from backend.config import SECRET_KEY
|
||||||
|
import os
|
||||||
from fastapi.security import HTTPBasicCredentials
|
from fastapi.security import HTTPBasicCredentials
|
||||||
from fastapi import Security
|
from fastapi import Security
|
||||||
from fastapi.security import HTTPBearer
|
from fastapi.security import HTTPBearer
|
||||||
|
|
@ -10,13 +12,11 @@ from backend.models.user import User
|
||||||
from backend.internal.database import get_session
|
from backend.internal.database import get_session
|
||||||
from sqlmodel import Session
|
from sqlmodel import Session
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from fastapi.security.oauth2 import OAuth2PasswordBearer
|
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
|
||||||
SECRET_KEY = "secret-key"
|
|
||||||
ALGORITHM = "HS256"
|
ALGORITHM = "HS256"
|
||||||
ACCESS_TOKEN_EXPIRE_DAYS = 360
|
ACCESS_TOKEN_EXPIRE_DAYS = 360
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
<span>Abbestellen</span>
|
<span>Abbestellen</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a id="admin" href="https://blauwasser-mausis.de">Approve</a>
|
<a id="admin" href="https://backend.blauwasser-mausis.de/?secret={{data.secret}}">Approve</a>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,17 @@
|
||||||
<body>
|
<body>
|
||||||
<div>
|
<div>
|
||||||
<h1>Neuer Artikel:</h1>
|
<h1>Neuer Artikel:</h1>
|
||||||
<h3>England</h3>
|
<h3>{{data.title}}</h3>
|
||||||
<h4>It's gettin' british, my dear!</h4>
|
<h4>{{data.subtitle}}</h4>
|
||||||
<p>Auf geht's nach Milano! Aber Moment mal...der Artikel heißt England?! Da kann irgendwas nicht stimmen. Ich frage mal in der Redaktion nach. Ihr lest währenddessen einfach schonmal weiter.</p>
|
<p>{{data.description}}</p>
|
||||||
|
|
||||||
<a href=https://blauwasser-mausis.de/blog/bootsuche-england/>
|
<a href={{data.link}}>
|
||||||
<button>Zum Artikel</button>
|
<button>Zum Artikel</button>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a id="admin" href="https://blauwasser-mausis.de">Approve</a>
|
<a href="https://blauwasser-mausis.de/blog/bootsuche-england">
|
||||||
|
<span>Abbestellen</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
@ -51,9 +53,4 @@
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#admin {
|
|
||||||
margin-top: 64px;
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
40
backend/src/backend/internal/jinja_templates/welcome.jinja
Normal file
40
backend/src/backend/internal/jinja_templates/welcome.jinja
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Newsletter</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<h1>Willkommen, {{data.name}}</h1>
|
||||||
|
<p>Du hast dich bei Blauwasser-Mausis.de angemeldet. Solltest du das nicht gewesen sein, dann lösche die Daten einfach über den untenstehenden Link.</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">
|
||||||
|
<span>Daten löschen</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 32px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: lightgrey;
|
||||||
|
padding: 32px 64px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
|
from backend.config import ENVIRONMENT
|
||||||
from mailtrap import Mail
|
from mailtrap import Mail
|
||||||
import mailtrap as mt
|
import mailtrap as mt
|
||||||
import os
|
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
|
class ArticleData():
|
||||||
class Newsletter():
|
|
||||||
def __init__(self, title, subtitle, description, link):
|
def __init__(self, title, subtitle, description, link):
|
||||||
self.title=title
|
self.title=title
|
||||||
self.subtitle=subtitle
|
self.subtitle=subtitle
|
||||||
|
|
@ -16,28 +15,37 @@ class Newsletter():
|
||||||
description: str
|
description: str
|
||||||
link: str
|
link: str
|
||||||
|
|
||||||
def send_newsletter_admin_approval(data: Newsletter ):
|
class AdminApprovalData(ArticleData):
|
||||||
|
def __init__(self,title, subtitle, description, link, secret):
|
||||||
|
ArticleData.__init__(self, title, subtitle, description, link)
|
||||||
|
self.secret = secret
|
||||||
|
|
||||||
|
secret: str
|
||||||
|
|
||||||
|
def send_newsletter_admin_approval(data: ArticleData ):
|
||||||
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('approval.jinja')
|
||||||
output = template.render(data=data)
|
output = template.render(data=data)
|
||||||
|
|
||||||
|
|
||||||
with open("src/backend/internal/jinja_templates/output.html", "w") as f:
|
|
||||||
f.write(output)
|
|
||||||
|
|
||||||
|
|
||||||
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="Jinja2 Template",
|
text=output,
|
||||||
)
|
)
|
||||||
|
send_mail(mail)
|
||||||
|
|
||||||
|
|
||||||
def send_mail(mail: Mail):
|
def send_mail(mail: Mail):
|
||||||
|
if ENVIRONMENT == "local":
|
||||||
|
print(f"Mail sent: {mail.subject}")
|
||||||
|
else:
|
||||||
client = mt.MailtrapClient(token="<YOUR_API_TOKEN>")
|
client = mt.MailtrapClient(token="<YOUR_API_TOKEN>")
|
||||||
client.send(mail)
|
client.send(mail)
|
||||||
|
|
||||||
def send_batch_mail():
|
def send_batch_mail():
|
||||||
pass
|
mt.BatchMail(
|
||||||
|
sender=mt.Address(email="newsletter@blauwasser-mausis.de", name="Flaschenpost"),
|
||||||
|
subject="Flaschenpost | Blauwasser-Mausis"
|
||||||
|
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,27 @@
|
||||||
|
|
||||||
from backend.internal.mail_util import Newsletter
|
from backend.config import FRONTEND_URL
|
||||||
|
from backend.internal.authentication import create_access_token
|
||||||
|
from backend.internal.mail_util import AdminApprovalData
|
||||||
from backend.internal.mail_util import send_newsletter_admin_approval
|
from backend.internal.mail_util import send_newsletter_admin_approval
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import feedparser
|
import feedparser
|
||||||
|
|
||||||
FEED_URL="http://localhost:4321/rss.xml"
|
|
||||||
|
|
||||||
def check_blog_rss_and_send_admin_mail():
|
def check_blog_rss_and_send_admin_mail():
|
||||||
feed = feedparser.parse(FEED_URL)
|
feed = feedparser.parse(f"{FRONTEND_URL}/rss.xml")
|
||||||
for article in feed.entries:
|
for article in feed.entries:
|
||||||
date_string = str(article["published"])
|
date_string = str(article["published"])
|
||||||
date_object = datetime.strptime(date_string, '%a, %d %b %Y %H:%M:%S %Z')
|
date_object = datetime.strptime(date_string, '%a, %d %b %Y %H:%M:%S %Z')
|
||||||
|
|
||||||
if date_object > (datetime.now() - timedelta(days=15)):
|
if date_object > (datetime.now() - timedelta(days=16)):
|
||||||
title = str(article["title"])
|
title = str(article["title"])
|
||||||
description = str(article["summary"])
|
description = str(article["summary"])
|
||||||
subtitle = str(article["subtitle"])
|
subtitle = str(article["subtitle"])
|
||||||
link = str(article["link"])
|
link = str(article["link"])
|
||||||
|
|
||||||
data = Newsletter(title, subtitle, description, link)
|
secret = create_access_token({"sub": title})
|
||||||
|
|
||||||
|
data = AdminApprovalData(title, subtitle, description, link, secret)
|
||||||
|
|
||||||
send_newsletter_admin_approval(data)
|
send_newsletter_admin_approval(data)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
from backend.internal.rss_reader import check_blog_rss_and_send_admin_mail
|
from backend.internal.rss_reader import check_blog_rss_and_send_admin_mail
|
||||||
from .routers import users, comments
|
from .routers import users, comments, newsletters
|
||||||
from backend.internal.database import create_db_and_tables
|
from backend.internal.database import create_db_and_tables
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi_crons import Crons, get_cron_router
|
from fastapi_crons import Crons
|
||||||
|
|
||||||
|
|
||||||
# TODO: Newsletter
|
# TODO: Newsletter
|
||||||
# TODO: env
|
|
||||||
# TODO: TLS
|
# TODO: TLS
|
||||||
# TODO: Deploy
|
# TODO: Deploy
|
||||||
|
|
||||||
|
|
@ -27,7 +29,4 @@ router = APIRouter()
|
||||||
|
|
||||||
app.include_router(users.router)
|
app.include_router(users.router)
|
||||||
app.include_router(comments.router)
|
app.include_router(comments.router)
|
||||||
|
app.include_router(newsletters.router)
|
||||||
check_blog_rss_and_send_admin_mail()
|
|
||||||
|
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
16
backend/src/backend/routers/newsletters.py
Normal file
16
backend/src/backend/routers/newsletters.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
from fastapi import Form
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/newsletter",
|
||||||
|
tags=[""],
|
||||||
|
dependencies=[],
|
||||||
|
responses={404: {"description": "Not found"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/approve",
|
||||||
|
status_code=201
|
||||||
|
)
|
||||||
|
def approve_newsletter(secret: str = Form()):
|
||||||
|
pass
|
||||||
|
|
@ -18,13 +18,16 @@ from fastapi import APIRouter
|
||||||
from fastapi_limiter.depends import RateLimiter
|
from fastapi_limiter.depends import RateLimiter
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
prefix="",
|
prefix="/users",
|
||||||
tags=[""],
|
|
||||||
dependencies=[],
|
dependencies=[],
|
||||||
responses={404: {"description": "Not found"}},
|
responses={404: {"description": "Not found"}},
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.post("/users", status_code=201)
|
@router.post(
|
||||||
|
"/",
|
||||||
|
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)) -> str:
|
||||||
user = User.model_validate(new_user)
|
user = User.model_validate(new_user)
|
||||||
|
|
||||||
|
|
@ -51,7 +54,7 @@ def login(user_code: str = Form(), session: Session = Depends(get_session)) -> s
|
||||||
return create_access_token({"sub": user.user_code})
|
return create_access_token({"sub": user.user_code})
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"users/forgot",
|
"/forgot",
|
||||||
dependencies=[Depends(RateLimiter(limiter=Limiter(Rate(1, Duration.MINUTE))))]
|
dependencies=[Depends(RateLimiter(limiter=Limiter(Rate(1, Duration.MINUTE))))]
|
||||||
)
|
)
|
||||||
def forgot_user_code(email: str = Form(), session: Session = Depends(get_session)):
|
def forgot_user_code(email: str = Form(), session: Session = Depends(get_session)):
|
||||||
|
|
@ -69,6 +72,14 @@ 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)
|
||||||
|
def delete(user_code: str = Form(), session: Session = Depends(get_session)) :
|
||||||
|
user = session.exec(select(User).where(User.user_code == user_code)).first()
|
||||||
|
|
||||||
|
if user is not None:
|
||||||
|
session.delete(user)
|
||||||
|
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
|
||||||
|
|
@ -77,5 +88,6 @@ def change_subscription(subscription: bool = Form(), current_user: User = Depend
|
||||||
|
|
||||||
|
|
||||||
# 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
|
# delete all deactivated users after 48h
|
||||||
Loading…
Add table
Reference in a new issue