diff --git a/backend/auth.py b/backend/auth.py
index b2736f5..942a3f1 100644
--- a/backend/auth.py
+++ b/backend/auth.py
@@ -87,7 +87,7 @@ def get_current_user(
user_id = int(payload["sub"])
with db() as conn:
row = conn.execute(
- "SELECT id, email, name, rolle, is_premium, is_moderator, is_banned, ban_reason, is_social_media, notes_ki_enabled, breeder_status, is_founder, is_partner, founder_number, email_verified FROM users WHERE id=?",
+ "SELECT id, email, name, rolle, is_premium, is_moderator, is_banned, ban_reason, is_social_media, notes_ki_enabled, breeder_status, is_founder, is_partner, founder_number FROM users WHERE id=?",
(user_id,)
).fetchone()
diff --git a/backend/database.py b/backend/database.py
index 5ea9f4a..76c1c2c 100644
--- a/backend/database.py
+++ b/backend/database.py
@@ -565,9 +565,6 @@ def _migrate(conn_factory):
("users", "is_partner", "INTEGER NOT NULL DEFAULT 0"),
("users", "founder_number", "INTEGER"),
("users", "is_founder_pending", "INTEGER NOT NULL DEFAULT 0"),
- # Passwort-Zurücksetzen
- ("users", "password_reset_token", "TEXT"),
- ("users", "password_reset_expires", "TEXT"),
]
with conn_factory() as conn:
for table, column, col_type in migrations:
diff --git a/backend/main.py b/backend/main.py
index e8720c9..ddb4acc 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -64,19 +64,6 @@ app = FastAPI(
redoc_url = None,
)
-class SecurityHeadersMiddleware(BaseHTTPMiddleware):
- async def dispatch(self, request: Request, call_next):
- response = await call_next(request)
- response.headers["X-Frame-Options"] = "SAMEORIGIN"
- response.headers["X-Content-Type-Options"] = "nosniff"
- response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
- response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=(self)"
- response.headers["X-XSS-Protection"] = "1; mode=block"
- return response
-
-app.add_middleware(SecurityHeadersMiddleware)
-
-
# Globales File-Upload-Limit (20 MB)
_MAX_UPLOAD_BYTES = 20 * 1024 * 1024
diff --git a/backend/routes/auth.py b/backend/routes/auth.py
index f810217..7661c80 100644
--- a/backend/routes/auth.py
+++ b/backend/routes/auth.py
@@ -3,7 +3,6 @@
import os
import secrets
import string
-from datetime import datetime, timedelta
from typing import Optional
from fastapi import APIRouter, HTTPException, Request, Response, Depends
@@ -78,8 +77,6 @@ async def register(data: RegisterRequest, response: Response, request: Request):
raise HTTPException(400, "Benutzername darf keine Leerzeichen enthalten.")
if is_username_blocked(name):
raise HTTPException(400, "Dieser Benutzername ist nicht erlaubt.")
- if len(data.password) < 8:
- raise HTTPException(400, "Passwort muss mindestens 8 Zeichen lang sein.")
with db() as conn:
if conn.execute("SELECT 1 FROM users WHERE email=?", (data.email,)).fetchone():
@@ -250,8 +247,7 @@ async def verify_email(token: str):
@router.post("/resend-verification")
-async def resend_verification(request: Request, user=Depends(get_current_user)):
- rl_check(request, max_requests=3, window_seconds=3600, key="resend_verify")
+async def resend_verification(user=Depends(get_current_user)):
with db() as conn:
row = conn.execute(
"SELECT email, name, email_verified FROM users WHERE id=?", (user["id"],)
@@ -267,65 +263,3 @@ async def resend_verification(request: Request, user=Depends(get_current_user)):
)
_send_verification_email(row["email"], row["name"], token)
return {"ok": True}
-
-
-class ForgotPasswordRequest(BaseModel):
- email: EmailStr
-
-class ResetPasswordRequest(BaseModel):
- token: str
- password: str
-
-@router.post("/forgot-password")
-async def forgot_password(data: ForgotPasswordRequest, request: Request):
- rl_check(request, max_requests=3, window_seconds=3600, key="forgot_pw")
- with db() as conn:
- user = conn.execute(
- "SELECT id, name FROM users WHERE email=?", (data.email,)
- ).fetchone()
- # Immer OK zurückgeben — kein User-Enumeration
- if user:
- token = secrets.token_urlsafe(32)
- expires = (datetime.utcnow() + timedelta(hours=2)).isoformat()
- with db() as conn:
- conn.execute(
- "UPDATE users SET password_reset_token=?, password_reset_expires=? WHERE id=?",
- (token, expires, user["id"])
- )
- app_url = os.getenv("APP_URL", "https://banyaro.app")
- subject = "Ban Yaro — Passwort zurücksetzen"
- body = (
- f"Hallo {user['name']},\n\n"
- "du hast eine Passwort-Zurücksetzen-Anfrage gestellt.\n\n"
- f"Klicke hier um ein neues Passwort zu setzen:\n"
- f"{app_url}/#reset-password?token={token}\n\n"
- "Der Link ist 2 Stunden gültig. Falls du keine Anfrage gestellt hast, ignoriere diese Mail.\n\n"
- "Viele Grüße,\nDas Ban Yaro Team"
- )
- from routes.outreach import _send_smtp
- try:
- _send_smtp(data.email, subject, body, "support")
- except Exception:
- pass
- return {"ok": True}
-
-
-@router.post("/reset-password")
-async def reset_password(data: ResetPasswordRequest, request: Request):
- rl_check(request, max_requests=5, window_seconds=3600, key="reset_pw")
- if len(data.password) < 8:
- raise HTTPException(400, "Passwort muss mindestens 8 Zeichen lang sein.")
- with db() as conn:
- user = conn.execute(
- "SELECT id, password_reset_expires FROM users WHERE password_reset_token=?",
- (data.token,)
- ).fetchone()
- if not user:
- raise HTTPException(400, "Ungültiger oder abgelaufener Link.")
- if user["password_reset_expires"] < datetime.utcnow().isoformat():
- raise HTTPException(400, "Dieser Link ist abgelaufen. Bitte fordere einen neuen an.")
- conn.execute(
- "UPDATE users SET pw_hash=?, password_reset_token=NULL, password_reset_expires=NULL WHERE id=?",
- (hash_password(data.password), user["id"])
- )
- return {"ok": True}
diff --git a/backend/routes/forum.py b/backend/routes/forum.py
index 0cfe1df..b6d204f 100644
--- a/backend/routes/forum.py
+++ b/backend/routes/forum.py
@@ -166,8 +166,6 @@ async def list_threads(
# ------------------------------------------------------------------
@router.post("/threads", status_code=201)
async def create_thread(data: ThreadCreate, user=Depends(get_current_user)):
- if not user.get("email_verified"):
- raise HTTPException(403, "Bitte bestätige zuerst deine E-Mail-Adresse um im Forum zu schreiben.")
if not data.titel.strip():
raise HTTPException(400, "Titel darf nicht leer sein.")
if not data.text.strip():
@@ -306,8 +304,6 @@ async def patch_thread(thread_id: int, data: ThreadPatch, user=Depends(get_curre
# ------------------------------------------------------------------
@router.post("/threads/{thread_id}/posts", status_code=201)
async def create_post(thread_id: int, data: PostCreate, user=Depends(get_current_user)):
- if not user.get("email_verified"):
- raise HTTPException(403, "Bitte bestätige zuerst deine E-Mail-Adresse um im Forum zu schreiben.")
if not data.text.strip():
raise HTTPException(400, "Text darf nicht leer sein.")
with db() as conn:
diff --git a/backend/routes/outreach.py b/backend/routes/outreach.py
index 6ec066c..11f4152 100644
--- a/backend/routes/outreach.py
+++ b/backend/routes/outreach.py
@@ -1,6 +1,5 @@
"""BAN YARO — Mailing (Admin)"""
-import imaplib
import os
import smtplib
import ssl
@@ -10,21 +9,16 @@ from email.utils import formataddr
from datetime import datetime
from typing import List, Optional
-import logging
-
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from auth import require_admin
from database import db
-router = APIRouter()
-_log = logging.getLogger(__name__)
+router = APIRouter()
_SMTP_HOST = os.getenv("SMTP_HOST", "mail.your-server.de")
_SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))
-_IMAP_HOST = os.getenv("IMAP_HOST", "mail.your-server.de")
-_IMAP_PORT = int(os.getenv("IMAP_PORT", "993"))
_ACCOUNTS = {
"partner": {
@@ -41,73 +35,23 @@ _ACCOUNTS = {
},
}
-# Mögliche Namen für den Sent-Ordner (Hetzner/Dovecot)
-_SENT_CANDIDATES = ["Sent", "Sent Messages", "Sent Items", "INBOX.Sent", "Gesendete Objekte"]
-
-
-def _imap_save_sent(msg_bytes: bytes, account: str):
- acc = _ACCOUNTS.get(account) or _ACCOUNTS["partner"]
- if not acc["user"] or not acc["pass"]:
- _log.warning("IMAP: Account '%s' nicht konfiguriert, überspringe.", account)
- return
- try:
- ctx = ssl.create_default_context()
- with imaplib.IMAP4_SSL(_IMAP_HOST, _IMAP_PORT, ssl_context=ctx) as imap:
- imap.login(acc["user"], acc["pass"])
- _, raw_folders = imap.list()
- available = [f.decode(errors="replace") for f in (raw_folders or [])]
- _log.info("IMAP Ordner (%s): %s", account, available)
-
- # Echten Ordnernamen aus LIST-Antwort extrahieren
- # Format: '(\Flags) "." INBOX.Sent' → letztes Token
- folder = None
- for line in available:
- name = line.rsplit('"." ', 1)[-1].strip().strip('"')
- for candidate in _SENT_CANDIDATES:
- if candidate.lower() in name.lower():
- folder = name
- break
- if folder:
- break
- if not folder:
- folder = "INBOX.Sent"
- _log.info("IMAP: speichere in Ordner '%s' (%s)", folder, account)
-
- typ, data = imap.append(
- folder,
- r"\Seen",
- imaplib.Time2Internaldate(datetime.now().timestamp()),
- msg_bytes,
- )
- _log.info("IMAP append: %s %s", typ, data)
- except Exception as e:
- _log.error("IMAP Sent-Speicherung fehlgeschlagen (%s): %s", account, e)
-
-
-def _build_message(to: str, subject: str, body: str, account: str) -> MIMEMultipart:
- acc = _ACCOUNTS.get(account) or _ACCOUNTS["partner"]
- msg = MIMEMultipart("alternative")
- msg["Subject"] = subject
- msg["From"] = formataddr((acc["name"], acc["from"]))
- msg["To"] = to
- msg["Reply-To"] = acc["from"]
- msg.attach(MIMEText(body, "plain", "utf-8"))
- return msg
-
def _send_smtp(to: str, subject: str, body: str, account: str = "partner"):
acc = _ACCOUNTS.get(account) or _ACCOUNTS["partner"]
if not acc["user"] or not acc["pass"]:
raise RuntimeError(f"SMTP-Account '{account}' nicht konfiguriert.")
- msg = _build_message(to, subject, body, account)
- msg_bytes = msg.as_bytes()
+ msg = MIMEMultipart("alternative")
+ msg["Subject"] = subject
+ msg["From"] = formataddr((acc["name"], acc["from"]))
+ msg["To"] = to
+ msg["Reply-To"] = acc["from"]
+ msg.attach(MIMEText(body, "plain", "utf-8"))
ctx = ssl.create_default_context()
with smtplib.SMTP(_SMTP_HOST, _SMTP_PORT, timeout=15) as s:
s.ehlo()
s.starttls(context=ctx)
s.login(acc["user"], acc["pass"])
- s.sendmail(acc["from"], [to], msg_bytes)
- _imap_save_sent(msg_bytes, account)
+ s.sendmail(acc["from"], [to], msg.as_bytes())
# ------------------------------------------------------------------
diff --git a/backend/static/icons/phosphor.svg b/backend/static/icons/phosphor.svg
index 3fcf69f..2fcc061 100644
--- a/backend/static/icons/phosphor.svg
+++ b/backend/static/icons/phosphor.svg
@@ -189,5 +189,4 @@
- -
`; } @@ -1429,38 +1414,6 @@ window.Page_settings = (() => { function _bindLoginForm() { _bindPwToggle('login-pw', 'login-pw-toggle'); - - document.getElementById('forgot-pw-link')?.addEventListener('click', () => { - const id = 'forgot-pw-modal'; - UI.modal.open({ - title: 'Passwort zurücksetzen', - body: ` - `, - footer: ` - - `, - }); - document.getElementById(id)?.addEventListener('submit', async e => { - e.preventDefault(); - const btn = document.querySelector(`[form="${id}"]`); - const email = document.getElementById('forgot-pw-email').value.trim(); - await UI.asyncButton(btn, async () => { - await API.post('/auth/forgot-password', { email }); - UI.modal.close(); - UI.toast.success('Falls ein Account existiert, haben wir dir einen Link geschickt.'); - }); - }); - }); - document.getElementById('auth-form')?.addEventListener('submit', async e => { e.preventDefault(); const btn = e.target.querySelector('[type="submit"]'); @@ -1657,93 +1610,6 @@ window.Page_settings = (() => { setTimeout(remove, 12000); } - // ---------------------------------------------------------- - // PASSWORT ZURÜCKSETZEN - // ---------------------------------------------------------- - function _renderResetPassword(token) { - _container.innerHTML = ` -- Wähle ein sicheres Passwort für deinen Account. -
-