From 0e18ef22916817d8843f145d65ec47195b5838c5 Mon Sep 17 00:00:00 2001 From: owain Date: Mon, 8 Jun 2026 13:26:35 +0100 Subject: [PATCH] Fix PocketID secret wiped on re-save; log token-exchange failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit save_pocketid_config cleared the stored client secret whenever the form was submitted with a blank secret field — but the UI hint says blank means "keep existing". Re-saving config (e.g. to set the allowed group) therefore wiped the secret and broke token exchange ("Token exchange failed"). Now a blank field keeps the existing secret; only a non-empty value overwrites it. Also log PocketID's actual token-endpoint response body on failure so the cause (invalid_client, redirect_uri mismatch, etc.) is visible in backend logs. Co-Authored-By: Claude Opus 4.8 --- backend/app/api/auth.py | 1 + backend/app/api/profile.py | 6 ++++-- milevault_export/backend/app/api/auth.py | 1 + milevault_export/backend/app/api/profile.py | 6 ++++-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py index 4e76906..6a637d3 100644 --- a/backend/app/api/auth.py +++ b/backend/app/api/auth.py @@ -119,6 +119,7 @@ async def pocketid_callback(code: str, db: AsyncSession = Depends(get_db)): "client_id": client_id, "client_secret": client_secret}, ) if resp.status_code != 200: + print(f"PocketID token exchange failed ({resp.status_code}): {resp.text}") raise HTTPException(status_code=400, detail="Token exchange failed") tokens = resp.json() userinfo_resp = await client.get( diff --git a/backend/app/api/profile.py b/backend/app/api/profile.py index 0e307d4..c1e9977 100644 --- a/backend/app/api/profile.py +++ b/backend/app/api/profile.py @@ -154,8 +154,10 @@ async def save_pocketid_config( current_user.pocketid_issuer = body.issuer.rstrip("/") if body.issuer else None if body.client_id is not None: current_user.pocketid_client_id = body.client_id or None - if body.client_secret is not None: - current_user.pocketid_client_secret = body.client_secret or None + # Only overwrite the secret when a non-empty value is supplied; a blank + # field means "keep the existing secret" (matches the UI hint). + if body.client_secret: + current_user.pocketid_client_secret = body.client_secret if body.allowed_group is not None: current_user.pocketid_allowed_group = body.allowed_group.strip() or None await db.commit() diff --git a/milevault_export/backend/app/api/auth.py b/milevault_export/backend/app/api/auth.py index 4e76906..6a637d3 100644 --- a/milevault_export/backend/app/api/auth.py +++ b/milevault_export/backend/app/api/auth.py @@ -119,6 +119,7 @@ async def pocketid_callback(code: str, db: AsyncSession = Depends(get_db)): "client_id": client_id, "client_secret": client_secret}, ) if resp.status_code != 200: + print(f"PocketID token exchange failed ({resp.status_code}): {resp.text}") raise HTTPException(status_code=400, detail="Token exchange failed") tokens = resp.json() userinfo_resp = await client.get( diff --git a/milevault_export/backend/app/api/profile.py b/milevault_export/backend/app/api/profile.py index 0e307d4..c1e9977 100644 --- a/milevault_export/backend/app/api/profile.py +++ b/milevault_export/backend/app/api/profile.py @@ -154,8 +154,10 @@ async def save_pocketid_config( current_user.pocketid_issuer = body.issuer.rstrip("/") if body.issuer else None if body.client_id is not None: current_user.pocketid_client_id = body.client_id or None - if body.client_secret is not None: - current_user.pocketid_client_secret = body.client_secret or None + # Only overwrite the secret when a non-empty value is supplied; a blank + # field means "keep the existing secret" (matches the UI hint). + if body.client_secret: + current_user.pocketid_client_secret = body.client_secret if body.allowed_group is not None: current_user.pocketid_allowed_group = body.allowed_group.strip() or None await db.commit()