banyaro/backend/auth.py
rene f378edab5d Jobs: Bewerbungssystem für Social-Media-Manager/in
Backend:
- job_applications + job_application_docs Tabellen in DB
- luna_trial_until Spalte in users (Migration)
- routes/jobs.py: POST /apply (FormData + Datei-Upload, max 3×10MB),
  GET /my-application, GET /luna-trial-status
- Admin: GET/PATCH /admin/applications, GET /admin/applications/{id}/docs/{doc_id}
- Bei Bewerbung: 14-Tage Luna-Probezugang automatisch aktiviert
- Bei Annahme: is_social_media=1 + Gründer-Status gesetzt
- Status-Mails (pending/reviewing/accepted/rejected) via email_html-Template
- auth.py: require_social_media prüft auch luna_trial_until

Frontend:
- pages/jobs.js: Stellenausschreibung + Bewerbungsformular
  (Name, E-Mail, Hund, Social-Handle, Motivation, Datei-Upload)
- Luna-Probezugang Teaser mit Countdown wenn aktiv
- Bestehende Bewerbung: Status-Screen statt Formular
- app.js: 'jobs' Seite registriert
- admin.js: neuer Tab 'Bewerbungen' (filtert nach Status,
  Statuswechsel per Dropdown, Detailansicht mit Anhang-Download,
  Admin-Notiz-Feld)
- admin.js: Tab 'Jobs' → 'Scheduler' umbenannt
2026-05-01 09:30:05 +02:00

140 lines
4.7 KiB
Python

"""
BAN YARO — Auth
JWT + Bcrypt. Einmal gebaut, von allen Routes genutzt.
"""
import os
import jwt
import bcrypt
import logging
from datetime import datetime, timedelta, timezone
from fastapi import Depends, HTTPException, status, Request
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from database import db
logger = logging.getLogger(__name__)
JWT_SECRET = os.getenv("JWT_SECRET", "change-me-in-production")
JWT_ALGO = "HS256"
JWT_EXPIRY = int(os.getenv("JWT_EXPIRY_DAYS", "30"))
if JWT_SECRET == "change-me-in-production" and os.getenv("ENV") == "production":
raise RuntimeError(
"SICHERHEITSFEHLER: JWT_SECRET ist nicht gesetzt. "
"Bitte JWT_SECRET in .env setzen und Container neu starten."
)
security = HTTPBearer(auto_error=False)
# ------------------------------------------------------------------
# Passwort
# ------------------------------------------------------------------
def hash_password(password: str) -> str:
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
def verify_password(password: str, hashed: str) -> bool:
return bcrypt.checkpw(password.encode(), hashed.encode())
# ------------------------------------------------------------------
# JWT
# ------------------------------------------------------------------
def create_token(user_id: int, rolle: str) -> str:
payload = {
"sub": str(user_id),
"rolle": rolle,
"exp": datetime.now(timezone.utc) + timedelta(days=JWT_EXPIRY),
"iat": datetime.now(timezone.utc),
}
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGO)
def decode_token(token: str) -> dict:
return jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGO])
# ------------------------------------------------------------------
# FastAPI Dependencies
# ------------------------------------------------------------------
def _get_token_from_request(
request: Request,
credentials: HTTPAuthorizationCredentials = Depends(security),
) -> str | None:
"""Token aus Bearer-Header oder HttpOnly-Cookie."""
if credentials:
return credentials.credentials
return request.cookies.get("by_token")
def get_current_user(
request: Request,
credentials: HTTPAuthorizationCredentials = Depends(security),
):
"""Dependency: gibt den eingeloggten User zurück oder wirft 401."""
token = _get_token_from_request(request, credentials)
if not token:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Nicht eingeloggt.")
try:
payload = decode_token(token)
except jwt.ExpiredSignatureError:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Session abgelaufen.")
except jwt.InvalidTokenError:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Ungültiges Token.")
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, luna_trial_until FROM users WHERE id=?",
(user_id,)
).fetchone()
if not row:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "User nicht gefunden.")
user = dict(row)
if user.get("is_banned"):
reason = user.get("ban_reason") or "Kein Grund angegeben."
raise HTTPException(status.HTTP_403_FORBIDDEN, f"Account gesperrt: {reason}")
return user
def get_current_user_optional(
request: Request,
credentials: HTTPAuthorizationCredentials = Depends(security),
):
"""Dependency: gibt User zurück falls eingeloggt, sonst None."""
try:
return get_current_user(request, credentials)
except HTTPException:
return None
def require_premium(user=Depends(get_current_user)):
"""Dependency: nur für Premium-User."""
if not user["is_premium"]:
raise HTTPException(
status.HTTP_402_PAYMENT_REQUIRED,
"Dieses Feature erfordert Ban Yaro Premium."
)
return user
def require_admin(user=Depends(get_current_user)):
"""Dependency: nur für Admins."""
if user["rolle"] != "admin":
raise HTTPException(status.HTTP_403_FORBIDDEN, "Kein Zugriff.")
return user
def require_social_media(user=Depends(get_current_user)):
"""Dependency: Social-Media-Manager, Luna-Probezugang oder Admin."""
from datetime import datetime as _dt
trial = user.get("luna_trial_until")
trial_active = bool(trial and _dt.utcnow().isoformat() < trial)
if not (user.get("is_social_media") or user["rolle"] == "admin" or trial_active):
raise HTTPException(status.HTTP_403_FORBIDDEN, "Kein Zugriff.")
return user