sync: encrypted cloud backup for portfolios + settings UX rework
Adds opt-in client-side-encrypted portfolio sync (paid). Browser
PBKDF2(PIN) → AES-GCM, server HKDF(pepper, user_id) outer wrap;
server stores opaque bytes only. Sliding-window rate limit on GET.
- new portfolio_sync table (migration 0015)
- POST/GET/DELETE /api/portfolio/sync + /status
- app/services/portfolio_sync.py crypto + rate limit
- app/routers/sync.py paid-gated
- app/static/js/portfolio-sync.js WebCrypto wrapper
- settings page: enable/disable + PIN modal
- PORTFOLIO_SYNC_PEPPER setting (warn on startup if missing)
Settings + import rework:
- /upload merged into /settings#import (legacy route 302s)
- drop CSV → auto-parse → preview → Import only / Import & sync
- nav slimmed to Dashboard / News / Log
- Settings + Logout moved to a user dropdown
- brand logo links to /
Collateral fixes:
- settings 500: re-fetch User in current session before mutating
referral_code (assign_code_if_missing was refreshing a User
loaded in the auth dep's now-closed session)
- csv_import: distinct error for unfunded T212 pies (all qty=0)
- db.py: drop pool_pre_ping (aiomysql 0.3.2 incompat); pin
isolation_level=READ COMMITTED to avoid gap-lock deadlocks
- alembic env: disable_existing_loggers=False so in-process
migrations don't silence uvicorn's loggers
- docker-compose.override.yml: dev-only volume mount + --reload
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
89632e9937
commit
f326b41a08
23 changed files with 1637 additions and 95 deletions
|
|
@ -20,6 +20,7 @@ from app.logging import configure_logging, get_logger
|
|||
from app.routers import api as api_router
|
||||
from app.routers import auth as auth_router
|
||||
from app.routers import pages as pages_router
|
||||
from app.routers import sync as sync_router
|
||||
from app.routers import universe as universe_router
|
||||
from app.services.feeds_bootstrap import bootstrap_feeds
|
||||
|
||||
|
|
@ -41,6 +42,12 @@ def _run_migrations() -> None:
|
|||
async def lifespan(app: FastAPI):
|
||||
configure_logging()
|
||||
log.info("cassandra.startup")
|
||||
s = get_settings()
|
||||
if not s.PORTFOLIO_SYNC_PEPPER and not s.DATABASE_URL.startswith("sqlite"):
|
||||
# Outer wrap still works (it just degrades to a per-user derived
|
||||
# key with no shared secret), but a DB leak would let an attacker
|
||||
# brute-force the PIN offline. Loud warning, not a hard failure.
|
||||
log.warning("cassandra.portfolio_sync.pepper_missing")
|
||||
try:
|
||||
# Alembic's env.py uses asyncio.run() internally; offload it to a
|
||||
# worker thread so it doesn't collide with FastAPI's running loop.
|
||||
|
|
@ -77,4 +84,5 @@ app.mount(
|
|||
app.include_router(auth_router.router, tags=["auth"])
|
||||
app.include_router(api_router.router, prefix="/api", tags=["api"])
|
||||
app.include_router(universe_router.router, prefix="/api", tags=["universe"])
|
||||
app.include_router(sync_router.router, tags=["portfolio-sync"])
|
||||
app.include_router(pages_router.router, tags=["pages"])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue