CI & Build / Build now, or wait for Android? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Failing after 9s
CI & Build / integration (push) Failing after 12s
CI & Build / Build & push image (push) Successful in 32s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m17s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m14s
Desktop (Tauri) / Update manifest (push) Successful in 5s
Operator: *"proxy hops defaults to 1 and should be in the settings UI not in the envs, we need the security values to be in the UI."* Overrules the call I made yesterday, and rule 25 is on your side — I argued deployment-topology, but the operator has to be able to SEE what protects them, and reading a container's environment is not seeing. Six new settings in a **Security** group: trusted proxy hops (default 1), the per-account and per-address sign-in limits with their shared window, and the sign-up limit with its own. `THOUGHTSYNC_TRUSTED_PROXY_HOPS` is gone; the rate limits are no longer hardcoded constants. **The hard part was keeping the throttle cheap.** It consults these BEFORE opening a database connection — deliberately, because a refused attempt is meant to cost nothing, and the hop count is needed to know who is even asking. A query per attempt would undo both. So there is a small cache seeded from the registry defaults (the app works with no database at all, which is what the DB-free unit lane relies on), loaded at boot, and refreshed on every settings save — the same live-update contract `session_ttl_days` already had. `SlidingWindow` now takes its limit and window as SUPPLIERS rather than values, so a saved number applies to the next attempt instead of the next deploy. **Bounds are rejected, not clamped.** A hop count of 99 would trust anything a caller sent; a sign-in limit of 0 would lock every account out permanently. Both now fail validation with a message naming the range, and the number input carries min/max so the browser objects first. Silently storing a different number than the one typed is how somebody ends up believing a protection is set to something it is not. `MAX_BUCKETS` stays a constant on purpose: it protects the limiter from itself rather than the app from a caller, and there is no operator judgment to apply. Two integration tests, because the whole point is the round trip: a dangerous value refused, a legitimate one reaching the cache the throttle reads and persisting; and every Security row reaching the admin payload with bounds and a description that explains itself.
52 lines
1.9 KiB
Python
52 lines
1.9 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
|
|
class Config:
|
|
"""Bootstrap configuration.
|
|
|
|
For a basic install, ``THOUGHTSYNC_DATABASE_URL`` is the ONLY required env var —
|
|
every other tunable lives in the DB-backed Settings UI (rule 25). The only other
|
|
env var is an optional "break-glass" item:
|
|
|
|
- ``THOUGHTSYNC_SECRET_KEY`` — optional override for the cookie-signing secret.
|
|
If unset, a key is generated and persisted in the DB (see
|
|
``thoughtsync.settings.load_or_create_secret_key``), so sessions survive
|
|
restarts with no volume required.
|
|
|
|
Uploaded media lives under ``DATA_DIR`` — a fixed, authoritative path
|
|
(``/var/thoughtsync``), intentionally NOT configurable (a mutable data path only
|
|
invites breakage). Mount a volume there if you want uploads to persist across
|
|
container recreation; a text-notes-only install never writes to it.
|
|
"""
|
|
|
|
DATA_DIR = "/var/thoughtsync"
|
|
|
|
DATABASE_URL = os.environ.get(
|
|
"THOUGHTSYNC_DATABASE_URL",
|
|
"postgresql+asyncpg://thoughtsync:thoughtsync@localhost:5432/thoughtsync",
|
|
)
|
|
|
|
@classmethod
|
|
def media_root(cls) -> Path:
|
|
return Path(cls.DATA_DIR) / "media"
|
|
|
|
@classmethod
|
|
def client_root(cls) -> Path:
|
|
"""Where the Android APK this server hands out lives.
|
|
|
|
Under DATA_DIR rather than baked into the image: the APK is ~55 MiB and an
|
|
install that never touches Android should not carry it. Being on the same
|
|
mounted volume as uploads also means an operator drops a build there once
|
|
and container recreation does not lose it. See client_dist.py.
|
|
"""
|
|
return Path(cls.DATA_DIR) / "client"
|
|
|
|
@classmethod
|
|
def secret_key_env(cls) -> str | None:
|
|
"""Optional break-glass override for the cookie-signing secret."""
|
|
return os.environ.get("THOUGHTSYNC_SECRET_KEY") or None
|
|
|