feat: add Consent model, migration, and repository
This commit is contained in:
parent
16f3e039d9
commit
9ccc6c885f
7 changed files with 200 additions and 3 deletions
|
|
@ -56,3 +56,11 @@ class MagicLink(BaseModel):
|
|||
used: bool = False
|
||||
created_by: str | None = None
|
||||
note: str | None = None
|
||||
|
||||
|
||||
class Consent(BaseModel):
|
||||
userid: str
|
||||
client_id: str
|
||||
scopes: list[str]
|
||||
created_at: datetime = Field(default_factory=_utcnow)
|
||||
updated_at: datetime = Field(default_factory=_utcnow)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from typing import Protocol, runtime_checkable
|
||||
|
||||
from porchlight.models import (
|
||||
Consent,
|
||||
MagicLink,
|
||||
PasswordCredential,
|
||||
User,
|
||||
|
|
@ -51,3 +52,14 @@ class MagicLinkRepository(Protocol):
|
|||
async def mark_used(self, token: str) -> bool: ...
|
||||
|
||||
async def delete_expired(self) -> int: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ConsentRepository(Protocol):
|
||||
async def get_consent(self, userid: str, client_id: str) -> Consent | None: ...
|
||||
|
||||
async def set_consent(self, userid: str, client_id: str, scopes: list[str]) -> None: ...
|
||||
|
||||
async def delete_consent(self, userid: str, client_id: str) -> bool: ...
|
||||
|
||||
async def list_consents(self, userid: str) -> list[Consent]: ...
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
CREATE TABLE user_consents (
|
||||
userid TEXT NOT NULL REFERENCES users(userid) ON DELETE CASCADE,
|
||||
client_id TEXT NOT NULL,
|
||||
scopes TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY (userid, client_id)
|
||||
);
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import json
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import aiosqlite
|
||||
|
||||
from porchlight.models import MagicLink, PasswordCredential, User, WebAuthnCredential
|
||||
from porchlight.models import Consent, MagicLink, PasswordCredential, User, WebAuthnCredential
|
||||
from porchlight.store.exceptions import DuplicateError
|
||||
|
||||
|
||||
|
|
@ -289,3 +290,56 @@ class SQLiteMagicLinkRepository:
|
|||
cursor = await self._db.execute("DELETE FROM magic_links WHERE expires_at < ? AND used = 0", (now,))
|
||||
await self._db.commit()
|
||||
return cursor.rowcount
|
||||
|
||||
|
||||
class SQLiteConsentRepository:
|
||||
def __init__(self, db: aiosqlite.Connection) -> None:
|
||||
self._db = db
|
||||
|
||||
def _row_to_consent(self, row: aiosqlite.Row) -> Consent:
|
||||
return Consent(
|
||||
userid=row["userid"],
|
||||
client_id=row["client_id"],
|
||||
scopes=json.loads(row["scopes"]),
|
||||
created_at=datetime.fromisoformat(row["created_at"]),
|
||||
updated_at=datetime.fromisoformat(row["updated_at"]),
|
||||
)
|
||||
|
||||
async def get_consent(self, userid: str, client_id: str) -> Consent | None:
|
||||
async with self._db.execute(
|
||||
"SELECT * FROM user_consents WHERE userid = ? AND client_id = ?",
|
||||
(userid, client_id),
|
||||
) as cursor:
|
||||
row = await cursor.fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
return self._row_to_consent(row)
|
||||
|
||||
async def set_consent(self, userid: str, client_id: str, scopes: list[str]) -> None:
|
||||
now = datetime.now(UTC).isoformat()
|
||||
await self._db.execute(
|
||||
"""
|
||||
INSERT INTO user_consents (userid, client_id, scopes, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT (userid, client_id)
|
||||
DO UPDATE SET scopes = excluded.scopes, updated_at = excluded.updated_at
|
||||
""",
|
||||
(userid, client_id, json.dumps(scopes), now, now),
|
||||
)
|
||||
await self._db.commit()
|
||||
|
||||
async def delete_consent(self, userid: str, client_id: str) -> bool:
|
||||
cursor = await self._db.execute(
|
||||
"DELETE FROM user_consents WHERE userid = ? AND client_id = ?",
|
||||
(userid, client_id),
|
||||
)
|
||||
await self._db.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
async def list_consents(self, userid: str) -> list[Consent]:
|
||||
async with self._db.execute(
|
||||
"SELECT * FROM user_consents WHERE userid = ? ORDER BY client_id",
|
||||
(userid,),
|
||||
) as cursor:
|
||||
rows = await cursor.fetchall()
|
||||
return [self._row_to_consent(row) for row in rows]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue