From 52e640cfd5f72abb9caa2db42369bffe55b38892 Mon Sep 17 00:00:00 2001 From: Estrella Pan Date: Fri, 10 Jul 2026 08:35:19 +0200 Subject: [PATCH] fix: satisfy auth persistence type checks --- backend/src/module/api/passkey.py | 20 +++++++++++++++----- backend/src/module/database/auth.py | 18 +++++++++++------- backend/src/module/database/user.py | 2 +- backend/src/module/ports/auth.py | 7 +++++-- 4 files changed, 32 insertions(+), 15 deletions(-) diff --git a/backend/src/module/api/passkey.py b/backend/src/module/api/passkey.py index d8690bac..dd1a0325 100644 --- a/backend/src/module/api/passkey.py +++ b/backend/src/module/api/passkey.py @@ -34,6 +34,12 @@ AuthService = Annotated[AuthenticationService, Depends(get_auth_service)] _GENERIC_ERROR = "Internal server error." +def _require_user_id(user: User) -> int: + if user.id is None: + raise RuntimeError("Persisted user has no primary key") + return user.id + + def _get_webauthn_from_request(request: Request): """ 从请求中构造 WebAuthnService @@ -91,12 +97,13 @@ async def get_registration_options( raise HTTPException(status_code=404, detail="User not found") # Get existing passkeys + user_id = _require_user_id(user) passkey_db = PasskeyDatabase(session) - existing_passkeys = await passkey_db.get_passkeys_by_user_id(user.id) + existing_passkeys = await passkey_db.get_passkeys_by_user_id(user_id) options = webauthn.generate_registration_options( username=username, - user_id=user.id, + user_id=user_id, existing_passkeys=existing_passkeys, ) @@ -201,8 +208,9 @@ async def get_passkey_login_options( passkeys = [] if user: + user_id = _require_user_id(user) passkey_db = PasskeyDatabase(session) - passkeys = await passkey_db.get_passkeys_by_user_id(user.id) + passkeys = await passkey_db.get_passkeys_by_user_id(user_id) if not user or not passkeys: # Same response whether the username doesn't exist or simply @@ -283,7 +291,7 @@ async def list_passkeys(username: str = Depends(get_current_user)): raise HTTPException(status_code=404, detail="User not found") passkey_db = PasskeyDatabase(session) - passkeys = await passkey_db.get_passkeys_by_user_id(user.id) + passkeys = await passkey_db.get_passkeys_by_user_id(_require_user_id(user)) return [passkey_db.to_list_model(pk) for pk in passkeys] @@ -311,7 +319,9 @@ async def delete_passkey( raise HTTPException(status_code=404, detail="User not found") passkey_db = PasskeyDatabase(session) - await passkey_db.delete_passkey(delete_data.passkey_id, user.id) + await passkey_db.delete_passkey( + delete_data.passkey_id, _require_user_id(user) + ) return JSONResponse( status_code=200, diff --git a/backend/src/module/database/auth.py b/backend/src/module/database/auth.py index 91b67422..fb3f3766 100644 --- a/backend/src/module/database/auth.py +++ b/backend/src/module/database/auth.py @@ -60,7 +60,7 @@ class AuthDatabase: result = await self.session.execute( select(AuthSession).where( AuthSession.token_hash == _hash_token(raw_token), - AuthSession.revoked_at.is_(None), + col(AuthSession.revoked_at).is_(None), ) ) auth_session = result.scalars().first() @@ -100,7 +100,7 @@ class AuthDatabase: result = await self.session.execute( select(AuthSession).where( AuthSession.token_hash == _hash_token(raw_token), - AuthSession.revoked_at.is_(None), + col(AuthSession.revoked_at).is_(None), ) ) auth_session = result.scalars().first() @@ -118,7 +118,7 @@ class AuthDatabase: result = await self.session.execute( select(AuthSession).where( AuthSession.user_id == user_id, - AuthSession.revoked_at.is_(None), + col(AuthSession.revoked_at).is_(None), ) ) sessions = list(result.scalars().all()) @@ -200,7 +200,7 @@ class AuthDatabase: select(ApiToken).where( ApiToken.token_hash == _hash_token(raw_token), ApiToken.scope == scope, - ApiToken.revoked_at.is_(None), + col(ApiToken.revoked_at).is_(None), ) ) token = result.scalars().first() @@ -236,8 +236,12 @@ class AuthDatabase: async def purge_user_credentials(self, user_id: int) -> None: """Remove credentials before deleting their owning user.""" await self.session.execute( - delete(AuthSession).where(AuthSession.user_id == user_id) + delete(AuthSession).where(col(AuthSession.user_id) == user_id) + ) + await self.session.execute( + delete(ApiToken).where(col(ApiToken.user_id) == user_id) + ) + await self.session.execute( + delete(Passkey).where(col(Passkey.user_id) == user_id) ) - await self.session.execute(delete(ApiToken).where(ApiToken.user_id == user_id)) - await self.session.execute(delete(Passkey).where(Passkey.user_id == user_id)) await self.session.commit() diff --git a/backend/src/module/database/user.py b/backend/src/module/database/user.py index 9fba1e82..1be7c522 100644 --- a/backend/src/module/database/user.py +++ b/backend/src/module/database/user.py @@ -65,7 +65,7 @@ class UserDatabase: async def _enabled_count(self) -> int: result = await self.session.execute( - select(func.count()).select_from(User).where(User.enabled.is_(True)) + select(func.count()).select_from(User).where(col(User.enabled).is_(True)) ) return int(result.scalar_one()) diff --git a/backend/src/module/ports/auth.py b/backend/src/module/ports/auth.py index 38b97c22..352f6f87 100644 --- a/backend/src/module/ports/auth.py +++ b/backend/src/module/ports/auth.py @@ -62,8 +62,11 @@ class SessionTokenRepository(Protocol): class AuthUnitOfWork(Protocol): - user: UserRepository - auth: SessionTokenRepository + @property + def user(self) -> UserRepository: ... + + @property + def auth(self) -> SessionTokenRepository: ... async def __aenter__(self) -> Self: ...