fix: production deployment — Docker, Nomad, Consul KV, SHA tags
- Dockerfile: multi-stage build, non-root user, src/static tracked with .gitkeep - Nomad job: force_pull=true, Traefik router fixed to tink-demo.i80.dk, loadbalancer.server.port=8000, job renamed from moneycapp-tink-demo - CI/CD: git SHA image tags (deterministic deploys), removed .env.production baking — secrets injected at runtime via Consul KV template stanza - Session security: asyncio lock prevents duplicate code exchange on callback, guard for already-stored token, api_log moved server-side (cookie overflow fix) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -40,7 +40,10 @@ def _session(request: Request) -> dict:
|
||||
# Server-side token store — keeps JWTs OUT of the session cookie
|
||||
# (cookie limit is 4KB; two JWTs alone are ~1.3KB before base64 overhead)
|
||||
# ---------------------------------------------------------------------------
|
||||
import asyncio
|
||||
|
||||
_token_store: dict[str, dict] = {} # sid → {"app_token": str, "user_token": str}
|
||||
_callback_locks: dict[str, asyncio.Lock] = {} # sid → Lock (prevents concurrent code exchange)
|
||||
|
||||
|
||||
def _get_sid(sess: dict) -> str:
|
||||
@@ -69,16 +72,24 @@ def _ctx(request: Request, extra: dict) -> dict:
|
||||
|
||||
|
||||
def _logger(sess: dict):
|
||||
"""Returns a callback that appends log entries to sess['api_log']."""
|
||||
"""Returns a callback that appends log entries to server-side store (not cookie)."""
|
||||
def cb(entry: dict):
|
||||
log = sess.setdefault("api_log", [])
|
||||
sid = sess.get("sid", "")
|
||||
if not sid:
|
||||
return
|
||||
store = _token_store.setdefault(sid, {})
|
||||
log = store.setdefault("api_log", [])
|
||||
log.append(entry)
|
||||
# keep last 50 entries
|
||||
if len(log) > 50:
|
||||
sess["api_log"] = log[-50:]
|
||||
store["api_log"] = log[-50:]
|
||||
return cb
|
||||
|
||||
|
||||
def _get_api_log(sess: dict) -> list:
|
||||
sid = sess.get("sid", "")
|
||||
return _token_store.get(sid, {}).get("api_log", [])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Landing
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -108,7 +119,7 @@ async def debug_session(request: Request):
|
||||
for k, v in sess.items()
|
||||
if k != "api_log"
|
||||
}
|
||||
safe["api_log_count"] = len(sess.get("api_log", []))
|
||||
safe["api_log_count"] = len(_get_api_log(sess))
|
||||
safe["cookie_size_bytes"] = len(str(request.session))
|
||||
return safe
|
||||
|
||||
@@ -402,28 +413,35 @@ async def tink_callback(request: Request, code: Optional[str] = None,
|
||||
print(f"[CALLBACK] Tink returned error: {error}")
|
||||
return RedirectResponse(f"/demo/step/3?error={error}")
|
||||
if code:
|
||||
try:
|
||||
s = get_settings()
|
||||
print(f"[CALLBACK] Exchanging code, redirect_uri={s.tink_redirect_uri!r}")
|
||||
client = _client(log_cb=_logger(sess))
|
||||
tokens = await client.exchange_code_for_token(
|
||||
code, redirect_uri=s.tink_redirect_uri
|
||||
)
|
||||
print(f"[CALLBACK] Token response keys: {list(tokens.keys())}")
|
||||
user_token = tokens.get("access_token", "")
|
||||
if not user_token:
|
||||
print(f"[CALLBACK] ERROR: access_token missing, got: {tokens}")
|
||||
return RedirectResponse(
|
||||
f"/demo/step/3?cb_error=Token+exchange+ok+but+access_token+missing+in+response",
|
||||
status_code=303,
|
||||
sid = sess.get("sid", "unknown")
|
||||
if sid not in _callback_locks:
|
||||
_callback_locks[sid] = asyncio.Lock()
|
||||
async with _callback_locks[sid]:
|
||||
if _load_token(sess, "user_token"):
|
||||
print(f"[CALLBACK] Already have user_token — skipping duplicate exchange")
|
||||
return RedirectResponse("/demo/step/3?cb_success=1", status_code=303)
|
||||
try:
|
||||
s = get_settings()
|
||||
print(f"[CALLBACK] Exchanging code, redirect_uri={s.tink_redirect_uri!r}")
|
||||
client = _client(log_cb=_logger(sess))
|
||||
tokens = await client.exchange_code_for_token(
|
||||
code, redirect_uri=s.tink_redirect_uri
|
||||
)
|
||||
_store_token(sess, "user_token", user_token)
|
||||
print(f"[CALLBACK] SUCCESS — user_token saved ({len(user_token)} chars)")
|
||||
return RedirectResponse("/demo/step/3?cb_success=1", status_code=303)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f"[CALLBACK] EXCEPTION: {e}\n{traceback.format_exc()}")
|
||||
return RedirectResponse(f"/demo/step/3?cb_error={str(e)}", status_code=303)
|
||||
print(f"[CALLBACK] Token response keys: {list(tokens.keys())}")
|
||||
user_token = tokens.get("access_token", "")
|
||||
if not user_token:
|
||||
print(f"[CALLBACK] ERROR: access_token missing, got: {tokens}")
|
||||
return RedirectResponse(
|
||||
f"/demo/step/3?cb_error=Token+exchange+ok+but+access_token+missing+in+response",
|
||||
status_code=303,
|
||||
)
|
||||
_store_token(sess, "user_token", user_token)
|
||||
print(f"[CALLBACK] SUCCESS — user_token saved ({len(user_token)} chars)")
|
||||
return RedirectResponse("/demo/step/3?cb_success=1", status_code=303)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f"[CALLBACK] EXCEPTION: {e}\n{traceback.format_exc()}")
|
||||
return RedirectResponse(f"/demo/step/3?cb_error={str(e)}", status_code=303)
|
||||
print(f"[CALLBACK] No code — bare redirect to step 3")
|
||||
return RedirectResponse("/demo/step/3", status_code=303)
|
||||
|
||||
@@ -617,7 +635,7 @@ async def step6(request: Request):
|
||||
@router.get("/demo/log", response_class=HTMLResponse)
|
||||
async def api_log(request: Request):
|
||||
sess = _session(request)
|
||||
log = sess.get("api_log", [])
|
||||
log = _get_api_log(sess)
|
||||
return templates.TemplateResponse("log.html", _ctx(request, {
|
||||
"log": list(reversed(log)), # newest first
|
||||
"log_count": len(log),
|
||||
@@ -627,7 +645,9 @@ async def api_log(request: Request):
|
||||
@router.post("/demo/log/clear")
|
||||
async def clear_log(request: Request):
|
||||
sess = _session(request)
|
||||
sess["api_log"] = []
|
||||
sid = sess.get("sid", "")
|
||||
if sid and sid in _token_store:
|
||||
_token_store[sid].pop("api_log", None)
|
||||
return RedirectResponse("/demo/log", status_code=303)
|
||||
|
||||
|
||||
|
||||
0
src/static/.gitkeep
Normal file
0
src/static/.gitkeep
Normal file
Reference in New Issue
Block a user