lazyflat/web/app.py
EiSiMo d06dfdaca1 refactor: rename wohnungsdidi → lazyflat
Container names, FastAPI titles, email subjects, filenames, brand text,
session cookie, User-Agent, docstrings, README. Volume lazyflat_data and
/data/lazyflat.sqlite already used the new name, so on-disk data is
preserved; dropped the now-obsolete legacy-rename comments.

Side effect: SESSION_COOKIE_NAME change logs everyone out on deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 09:26:12 +02:00

99 lines
3.4 KiB
Python

"""
lazyflat web app.
Tabs:
- / → Wohnungen
- /bewerbungen → Bewerbungen (history + forensics ZIP for failed runs)
- /einstellungen/<section> → Einstellungen: profil | filter | benachrichtigungen | account
- /admin/<section> → Admin-only: protokoll | benutzer
All state-changing POSTs require CSRF. Internal endpoints require INTERNAL_API_KEY.
This module is intentionally thin: lifespan, middleware, template filters, and
`include_router` calls. All business logic lives in `common.py` and
`routes/*.py`.
"""
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, Response
from fastapi.staticfiles import StaticFiles
import db
import enrichment
import retention
from auth import bootstrap_admin
from common import _de_dt, _iso_utc, logger, templates
from routes.admin import router as admin_router
from routes.auth import router as auth_router
from routes.bewerbungen import router as bewerbungen_router
from routes.einstellungen import router as einstellungen_router
from routes.internal import router as internal_router
from routes.wohnungen import router as wohnungen_router
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)-5s %(name)s: %(message)s",
datefmt="%H:%M:%S",
)
logging.getLogger("urllib3").setLevel(logging.WARNING)
# ---------------------------------------------------------------------------
# App + Jinja
# ---------------------------------------------------------------------------
@asynccontextmanager
async def lifespan(_app: FastAPI):
db.init_db()
db.seed_secrets_from_env()
bootstrap_admin()
retention.start()
logger.info("web service ready")
yield
app = FastAPI(lifespan=lifespan, title="lazyflat", docs_url=None, redoc_url=None, openapi_url=None)
app.mount("/static", StaticFiles(directory="static"), name="static")
# Template filters are registered on the shared Jinja environment (owned by
# common.templates) so every router that renders a template sees them.
templates.env.filters["de_dt"] = _de_dt
templates.env.filters["iso_utc"] = _iso_utc
templates.env.filters["flat_slug"] = lambda s: enrichment.flat_slug(str(s or ""))
@app.middleware("http")
async def security_headers(request: Request, call_next):
resp: Response = await call_next(request)
resp.headers.setdefault("X-Frame-Options", "DENY")
resp.headers.setdefault("X-Content-Type-Options", "nosniff")
resp.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin")
resp.headers.setdefault("Permissions-Policy", "geolocation=(), camera=(), microphone=()")
resp.headers.setdefault(
"Content-Security-Policy",
"default-src 'self'; "
"script-src 'self' https://cdn.tailwindcss.com https://unpkg.com; "
"style-src 'self' https://cdn.tailwindcss.com https://unpkg.com 'unsafe-inline'; "
"img-src 'self' data: "
"https://*.basemaps.cartocdn.com https://basemaps.cartocdn.com "
"https://unpkg.com; "
"connect-src 'self'; frame-ancestors 'none';"
)
return resp
@app.get("/health")
def health():
return {"status": "ok"}
# Routers are registered without prefixes — paths stay absolute.
app.include_router(auth_router)
app.include_router(wohnungen_router)
app.include_router(bewerbungen_router)
app.include_router(einstellungen_router)
app.include_router(admin_router)
app.include_router(internal_router)