Ergänzung Datenschutzerklärung und Impressum, Fristenformular in Einstellungen

This commit is contained in:
2026-08-10 06:44:11 +02:00
parent 8ffbda875e
commit 777e3e5447
25 changed files with 1310 additions and 139 deletions

View File

@@ -26,16 +26,18 @@ from app.mail import (
from app.maintenance import describe, run_cleanup
from app.models import EmailChange, User
from app.schemas import MailCheckOut, MailTestIn, MessageOut
from app.runtime_settings import BY_KEY, describe_durations, set_duration
from app.schemas_admin import (
AdminSettingsIn,
AdminSettingsOut,
AdminStatsOut,
AdminUserOut,
DeleteUserIn,
DurationOut,
EmailChangeIn,
UserCreateIn,
)
from app.security import check_rate_limit, normalize_email, utcnow
from app.security import bucket_key, check_rate_limit, normalize_email, utcnow
from app.users import (
EMAIL_CHANGE_HOURS,
WELCOME_DAYS,
@@ -66,18 +68,17 @@ def _settings_out(db) -> AdminSettingsOut:
return AdminSettingsOut(
allow_self_registration=self_registration_enabled(db),
locked_by_env=registration_locked_by_env(),
auto_deactivate_months=months_setting(
db, "auto_deactivate_months", DEFAULT_DEACTIVATE_MONTHS
),
auto_delete_months=months_setting(
db, "auto_delete_months", DEFAULT_DELETE_MONTHS
),
auto_archive_months=months_setting(
db, "auto_archive_months", DEFAULT_ARCHIVE_MONTHS
),
archive_delete_months=months_setting(
db, "archive_delete_months", DEFAULT_ARCHIVE_DELETE_MONTHS
),
durations=[DurationOut(**entry) for entry in describe_durations(db)],
fixed={
"Zähler der Missbrauchsabwehr": "24 Stunden",
"Fehlgeschlagene Produktabfragen":
f"{settings.product_miss_days} Tage (PRODUCT_MISS_DAYS)",
"Aufräumlauf":
f"alle {settings.cleanup_interval_hours} Stunden "
"(CLEANUP_INTERVAL_HOURS)",
"Zugriffsprotokolle des Webservers":
"Sache der Protokollrotation auf dem Server, nicht der Anwendung",
},
)
@@ -102,14 +103,12 @@ def write_settings(payload: AdminSettingsIn, db: DbSession, admin: AdminUser):
"true" if payload.allow_self_registration else "false",
)
if payload.auto_deactivate_months is not None:
set_setting(db, "auto_deactivate_months", str(payload.auto_deactivate_months))
if payload.auto_delete_months is not None:
set_setting(db, "auto_delete_months", str(payload.auto_delete_months))
if payload.auto_archive_months is not None:
set_setting(db, "auto_archive_months", str(payload.auto_archive_months))
if payload.archive_delete_months is not None:
set_setting(db, "archive_delete_months", str(payload.archive_delete_months))
for key, value in (payload.durations or {}).items():
if key not in BY_KEY:
raise HTTPException(
status.HTTP_400_BAD_REQUEST, f"Unbekannte Frist: {key}"
)
set_duration(db, key, value)
db.commit()
return _settings_out(db)
@@ -222,7 +221,8 @@ def add_user(
background: BackgroundTasks,
):
if not check_rate_limit(
db, f"admin-create:{client_ip(request)}", limit=30, window_minutes=60
db, bucket_key(db, "admin-create", client_ip(request)),
limit=30, window_minutes=60,
):
db.commit()
raise HTTPException(status.HTTP_429_TOO_MANY_REQUESTS, "Zu viele Anlagen.")
@@ -241,7 +241,8 @@ def add_user(
background.add_task(
send_welcome, email, token,
admin.display_name or admin.email.split("@")[0], WELCOME_DAYS,
admin.display_name or admin.email.split("@")[0],
get_duration(db, "welcome_days"),
)
return _user_out(user, {}, {}, {})
@@ -268,16 +269,19 @@ def resend_welcome(
"nutzt die Person „Passwort vergessen“ auf der Anmeldeseite.",
)
if not check_rate_limit(db, f"admin-welcome:{user.id}", limit=5, window_minutes=60):
if not check_rate_limit(
db, bucket_key(db, "admin-welcome", user.id), limit=5, window_minutes=60
):
db.commit()
raise HTTPException(status.HTTP_429_TOO_MANY_REQUESTS, "Zu viele Versuche.")
token = issue_token(db, user, "welcome", hours=WELCOME_DAYS * 24)
days = get_duration(db, "welcome_days")
token = issue_token(db, user, "welcome", hours=days * 24)
db.commit()
background.add_task(
send_welcome, user.email, token,
admin.display_name or admin.email.split("@")[0], WELCOME_DAYS,
admin.display_name or admin.email.split("@")[0], days,
)
return MessageOut(detail=f"Willkommensnachricht erneut an {user.email} versendet.")
@@ -310,12 +314,13 @@ def change_email(
change, raw_new, raw_old = start_email_change(db, user, target, admin.id)
db.commit()
change_hours = get_duration(db, "email_change_hours")
background.add_task(
send_email_change_verify, target, raw_new, old_email, EMAIL_CHANGE_HOURS
send_email_change_verify, target, raw_new, old_email, change_hours
)
if change.requires_old and raw_old:
background.add_task(
send_email_change_verify_old, old_email, raw_old, target, EMAIL_CHANGE_HOURS
send_email_change_verify_old, old_email, raw_old, target, change_hours
)
detail = (
f"Bestätigungslinks an {target} und {old_email} versendet. "
@@ -324,7 +329,7 @@ def change_email(
)
else:
background.add_task(
send_email_change_notice, old_email, target, EMAIL_CHANGE_HOURS
send_email_change_notice, old_email, target, change_hours
)
detail = (
f"Bestätigungslink an {target} versendet, Hinweis an {old_email}. "

View File

@@ -28,6 +28,7 @@ from app.schemas import (
RegisterIn,
UserOut,
)
from app.runtime_settings import get_duration
from app.users import apply_if_complete
from app.security import (
check_rate_limit,
@@ -96,7 +97,8 @@ def register(
)
if not check_rate_limit(
db, f"register:{client_ip(request)}", limit=5, window_minutes=60
db, bucket_key(db, "register", client_ip(request)),
limit=5, window_minutes=60,
):
db.commit()
raise HTTPException(status.HTTP_429_TOO_MANY_REQUESTS, "Zu viele Versuche.")
@@ -113,12 +115,14 @@ def register(
)
db.add(user)
db.flush()
raw = _issue_token(db, user, "verify", hours=24)
raw = _issue_token(db, user, "verify", hours=get_duration(db, "verify_hours"))
background.add_task(send_verification, email, raw)
elif existing.verified_at is None:
# Unbestaetigtes Konto: neuen Link schicken, statt zu verraten,
# dass die Adresse schon vergeben ist.
raw = _issue_token(db, existing, "verify", hours=24)
raw = _issue_token(
db, existing, "verify", hours=get_duration(db, "verify_hours")
)
background.add_task(send_verification, email, raw)
# Bestaetigtes Konto: nichts tun, aber gleiche Antwort geben.
@@ -148,8 +152,13 @@ def verify(token: str, db: DbSession):
def login(payload: LoginIn, request: Request, response: Response, db: DbSession):
email = normalize_email(payload.email)
ip_ok = check_rate_limit(db, f"login-ip:{client_ip(request)}", limit=20, window_minutes=15)
acct_ok = check_rate_limit(db, f"login-acct:{email}", limit=8, window_minutes=15)
ip_ok = check_rate_limit(
db, bucket_key(db, "login-ip", client_ip(request)),
limit=20, window_minutes=15,
)
acct_ok = check_rate_limit(
db, bucket_key(db, "login-acct", email), limit=8, window_minutes=15
)
if not (ip_ok and acct_ok):
db.commit()
raise HTTPException(
@@ -266,7 +275,8 @@ def reset_request(
background: BackgroundTasks,
):
if not check_rate_limit(
db, f"reset:{client_ip(request)}", limit=5, window_minutes=60
db, bucket_key(db, "reset", client_ip(request)),
limit=5, window_minutes=60,
):
db.commit()
raise HTTPException(status.HTTP_429_TOO_MANY_REQUESTS, "Zu viele Versuche.")
@@ -274,7 +284,7 @@ def reset_request(
email = normalize_email(payload.email)
user = db.scalar(select(User).where(User.email == email))
if user is not None and user.is_active and user.verified_at is not None:
raw = _issue_token(db, user, "reset", hours=1)
raw = _issue_token(db, user, "reset", hours=get_duration(db, "reset_hours"))
background.add_task(send_password_reset, email, raw)
db.commit()

View File

@@ -31,7 +31,7 @@ from app.schemas_shopping import (
PublicToggleIn,
PublicViewOut,
)
from app.security import check_rate_limit, hash_token, new_token, utcnow
from app.security import bucket_key, check_rate_limit, hash_token, new_token, utcnow
router = APIRouter(tags=["public"])
@@ -189,7 +189,8 @@ def _load_share(db: DbSession, token: str, request: Request) -> PublicShare:
# bremsen, bevor jemand Token durchprobiert. Bei 256 Bit Zufall ist
# Raten aussichtslos, aber die Last soll trotzdem begrenzt bleiben.
if not check_rate_limit(
db, f"public:{client_ip(request)}", limit=120, window_minutes=15
db, bucket_key(db, "public", client_ip(request)),
limit=120, window_minutes=15,
):
db.commit()
raise HTTPException(

View File

@@ -40,10 +40,17 @@ from app.security import (
router = APIRouter(tags=["sharing"])
# Vorgabewert; maßgeblich ist die Einstellung in der Verwaltung.
INVITE_DAYS = 14
MAX_OPEN_INVITES = 50
def _invite_days(db) -> int:
from app.runtime_settings import get_duration
return get_duration(db, "invite_days")
def _status(invite: ListInvite) -> str:
if invite.revoked_at:
return "revoked"
@@ -78,8 +85,13 @@ def _guard_invite_rate(db: DbSession, request: Request, list_id: str) -> None:
unerwünschte Mails missbrauchen - jede Einladung erzeugt schließlich
eine Nachricht an eine frei wählbare Adresse.
"""
ok_list = check_rate_limit(db, f"invite-list:{list_id}", limit=20, window_minutes=60)
ok_ip = check_rate_limit(db, f"invite-ip:{client_ip(request)}", limit=30, window_minutes=60)
ok_list = check_rate_limit(
db, bucket_key(db, "invite-list", list_id), limit=20, window_minutes=60
)
ok_ip = check_rate_limit(
db, bucket_key(db, "invite-ip", client_ip(request)),
limit=30, window_minutes=60,
)
if not (ok_list and ok_ip):
db.commit()
raise HTTPException(
@@ -159,6 +171,7 @@ def create_invite(
).all():
old.revoked_at = utcnow()
valid_days = _invite_days(db)
raw = new_token()
invite = ListInvite(
list_id=lst.id,
@@ -166,7 +179,7 @@ def create_invite(
role=payload.role,
token_hash=hash_token(raw),
invited_by=user.id,
expires_at=utcnow() + timedelta(days=INVITE_DAYS),
expires_at=utcnow() + timedelta(days=valid_days),
)
db.add(invite)
db.commit()
@@ -174,7 +187,7 @@ def create_invite(
background.add_task(
send_invitation, email, raw,
lst.name, user.display_name or user.email.split("@")[0], INVITE_DAYS,
lst.name, user.display_name or user.email.split("@")[0], valid_days,
)
return _invite_out(invite)
@@ -204,10 +217,11 @@ def resend_invite(
_guard_invite_rate(db, request, lst.id)
valid_days = _invite_days(db)
raw = new_token()
invite.token_hash = hash_token(raw)
invite.revoked_at = None
invite.expires_at = utcnow() + timedelta(days=INVITE_DAYS)
invite.expires_at = utcnow() + timedelta(days=valid_days)
invite.last_sent_at = utcnow()
invite.send_count += 1
db.commit()
@@ -215,7 +229,7 @@ def resend_invite(
background.add_task(
send_invitation, invite.email, raw,
lst.name, user.display_name or user.email.split("@")[0], INVITE_DAYS,
lst.name, user.display_name or user.email.split("@")[0], valid_days,
)
return _invite_out(invite)