Test test test
All checks were successful
Build and Deploy / deploy (push) Successful in 22s

This commit is contained in:
Henrik Jess Nielsen
2026-05-22 19:18:38 +02:00
parent 3f687bb212
commit a77c709d4d
4 changed files with 377 additions and 189 deletions

View File

@@ -20,7 +20,7 @@ router = APIRouter()
templates = Jinja2Templates(directory="src/templates")
def _client() -> TinkClient:
def _client(log_cb=None) -> TinkClient:
s = get_settings()
return TinkClient(
client_id=s.tink_client_id,
@@ -28,6 +28,7 @@ def _client() -> TinkClient:
redirect_uri=s.tink_redirect_uri,
api_base=s.tink_api_base,
link_base=s.tink_link_base,
on_request=log_cb,
)
@@ -35,6 +36,23 @@ def _session(request: Request) -> dict:
return request.session.setdefault("demo", {})
def _ctx(request: Request, extra: dict) -> dict:
"""Base template context — always includes session_customer."""
sess = _session(request)
return {"request": request, "session_customer": sess.get("external_user_id", ""), **extra}
def _logger(sess: dict):
"""Returns a callback that appends log entries to sess['api_log']."""
def cb(entry: dict):
log = sess.setdefault("api_log", [])
log.append(entry)
# keep last 50 entries
if len(log) > 50:
sess["api_log"] = log[-50:]
return cb
# ---------------------------------------------------------------------------
# Landing
# ---------------------------------------------------------------------------
@@ -57,7 +75,8 @@ async def reset_demo(request: Request):
@router.get("/demo/step/1", response_class=HTMLResponse)
async def step1(request: Request):
client = _client()
sess = _session(request)
client = _client(log_cb=_logger(sess))
s = get_settings()
error = None
result = None
@@ -70,12 +89,11 @@ async def step1(request: Request):
)
try:
result = await client.get_app_token(scope="user:create")
_session(request)["app_token"] = result["access_token"]
sess["app_token"] = result["access_token"]
except Exception as e:
error = str(e)
return templates.TemplateResponse("step.html", {
"request": request,
return templates.TemplateResponse("step.html", _ctx(request, {
"step": 1,
"title": "Authenticate",
"subtitle": "Client Credentials Flow",
@@ -87,7 +105,7 @@ async def step1(request: Request):
"error": error,
"next_step": 2,
"prev_step": None,
})
}))
# ---------------------------------------------------------------------------
@@ -99,9 +117,46 @@ async def step2_get(request: Request):
sess = _session(request)
s = get_settings()
app_token = sess.get("app_token", "")
external_user_id = f"moneycapp-demo-{secrets.token_hex(4)}"
error = None
body = json.dumps({"external_user_id": external_user_id, "market": "DK", "locale": "da_DK"}, indent=2)
if not app_token:
error = "Mangler app token — gå tilbage til Step 1 og kør authentication først."
# If user already created this session, skip the form
existing_user_id = sess.get("user_id", "")
existing_external_id = sess.get("external_user_id", "")
return templates.TemplateResponse("step2.html", _ctx(request, {
"step": 2,
"error": error,
"existing_user_id": existing_user_id,
"existing_external_id": existing_external_id,
"app_token_ok": bool(app_token),
"next_step": 3,
"prev_step": 1,
}))
@router.post("/demo/step/2", response_class=HTMLResponse)
async def step2_post(request: Request,
customer_name: str = Form(default=""),
market: str = Form(default="DK")):
sess = _session(request)
s = get_settings()
app_token = sess.get("app_token", "")
error = None
result = None
# Build external_user_id from customer name
if customer_name.strip():
slug = customer_name.strip().lower().replace(" ", "-")
import re
slug = re.sub(r"[^a-z0-9\-]", "", slug)
external_user_id = f"moneycapp-{slug}"
else:
external_user_id = f"moneycapp-demo-{secrets.token_hex(4)}"
body = json.dumps({"external_user_id": external_user_id, "market": market, "locale": "da_DK"}, indent=2)
curl_example = (
f"curl -X POST https://api.tink.com/api/v1/user/create \\\n"
f" -H 'Authorization: Bearer $APP_TOKEN' \\\n"
@@ -109,33 +164,30 @@ async def step2_get(request: Request):
f" -d '{body}'"
)
error = None
result = None
if not app_token:
error = "Mangler app token — gå tilbage til Step 1 og kør authentication først."
error = "Mangler app token — gå tilbage til Step 1."
else:
try:
client = _client()
result = await client.create_user(app_token, external_user_id)
client = _client(log_cb=_logger(sess))
result = await client.create_user(app_token, external_user_id, market=market)
sess["user_id"] = result.get("user_id", "")
sess["external_user_id"] = external_user_id
except Exception as e:
error = str(e)
return templates.TemplateResponse("step.html", {
"request": request,
return templates.TemplateResponse("step.html", _ctx(request, {
"step": 2,
"title": "Opret Bruger",
"subtitle": "Create Test Customer",
"subtitle": f"Kunde: {external_user_id}",
"endpoint": "POST /api/v1/user/create",
"api_version": "v1",
"description": "Vi opretter en ny Tink-bruger med et unikt external_user_id — det er dit interne kunde-ID. Tink returnerer et user_id som vi bruger i de næste kald.",
"description": f"Oprettet Tink-bruger med external_user_id <code class='text-violet-300 font-mono'>{external_user_id}</code> — dette er dit interne kunde-ID. Tink returnerer et user_id som bruges i næste kald.",
"curl_example": curl_example,
"result": result,
"error": error,
"next_step": 3,
"prev_step": 1,
})
}))
# ---------------------------------------------------------------------------
@@ -175,7 +227,11 @@ async def step3_get(request: Request):
try:
credentials = await client.list_credentials(user_token)
except Exception as e:
error = str(e)
# credentials:read may not be granted in simple Tink Link flow — not fatal
if "403" in str(e):
credentials = {"note": "credentials:read kræver authorization-grant flow"}
else:
error = str(e)
curl_example = (
"# Simpelt Tink Link flow — ingen pre-oprettet bruger nødvendig\n"
@@ -193,8 +249,7 @@ async def step3_get(request: Request):
" -d 'code=$CODE'"
)
return templates.TemplateResponse("step.html", {
"request": request,
return templates.TemplateResponse("step.html", _ctx(request, {
"step": 3,
"title": "Tilslut Bank",
"subtitle": "Tink Link — Bank Connection",
@@ -209,7 +264,7 @@ async def step3_get(request: Request):
"cb_success": cb_success,
"next_step": 4,
"prev_step": 2,
})
}))
@router.post("/demo/step/3", response_class=HTMLResponse)
@@ -217,7 +272,7 @@ async def step3_post(request: Request, code: str = Form(...)):
"""Manual code entry — exchange a code obtained via console.tink.com/callback."""
sess = _session(request)
try:
client = _client()
client = _client(log_cb=_logger(sess))
tokens = await client.exchange_code_for_token(
code=code.strip(),
redirect_uri=CONSOLE_CALLBACK,
@@ -238,7 +293,7 @@ async def tink_callback(request: Request, code: Optional[str] = None,
return RedirectResponse(f"/demo/step/3?error={error}")
if code:
try:
client = _client()
client = _client(log_cb=_logger(sess))
tokens = await client.exchange_code_for_token(code)
sess["user_token"] = tokens.get("access_token", "")
return RedirectResponse("/demo/step/3?cb_success=1", status_code=303)