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>
99 lines
3.4 KiB
Python
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)
|