|
31 | 31 | from pydantic import BaseModel |
32 | 32 | from starlette.middleware.base import BaseHTTPMiddleware |
33 | 33 |
|
34 | | -from app import auth, catalog, compose_generator, database, exchange_rates, fleet_key, metrics |
| 34 | +from app import auth, catalog, compose_generator, database, exchange_rates, fleet_key, metrics, setup_token |
35 | 35 |
|
36 | 36 | logging.basicConfig( |
37 | 37 | level=logging.INFO, |
@@ -75,16 +75,16 @@ def _on_done(t: asyncio.Task) -> None: |
75 | 75 | _LOGIN_WINDOW_SECONDS = 300 |
76 | 76 |
|
77 | 77 |
|
78 | | -def _check_login_rate(client_ip: str) -> None: |
| 78 | +def _check_login_rate(ip: str) -> None: |
79 | 79 | now = monotonic() |
80 | | - attempts = _login_attempts[client_ip] |
81 | | - _login_attempts[client_ip] = [t for t in attempts if now - t < _LOGIN_WINDOW_SECONDS] |
82 | | - if len(_login_attempts[client_ip]) >= _LOGIN_MAX_ATTEMPTS: |
| 80 | + attempts = _login_attempts[ip] |
| 81 | + _login_attempts[ip] = [t for t in attempts if now - t < _LOGIN_WINDOW_SECONDS] |
| 82 | + if len(_login_attempts[ip]) >= _LOGIN_MAX_ATTEMPTS: |
83 | 83 | raise HTTPException(status_code=429, detail="Too many login attempts. Try again in a few minutes.") |
84 | 84 |
|
85 | 85 |
|
86 | | -def _record_failed_login(client_ip: str) -> None: |
87 | | - _login_attempts[client_ip].append(monotonic()) |
| 86 | +def _record_failed_login(ip: str) -> None: |
| 87 | + _login_attempts[ip].append(monotonic()) |
88 | 88 |
|
89 | 89 |
|
90 | 90 | def _safe_json(raw: str, fallback: Any = None) -> Any: |
@@ -273,6 +273,15 @@ async def _run_data_retention() -> None: |
273 | 273 | logger.warning("Data retention error: %s", exc) |
274 | 274 |
|
275 | 275 |
|
| 276 | +async def _run_vacuum() -> None: |
| 277 | + """Reclaim disk left by retention deletes (SQLite does not auto-shrink).""" |
| 278 | + try: |
| 279 | + await database.vacuum_database() |
| 280 | + logger.info("Database VACUUM complete") |
| 281 | + except Exception as exc: |
| 282 | + logger.warning("Database VACUUM error: %s", exc) |
| 283 | + |
| 284 | + |
276 | 285 | async def _check_stale_workers() -> None: |
277 | 286 | """Mark workers as offline if stale, and purge workers offline > 1 hour.""" |
278 | 287 | try: |
@@ -312,6 +321,22 @@ async def lifespan(app: FastAPI): |
312 | 321 | _changed = _u.get("password_changed_at") or 0.0 |
313 | 322 | if _changed: |
314 | 323 | auth.set_user_pwd_epoch(_u["id"], _changed) |
| 324 | + # First-run setup token: while no users exist, require a one-time token |
| 325 | + # (printed below) for /register so a proxy-exposed instance cannot be seized |
| 326 | + # by the first public visitor. Persisted in config so it survives restarts; |
| 327 | + # cleared once the owner account is created. |
| 328 | + if not await database.has_any_users(): |
| 329 | + _tok = await database.get_config("_setup_token") |
| 330 | + if not _tok: |
| 331 | + _tok = setup_token.generate() |
| 332 | + await database.set_config("_setup_token", _tok) |
| 333 | + setup_token.set_active(_tok) |
| 334 | + logger.warning( |
| 335 | + "FIRST-RUN SETUP: no account exists yet. Open /register and enter this " |
| 336 | + "one-time setup token to create the owner account: %s (shown only here; " |
| 337 | + "not embedded in any URL so it stays out of proxy logs and browser history)", |
| 338 | + _tok, |
| 339 | + ) |
315 | 340 | catalog.load_services() |
316 | 341 | catalog.register_sighup() |
317 | 342 |
|
@@ -355,6 +380,15 @@ def _on_job_event(event): |
355 | 380 | coalesce=True, |
356 | 381 | misfire_grace_time=300, |
357 | 382 | ) |
| 383 | + scheduler.add_job( |
| 384 | + _run_vacuum, |
| 385 | + "interval", |
| 386 | + weeks=1, |
| 387 | + id="db_vacuum", |
| 388 | + max_instances=1, |
| 389 | + coalesce=True, |
| 390 | + misfire_grace_time=300, |
| 391 | + ) |
358 | 392 | scheduler.add_job( |
359 | 393 | exchange_rates.refresh, |
360 | 394 | "interval", |
@@ -424,9 +458,11 @@ async def dispatch(self, request, call_next): |
424 | 458 | from app.deps import ( # noqa: E402 |
425 | 459 | _login_redirect, # noqa: F401 (re-exported for app.main.* test/router surface) |
426 | 460 | _require_auth_api, |
| 461 | + _require_first_run_access, # noqa: F401 (re-exported for app.main.* router surface) |
427 | 462 | _require_owner, |
428 | 463 | _require_private_network, # noqa: F401 (re-exported for app.main.* router surface) |
429 | 464 | _require_writer, |
| 465 | + client_ip, # noqa: F401 (re-exported for app.main.* router surface) |
430 | 466 | templates, # noqa: F401 (re-exported for app.main.* router/test surface) |
431 | 467 | ) |
432 | 468 |
|
|
0 commit comments