feat: add SQLiteCredentialRepository with tests

This commit is contained in:
Johan Lundberg 2026-02-13 13:44:32 +01:00
parent d941209f1e
commit bbe0dac8cb
No known key found for this signature in database
GPG key ID: A6C152738D03C7D1
2 changed files with 283 additions and 1 deletions

View file

@ -2,7 +2,7 @@ from datetime import UTC, datetime
import aiosqlite
from fastapi_oidc_op.models import User
from fastapi_oidc_op.models import PasswordCredential, User, WebAuthnCredential
from fastapi_oidc_op.store.exceptions import DuplicateError
@ -140,3 +140,101 @@ class SQLiteUserRepository:
cursor = await self._db.execute("DELETE FROM users WHERE userid = ?", (userid,))
await self._db.commit()
return cursor.rowcount > 0
class SQLiteCredentialRepository:
def __init__(self, db: aiosqlite.Connection) -> None:
self._db = db
def _row_to_webauthn(self, row: aiosqlite.Row) -> WebAuthnCredential:
return WebAuthnCredential(
user_id=row["user_id"],
credential_id=bytes(row["credential_id"]),
public_key=bytes(row["public_key"]),
sign_count=row["sign_count"],
device_name=row["device_name"],
created_at=datetime.fromisoformat(row["created_at"]),
)
def _row_to_password(self, row: aiosqlite.Row) -> PasswordCredential:
return PasswordCredential(
user_id=row["user_id"],
password_hash=row["password_hash"],
created_at=datetime.fromisoformat(row["created_at"]),
)
async def create_webauthn(self, credential: WebAuthnCredential) -> WebAuthnCredential:
try:
await self._db.execute(
"""
INSERT INTO webauthn_credentials (user_id, credential_id, public_key, sign_count, device_name, created_at)
VALUES (?, ?, ?, ?, ?, ?)
""",
(
credential.user_id,
credential.credential_id,
credential.public_key,
credential.sign_count,
credential.device_name,
credential.created_at.isoformat(),
),
)
await self._db.commit()
except aiosqlite.IntegrityError as e:
raise DuplicateError(str(e)) from e
return credential
async def create_password(self, credential: PasswordCredential) -> PasswordCredential:
try:
await self._db.execute(
"INSERT INTO password_credentials (user_id, password_hash, created_at) VALUES (?, ?, ?)",
(credential.user_id, credential.password_hash, credential.created_at.isoformat()),
)
await self._db.commit()
except aiosqlite.IntegrityError as e:
raise DuplicateError(str(e)) from e
return credential
async def get_webauthn_by_user(self, user_id: str) -> list[WebAuthnCredential]:
async with self._db.execute("SELECT * FROM webauthn_credentials WHERE user_id = ?", (user_id,)) as cursor:
rows = await cursor.fetchall()
return [self._row_to_webauthn(row) for row in rows]
async def get_webauthn_by_credential_id(self, credential_id: bytes) -> WebAuthnCredential | None:
async with self._db.execute(
"SELECT * FROM webauthn_credentials WHERE credential_id = ?", (credential_id,)
) as cursor:
row = await cursor.fetchone()
if row is None:
return None
return self._row_to_webauthn(row)
async def get_password_by_user(self, user_id: str) -> PasswordCredential | None:
async with self._db.execute("SELECT * FROM password_credentials WHERE user_id = ?", (user_id,)) as cursor:
row = await cursor.fetchone()
if row is None:
return None
return self._row_to_password(row)
async def update_webauthn(self, credential: WebAuthnCredential) -> WebAuthnCredential:
cursor = await self._db.execute(
"UPDATE webauthn_credentials SET sign_count = ?, device_name = ? WHERE user_id = ? AND credential_id = ?",
(credential.sign_count, credential.device_name, credential.user_id, credential.credential_id),
)
if cursor.rowcount == 0:
raise ValueError(f"WebAuthn credential not found for user: {credential.user_id}")
await self._db.commit()
return credential
async def delete_webauthn(self, user_id: str, credential_id: bytes) -> bool:
cursor = await self._db.execute(
"DELETE FROM webauthn_credentials WHERE user_id = ? AND credential_id = ?",
(user_id, credential_id),
)
await self._db.commit()
return cursor.rowcount > 0
async def delete_password(self, user_id: str) -> bool:
cursor = await self._db.execute("DELETE FROM password_credentials WHERE user_id = ?", (user_id,))
await self._db.commit()
return cursor.rowcount > 0