fix: link Tink Link to the user created in Step 2
All checks were successful
Build and Deploy / deploy (push) Successful in 22s
All checks were successful
Build and Deploy / deploy (push) Successful in 22s
Previously Step 2 (create user) and Step 3 (Tink Link) were disconnected — the bank connection went to an anonymous new user, not the one just created. Fix: Step 3 now calls /api/v1/oauth/authorization-grant/delegate with the user_id from session to get an authorization_code, which is injected into the Tink Link URL. This binds the bank connection to the correct customer. Also stores user_market in session so Step 3 uses the same market as Step 2. Shows a note confirming which user Tink Link is bound to.
This commit is contained in:
@@ -172,6 +172,7 @@ async def step2_post(request: Request,
|
|||||||
result = await client.create_user(app_token, external_user_id, market=market)
|
result = await client.create_user(app_token, external_user_id, market=market)
|
||||||
sess["user_id"] = result.get("user_id", "")
|
sess["user_id"] = result.get("user_id", "")
|
||||||
sess["external_user_id"] = external_user_id
|
sess["external_user_id"] = external_user_id
|
||||||
|
sess["user_market"] = market
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error = str(e)
|
error = str(e)
|
||||||
|
|
||||||
@@ -201,17 +202,41 @@ CONSOLE_CALLBACK = "https://console.tink.com/callback"
|
|||||||
async def step3_get(request: Request):
|
async def step3_get(request: Request):
|
||||||
sess = _session(request)
|
sess = _session(request)
|
||||||
s = get_settings()
|
s = get_settings()
|
||||||
client = _client()
|
client = _client(log_cb=_logger(sess))
|
||||||
|
|
||||||
tink_link_url = client.get_tink_link_url(market="DK")
|
|
||||||
dev_tink_link_url = client.get_tink_link_url(
|
|
||||||
market="DK", redirect_uri_override=CONSOLE_CALLBACK
|
|
||||||
)
|
|
||||||
error = None
|
error = None
|
||||||
credentials = None
|
credentials = None
|
||||||
|
auth_code_error = None
|
||||||
cb_success = request.query_params.get("cb_success")
|
cb_success = request.query_params.get("cb_success")
|
||||||
cb_error = request.query_params.get("cb_error")
|
cb_error = request.query_params.get("cb_error")
|
||||||
|
|
||||||
|
# If we have a user from Step 2, generate an authorization_code for them
|
||||||
|
# so Tink Link connects the bank to THAT specific user (not a new anonymous one)
|
||||||
|
user_id = sess.get("user_id", "")
|
||||||
|
app_token = sess.get("app_token", "")
|
||||||
|
authorization_code = None
|
||||||
|
|
||||||
|
if user_id and app_token and not sess.get("user_token"):
|
||||||
|
try:
|
||||||
|
grant = await client.get_authorization_grant_token(
|
||||||
|
app_token=app_token,
|
||||||
|
user_id=user_id,
|
||||||
|
scope="accounts:read,transactions:read,credentials:read",
|
||||||
|
)
|
||||||
|
authorization_code = grant.get("code", "")
|
||||||
|
except Exception as e:
|
||||||
|
auth_code_error = str(e)
|
||||||
|
|
||||||
|
tink_link_url = client.get_tink_link_url(
|
||||||
|
market=sess.get("user_market", "DK"),
|
||||||
|
authorization_code=authorization_code or None,
|
||||||
|
)
|
||||||
|
dev_tink_link_url = client.get_tink_link_url(
|
||||||
|
market=sess.get("user_market", "DK"),
|
||||||
|
authorization_code=authorization_code or None,
|
||||||
|
redirect_uri_override=CONSOLE_CALLBACK,
|
||||||
|
)
|
||||||
|
|
||||||
# Demo mode: auto-mark as connected with mock data
|
# Demo mode: auto-mark as connected with mock data
|
||||||
if s.demo_mode and not sess.get("user_token"):
|
if s.demo_mode and not sess.get("user_token"):
|
||||||
sess["user_token"] = "demo-mode-token"
|
sess["user_token"] = "demo-mode-token"
|
||||||
@@ -227,35 +252,46 @@ async def step3_get(request: Request):
|
|||||||
try:
|
try:
|
||||||
credentials = await client.list_credentials(user_token)
|
credentials = await client.list_credentials(user_token)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# credentials:read may not be granted in simple Tink Link flow — not fatal
|
|
||||||
if "403" in str(e):
|
if "403" in str(e):
|
||||||
credentials = {"note": "credentials:read kræver authorization-grant flow"}
|
credentials = {"note": "credentials:read kræver authorization-grant flow"}
|
||||||
else:
|
else:
|
||||||
error = str(e)
|
error = str(e)
|
||||||
|
|
||||||
|
external_user_id = sess.get("external_user_id", "")
|
||||||
|
uid_display = user_id or "$USER_ID"
|
||||||
curl_example = (
|
curl_example = (
|
||||||
"# Simpelt Tink Link flow — ingen pre-oprettet bruger nødvendig\n"
|
"# Step 1: Hent authorization_code for den specifikke bruger\n"
|
||||||
f"# Redirect brugeren direkte til:\n"
|
"curl -X POST https://api.tink.com/api/v1/oauth/authorization-grant/delegate \\\n"
|
||||||
f"https://link.tink.com/1.0/transactions/connect-accounts\n"
|
" -H 'Authorization: Bearer $APP_TOKEN' \\\n"
|
||||||
f" ?client_id=$CLIENT_ID\n"
|
" -d 'actor_client_id=$CLIENT_ID' \\\n"
|
||||||
f" &redirect_uri=$REDIRECT_URI\n"
|
f" -d 'user_id={uid_display}' \\\n"
|
||||||
f" &market=DK\n"
|
" -d 'scope=accounts:read,transactions:read'\n\n"
|
||||||
f" &scope=accounts:read,transactions:read,credentials:read\n\n"
|
"# Step 2: Send authorization_code med i Tink Link URL\n"
|
||||||
"# Tink Link redirecter tilbage med ?code=... som du exchangeer for user token:\n"
|
"https://link.tink.com/1.0/transactions/connect-accounts\n"
|
||||||
|
" ?client_id=$CLIENT_ID\n"
|
||||||
|
" &redirect_uri=$REDIRECT_URI\n"
|
||||||
|
" &authorization_code=$CODE ← binder til din bruger!\n\n"
|
||||||
|
"# Step 3: Exchange callback code for user token\n"
|
||||||
"curl -X POST https://api.tink.com/api/v1/oauth/token \\\n"
|
"curl -X POST https://api.tink.com/api/v1/oauth/token \\\n"
|
||||||
" -d 'grant_type=authorization_code' \\\n"
|
" -d 'grant_type=authorization_code' \\\n"
|
||||||
" -d 'client_id=$CLIENT_ID' \\\n"
|
" -d 'client_id=$CLIENT_ID' \\\n"
|
||||||
" -d 'client_secret=$CLIENT_SECRET' \\\n"
|
" -d 'client_secret=$CLIENT_SECRET' \\\n"
|
||||||
" -d 'code=$CODE'"
|
" -d 'code=$CALLBACK_CODE'"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
linked_user_note = None
|
||||||
|
if authorization_code:
|
||||||
|
linked_user_note = f"Tink Link er bundet til bruger <code class='text-violet-300 font-mono'>{external_user_id}</code> via authorization_code"
|
||||||
|
elif user_id:
|
||||||
|
linked_user_note = f"⚠️ Kunne ikke hente authorization_code: {auth_code_error or 'mangler app token'} — Tink Link opretter en ny anonym bruger"
|
||||||
|
|
||||||
return templates.TemplateResponse("step.html", _ctx(request, {
|
return templates.TemplateResponse("step.html", _ctx(request, {
|
||||||
"step": 3,
|
"step": 3,
|
||||||
"title": "Tilslut Bank",
|
"title": "Tilslut Bank",
|
||||||
"subtitle": "Tink Link — Bank Connection",
|
"subtitle": "Tink Link — Bank Connection",
|
||||||
"endpoint": "Tink Link /1.0/transactions/connect-accounts",
|
"endpoint": "GET /api/v1/oauth/authorization-grant/delegate",
|
||||||
"api_version": "Link v1",
|
"api_version": "v1",
|
||||||
"description": "Brugeren åbner Tink Link, vælger Tink Demo Bank og logger ind med test-credentials fra Console. Tink redirecter tilbage med en authorization_code som automatisk exchang'es til et user token.",
|
"description": linked_user_note or "Åbn Tink Link, vælg Tink Demo Bank og log ind med test-credentials.",
|
||||||
"curl_example": curl_example,
|
"curl_example": curl_example,
|
||||||
"result": credentials,
|
"result": credentials,
|
||||||
"tink_link_url": tink_link_url,
|
"tink_link_url": tink_link_url,
|
||||||
|
|||||||
Reference in New Issue
Block a user