Ergänzung Datenschutzerklärung und Impressum, Fristenformular in Einstellungen
This commit is contained in:
@@ -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}. "
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user