From e0ddc4cbf4fc0587d6abd8c3bf9f683ab17e09f5 Mon Sep 17 00:00:00 2001 From: owain Date: Mon, 8 Jun 2026 13:37:19 +0100 Subject: [PATCH] Fix PocketID config lookup when multiple admins exist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _get_pocketid_config / _get_allowed_group selected an admin row with an unordered LIMIT 1. With more than one admin (e.g. the seeded password admin plus a passkey-linked admin), this non-deterministically returned an admin without PocketID config — making the passkey button disappear (available=false) and group gating inconsistent. Add _config_admin() which prefers the admin that actually has an issuer set, then falls back to the lowest-id admin. Co-Authored-By: Claude Opus 4.8 --- backend/app/api/auth.py | 29 ++++++++++++++++++++---- milevault_export/backend/app/api/auth.py | 29 ++++++++++++++++++++---- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py index 6a637d3..70a1f73 100644 --- a/backend/app/api/auth.py +++ b/backend/app/api/auth.py @@ -14,10 +14,32 @@ from app.models.user import User router = APIRouter() +async def _config_admin(db: AsyncSession): + """The admin row that holds instance-wide PocketID settings. + + Settings live on an admin user row, but there can be more than one admin. + Prefer the admin that actually has an issuer configured; otherwise fall back + to the lowest-id admin. Without this, an unordered LIMIT 1 could return an + admin with no config and make PocketID look disabled / gating inconsistent. + """ + result = await db.execute( + select(User) + .where(User.is_admin == True, User.pocketid_issuer.isnot(None)) + .order_by(User.id) + .limit(1) + ) + admin = result.scalar_one_or_none() + if admin is None: + result = await db.execute( + select(User).where(User.is_admin == True).order_by(User.id).limit(1) + ) + admin = result.scalar_one_or_none() + return admin + + async def _get_pocketid_config(db: AsyncSession): """Get PocketID config from DB (admin user) falling back to env vars.""" - result = await db.execute(select(User).where(User.is_admin == True).limit(1)) - admin = result.scalar_one_or_none() + admin = await _config_admin(db) issuer = (admin and admin.pocketid_issuer) or settings.pocketid_issuer client_id = (admin and admin.pocketid_client_id) or settings.pocketid_client_id client_secret = (admin and admin.pocketid_client_secret) or settings.pocketid_client_secret @@ -26,8 +48,7 @@ async def _get_pocketid_config(db: AsyncSession): async def _get_allowed_group(db: AsyncSession): """Group a PocketID user must belong to in order to sign in (None = allow all).""" - result = await db.execute(select(User).where(User.is_admin == True).limit(1)) - admin = result.scalar_one_or_none() + admin = await _config_admin(db) group = (admin and admin.pocketid_allowed_group) or settings.pocketid_allowed_group return (group or "").strip() or None diff --git a/milevault_export/backend/app/api/auth.py b/milevault_export/backend/app/api/auth.py index 6a637d3..70a1f73 100644 --- a/milevault_export/backend/app/api/auth.py +++ b/milevault_export/backend/app/api/auth.py @@ -14,10 +14,32 @@ from app.models.user import User router = APIRouter() +async def _config_admin(db: AsyncSession): + """The admin row that holds instance-wide PocketID settings. + + Settings live on an admin user row, but there can be more than one admin. + Prefer the admin that actually has an issuer configured; otherwise fall back + to the lowest-id admin. Without this, an unordered LIMIT 1 could return an + admin with no config and make PocketID look disabled / gating inconsistent. + """ + result = await db.execute( + select(User) + .where(User.is_admin == True, User.pocketid_issuer.isnot(None)) + .order_by(User.id) + .limit(1) + ) + admin = result.scalar_one_or_none() + if admin is None: + result = await db.execute( + select(User).where(User.is_admin == True).order_by(User.id).limit(1) + ) + admin = result.scalar_one_or_none() + return admin + + async def _get_pocketid_config(db: AsyncSession): """Get PocketID config from DB (admin user) falling back to env vars.""" - result = await db.execute(select(User).where(User.is_admin == True).limit(1)) - admin = result.scalar_one_or_none() + admin = await _config_admin(db) issuer = (admin and admin.pocketid_issuer) or settings.pocketid_issuer client_id = (admin and admin.pocketid_client_id) or settings.pocketid_client_id client_secret = (admin and admin.pocketid_client_secret) or settings.pocketid_client_secret @@ -26,8 +48,7 @@ async def _get_pocketid_config(db: AsyncSession): async def _get_allowed_group(db: AsyncSession): """Group a PocketID user must belong to in order to sign in (None = allow all).""" - result = await db.execute(select(User).where(User.is_admin == True).limit(1)) - admin = result.scalar_one_or_none() + admin = await _config_admin(db) group = (admin and admin.pocketid_allowed_group) or settings.pocketid_allowed_group return (group or "").strip() or None