Major cleanup and refactoring of the backend (frontend not fully updated).
This commit is contained in:
50
passkey/db/__init__.py
Normal file
50
passkey/db/__init__.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
Database module for WebAuthn passkey authentication.
|
||||
|
||||
This module provides dataclasses and database abstractions for managing
|
||||
users, credentials, and sessions in a WebAuthn authentication system.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
"""User data structure."""
|
||||
|
||||
user_uuid: UUID
|
||||
user_name: str
|
||||
created_at: datetime | None = None
|
||||
last_seen: datetime | None = None
|
||||
visits: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class Credential:
|
||||
"""Credential data structure."""
|
||||
|
||||
uuid: UUID
|
||||
credential_id: bytes
|
||||
user_uuid: UUID
|
||||
aaguid: UUID
|
||||
public_key: bytes
|
||||
sign_count: int
|
||||
created_at: datetime
|
||||
last_used: datetime | None = None
|
||||
last_verified: datetime | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Session:
|
||||
"""Session data structure."""
|
||||
|
||||
key: bytes
|
||||
user_uuid: UUID
|
||||
expires: datetime
|
||||
credential_uuid: UUID | None = None
|
||||
info: dict | None = None
|
||||
|
||||
|
||||
__all__ = ["User", "Credential", "Session"]
|
||||
@@ -5,10 +5,8 @@ This module provides an async database layer using SQLAlchemy async mode
|
||||
for managing users and credentials in a WebAuthn authentication system.
|
||||
"""
|
||||
|
||||
import secrets
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import (
|
||||
@@ -25,7 +23,7 @@ from sqlalchemy.dialects.sqlite import BLOB, JSON
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||
|
||||
from ..sansio import StoredCredential
|
||||
from . import Credential, Session, User
|
||||
|
||||
DB_PATH = "sqlite+aiosqlite:///webauthn.db"
|
||||
|
||||
@@ -38,7 +36,7 @@ class Base(DeclarativeBase):
|
||||
class UserModel(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
user_id: Mapped[bytes] = mapped_column(LargeBinary(16), primary_key=True)
|
||||
user_uuid: Mapped[bytes] = mapped_column(LargeBinary(16), primary_key=True)
|
||||
user_name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
||||
last_seen: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
@@ -52,10 +50,12 @@ class UserModel(Base):
|
||||
|
||||
class CredentialModel(Base):
|
||||
__tablename__ = "credentials"
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
credential_id: Mapped[bytes] = mapped_column(LargeBinary(64), unique=True)
|
||||
user_id: Mapped[bytes] = mapped_column(
|
||||
LargeBinary(16), ForeignKey("users.user_id", ondelete="CASCADE")
|
||||
uuid: Mapped[bytes] = mapped_column(LargeBinary(16), primary_key=True)
|
||||
credential_id: Mapped[bytes] = mapped_column(
|
||||
LargeBinary(64), unique=True, index=True
|
||||
)
|
||||
user_uuid: Mapped[bytes] = mapped_column(
|
||||
LargeBinary(16), ForeignKey("users.user_uuid", ondelete="CASCADE")
|
||||
)
|
||||
aaguid: Mapped[bytes] = mapped_column(LargeBinary(16), nullable=False)
|
||||
public_key: Mapped[bytes] = mapped_column(BLOB, nullable=False)
|
||||
@@ -71,29 +71,20 @@ class CredentialModel(Base):
|
||||
class SessionModel(Base):
|
||||
__tablename__ = "sessions"
|
||||
|
||||
token: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
user_id: Mapped[bytes] = mapped_column(
|
||||
LargeBinary(16), ForeignKey("users.user_id", ondelete="CASCADE")
|
||||
key: Mapped[bytes] = mapped_column(LargeBinary(16), primary_key=True)
|
||||
user_uuid: Mapped[bytes] = mapped_column(
|
||||
LargeBinary(16), ForeignKey("users.user_uuid", ondelete="CASCADE")
|
||||
)
|
||||
credential_id: Mapped[int | None] = mapped_column(
|
||||
Integer, ForeignKey("credentials.id", ondelete="SET NULL")
|
||||
credential_uuid: Mapped[bytes | None] = mapped_column(
|
||||
LargeBinary(16), ForeignKey("credentials.uuid", ondelete="CASCADE")
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
||||
expires: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
||||
info: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
|
||||
# Relationship to user
|
||||
user: Mapped["UserModel"] = relationship("UserModel")
|
||||
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
user_id: UUID
|
||||
user_name: str
|
||||
created_at: datetime | None = None
|
||||
last_seen: datetime | None = None
|
||||
visits: int = 0
|
||||
|
||||
|
||||
# Global engine and session factory
|
||||
engine = create_async_engine(DB_PATH, echo=False)
|
||||
async_session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
||||
@@ -116,15 +107,15 @@ class DB:
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
async def get_user_by_user_id(self, user_id: UUID) -> User:
|
||||
"""Get user record by WebAuthn user ID."""
|
||||
stmt = select(UserModel).where(UserModel.user_id == user_id.bytes)
|
||||
async def get_user_by_user_uuid(self, user_uuid: UUID) -> User:
|
||||
"""Get user record by WebAuthn user UUID."""
|
||||
stmt = select(UserModel).where(UserModel.user_uuid == user_uuid.bytes)
|
||||
result = await self.session.execute(stmt)
|
||||
user_model = result.scalar_one_or_none()
|
||||
|
||||
if user_model:
|
||||
return User(
|
||||
user_id=UUID(bytes=user_model.user_id),
|
||||
user_uuid=UUID(bytes=user_model.user_uuid),
|
||||
user_name=user_model.user_name,
|
||||
created_at=user_model.created_at,
|
||||
last_seen=user_model.last_seen,
|
||||
@@ -135,7 +126,7 @@ class DB:
|
||||
async def create_user(self, user: User) -> None:
|
||||
"""Create a new user."""
|
||||
user_model = UserModel(
|
||||
user_id=user.user_id.bytes,
|
||||
user_uuid=user.user_uuid.bytes,
|
||||
user_name=user.user_name,
|
||||
created_at=user.created_at or datetime.now(),
|
||||
last_seen=user.last_seen,
|
||||
@@ -144,11 +135,12 @@ class DB:
|
||||
self.session.add(user_model)
|
||||
await self.session.flush()
|
||||
|
||||
async def create_credential(self, credential: StoredCredential) -> None:
|
||||
async def create_credential(self, credential: Credential) -> None:
|
||||
"""Store a credential for a user."""
|
||||
credential_model = CredentialModel(
|
||||
uuid=credential.uuid.bytes,
|
||||
credential_id=credential.credential_id,
|
||||
user_id=credential.user_id.bytes,
|
||||
user_uuid=credential.user_uuid.bytes,
|
||||
aaguid=credential.aaguid.bytes,
|
||||
public_key=credential.public_key,
|
||||
sign_count=credential.sign_count,
|
||||
@@ -159,7 +151,7 @@ class DB:
|
||||
self.session.add(credential_model)
|
||||
await self.session.flush()
|
||||
|
||||
async def get_credential_by_id(self, credential_id: bytes) -> StoredCredential:
|
||||
async def get_credential_by_id(self, credential_id: bytes) -> Credential:
|
||||
"""Get credential by credential ID."""
|
||||
stmt = select(CredentialModel).where(
|
||||
CredentialModel.credential_id == credential_id
|
||||
@@ -167,28 +159,29 @@ class DB:
|
||||
result = await self.session.execute(stmt)
|
||||
credential_model = result.scalar_one_or_none()
|
||||
|
||||
if credential_model:
|
||||
return StoredCredential(
|
||||
credential_id=credential_model.credential_id,
|
||||
user_id=UUID(bytes=credential_model.user_id),
|
||||
aaguid=UUID(bytes=credential_model.aaguid),
|
||||
public_key=credential_model.public_key,
|
||||
sign_count=credential_model.sign_count,
|
||||
created_at=credential_model.created_at,
|
||||
last_used=credential_model.last_used,
|
||||
last_verified=credential_model.last_verified,
|
||||
)
|
||||
raise ValueError("Credential not registered")
|
||||
if not credential_model:
|
||||
raise ValueError("Credential not registered")
|
||||
return Credential(
|
||||
uuid=UUID(bytes=credential_model.uuid),
|
||||
credential_id=credential_model.credential_id,
|
||||
user_uuid=UUID(bytes=credential_model.user_uuid),
|
||||
aaguid=UUID(bytes=credential_model.aaguid),
|
||||
public_key=credential_model.public_key,
|
||||
sign_count=credential_model.sign_count,
|
||||
created_at=credential_model.created_at,
|
||||
last_used=credential_model.last_used,
|
||||
last_verified=credential_model.last_verified,
|
||||
)
|
||||
|
||||
async def get_credentials_by_user_id(self, user_id: UUID) -> list[bytes]:
|
||||
async def get_credentials_by_user_uuid(self, user_uuid: UUID) -> list[bytes]:
|
||||
"""Get all credential IDs for a user."""
|
||||
stmt = select(CredentialModel.credential_id).where(
|
||||
CredentialModel.user_id == user_id.bytes
|
||||
CredentialModel.user_uuid == user_uuid.bytes
|
||||
)
|
||||
result = await self.session.execute(stmt)
|
||||
return [row[0] for row in result.fetchall()]
|
||||
|
||||
async def update_credential(self, credential: StoredCredential) -> None:
|
||||
async def update_credential(self, credential: Credential) -> None:
|
||||
"""Update the sign count, created_at, last_used, and last_verified for a credential."""
|
||||
stmt = (
|
||||
update(CredentialModel)
|
||||
@@ -202,7 +195,7 @@ class DB:
|
||||
)
|
||||
await self.session.execute(stmt)
|
||||
|
||||
async def login(self, user_id: UUID, credential: StoredCredential) -> None:
|
||||
async def login(self, user_uuid: UUID, credential: Credential) -> None:
|
||||
"""Update the last_seen timestamp for a user and the credential record used for logging in."""
|
||||
async with self.session.begin():
|
||||
# Update credential
|
||||
@@ -211,137 +204,77 @@ class DB:
|
||||
# Update user's last_seen and increment visits
|
||||
stmt = (
|
||||
update(UserModel)
|
||||
.where(UserModel.user_id == user_id.bytes)
|
||||
.where(UserModel.user_uuid == user_uuid.bytes)
|
||||
.values(last_seen=credential.last_used, visits=UserModel.visits + 1)
|
||||
)
|
||||
await self.session.execute(stmt)
|
||||
|
||||
async def create_new_session(
|
||||
self, user_id: UUID, credential: StoredCredential
|
||||
) -> None:
|
||||
"""Create a new session for a user by incrementing visits and updating last_seen."""
|
||||
async with self.session.begin():
|
||||
# Update credential
|
||||
await self.update_credential(credential)
|
||||
|
||||
# Update user's last_seen and increment visits
|
||||
stmt = (
|
||||
update(UserModel)
|
||||
.where(UserModel.user_id == user_id.bytes)
|
||||
.values(last_seen=credential.last_used, visits=UserModel.visits + 1)
|
||||
)
|
||||
await self.session.execute(stmt)
|
||||
|
||||
async def delete_credential(self, credential_id: bytes) -> None:
|
||||
async def delete_credential(self, uuid: UUID, user_uuid: UUID) -> None:
|
||||
"""Delete a credential by its ID."""
|
||||
stmt = delete(CredentialModel).where(
|
||||
CredentialModel.credential_id == credential_id
|
||||
stmt = (
|
||||
delete(CredentialModel)
|
||||
.where(CredentialModel.uuid == uuid.bytes)
|
||||
.where(CredentialModel.user_uuid == user_uuid.bytes)
|
||||
)
|
||||
await self.session.execute(stmt)
|
||||
await self.session.commit()
|
||||
|
||||
async def get_user_by_username(self, user_name: str) -> User | None:
|
||||
"""Get user by username."""
|
||||
stmt = select(UserModel).where(UserModel.user_name == user_name)
|
||||
result = await self.session.execute(stmt)
|
||||
user_model = result.scalar_one_or_none()
|
||||
|
||||
if user_model:
|
||||
return User(
|
||||
user_id=UUID(bytes=user_model.user_id),
|
||||
user_name=user_model.user_name,
|
||||
created_at=user_model.created_at,
|
||||
last_seen=user_model.last_seen,
|
||||
visits=user_model.visits,
|
||||
)
|
||||
return None
|
||||
|
||||
async def create_session(
|
||||
self,
|
||||
user_id: UUID,
|
||||
credential_id: int | None = None,
|
||||
token: str | None = None,
|
||||
info: dict | None = None,
|
||||
) -> str:
|
||||
"""Create a new authentication session for a user. If credential_id is None, creates a session without a specific credential."""
|
||||
if token is None:
|
||||
token = secrets.token_urlsafe(12)
|
||||
|
||||
user_uuid: UUID,
|
||||
key: bytes,
|
||||
expires: datetime,
|
||||
info: dict,
|
||||
credential_uuid: UUID | None = None,
|
||||
) -> bytes:
|
||||
"""Create a new authentication session for a user. If credential_uuid is None, creates a session without a specific credential."""
|
||||
session_model = SessionModel(
|
||||
token=token,
|
||||
user_id=user_id.bytes,
|
||||
credential_id=credential_id,
|
||||
created_at=datetime.now(),
|
||||
key=key,
|
||||
user_uuid=user_uuid.bytes,
|
||||
credential_uuid=credential_uuid.bytes if credential_uuid else None,
|
||||
expires=expires,
|
||||
info=info,
|
||||
)
|
||||
self.session.add(session_model)
|
||||
await self.session.flush()
|
||||
return token
|
||||
return key
|
||||
|
||||
async def create_session_by_credential_id(
|
||||
self,
|
||||
user_id: UUID,
|
||||
credential_id: bytes | None,
|
||||
token: str | None = None,
|
||||
info: dict | None = None,
|
||||
) -> str:
|
||||
"""Create a new authentication session for a user using WebAuthn credential ID. If credential_id is None, creates a session without a specific credential."""
|
||||
if credential_id is None:
|
||||
return await self.create_session(user_id, None, token, info)
|
||||
|
||||
# Get the database ID from the credential
|
||||
stmt = select(CredentialModel.id).where(
|
||||
CredentialModel.credential_id == credential_id
|
||||
)
|
||||
async def get_session(self, key: bytes) -> Session | None:
|
||||
"""Get session by 16-byte key."""
|
||||
stmt = select(SessionModel).where(SessionModel.key == key)
|
||||
result = await self.session.execute(stmt)
|
||||
db_credential_id = result.scalar_one()
|
||||
session_model = result.scalar_one_or_none()
|
||||
|
||||
return await self.create_session(user_id, db_credential_id, token, info)
|
||||
|
||||
async def get_session(self, token: str) -> SessionModel | None:
|
||||
"""Get session by token string."""
|
||||
stmt = select(SessionModel).where(SessionModel.token == token)
|
||||
result = await self.session.execute(stmt)
|
||||
session = result.scalar_one_or_none()
|
||||
|
||||
if session:
|
||||
# Check if session is expired (24 hours)
|
||||
expiry_time = session.created_at + timedelta(hours=24)
|
||||
if datetime.now() > expiry_time:
|
||||
# Clean up expired session
|
||||
await self.delete_session(token)
|
||||
return None
|
||||
|
||||
return session
|
||||
if session_model:
|
||||
return Session(
|
||||
key=session_model.key,
|
||||
user_uuid=UUID(bytes=session_model.user_uuid),
|
||||
credential_uuid=UUID(bytes=session_model.credential_uuid)
|
||||
if session_model.credential_uuid
|
||||
else None,
|
||||
expires=session_model.expires,
|
||||
info=session_model.info,
|
||||
)
|
||||
return None
|
||||
|
||||
async def delete_session(self, token: str) -> None:
|
||||
"""Delete a session by token."""
|
||||
stmt = delete(SessionModel).where(SessionModel.token == token)
|
||||
await self.session.execute(stmt)
|
||||
async def delete_session(self, key: bytes) -> None:
|
||||
"""Delete a session by 16-byte key."""
|
||||
await self.session.execute(delete(SessionModel).where(SessionModel.key == key))
|
||||
|
||||
async def update_session(self, key: bytes, expires: datetime, info: dict) -> None:
|
||||
"""Update session expiration time and/or info."""
|
||||
await self.session.execute(
|
||||
update(SessionModel)
|
||||
.where(SessionModel.key == key)
|
||||
.values(expires=expires, info=info)
|
||||
)
|
||||
|
||||
async def cleanup_expired_sessions(self) -> None:
|
||||
"""Remove expired sessions (older than 24 hours)."""
|
||||
expiry_time = datetime.now() - timedelta(hours=24)
|
||||
stmt = delete(SessionModel).where(SessionModel.created_at < expiry_time)
|
||||
"""Remove expired sessions."""
|
||||
current_time = datetime.now()
|
||||
stmt = delete(SessionModel).where(SessionModel.expires < current_time)
|
||||
await self.session.execute(stmt)
|
||||
|
||||
async def refresh_session(self, token: str) -> str | None:
|
||||
"""Refresh a session by updating its created_at timestamp."""
|
||||
session = await self.get_session(token)
|
||||
if not session:
|
||||
return None
|
||||
|
||||
# Delete old session
|
||||
await self.delete_session(token)
|
||||
|
||||
# Create new session with same user and credential
|
||||
return await self.create_session(
|
||||
user_id=UUID(bytes=session.user_id),
|
||||
credential_id=session.credential_id,
|
||||
info=session.info,
|
||||
)
|
||||
|
||||
|
||||
# Standalone functions that handle database connections internally
|
||||
async def init_database() -> None:
|
||||
@@ -350,7 +283,7 @@ async def init_database() -> None:
|
||||
await db.init_db()
|
||||
|
||||
|
||||
async def create_user_and_credential(user: User, credential: StoredCredential) -> None:
|
||||
async def create_user_and_credential(user: User, credential: Credential) -> None:
|
||||
"""Create a new user and their first credential in a single transaction."""
|
||||
async with connect() as db:
|
||||
await db.session.begin()
|
||||
@@ -360,97 +293,73 @@ async def create_user_and_credential(user: User, credential: StoredCredential) -
|
||||
await db.create_credential(credential)
|
||||
|
||||
|
||||
async def get_user_by_id(user_id: UUID) -> User:
|
||||
"""Get user record by WebAuthn user ID."""
|
||||
async def get_user_by_uuid(user_uuid: UUID) -> User:
|
||||
"""Get user record by WebAuthn user UUID."""
|
||||
async with connect() as db:
|
||||
return await db.get_user_by_user_id(user_id)
|
||||
return await db.get_user_by_user_uuid(user_uuid)
|
||||
|
||||
|
||||
async def create_credential_for_user(credential: StoredCredential) -> None:
|
||||
async def create_credential_for_user(credential: Credential) -> None:
|
||||
"""Store a credential for an existing user."""
|
||||
async with connect() as db:
|
||||
await db.create_credential(credential)
|
||||
|
||||
|
||||
async def get_credential_by_id(credential_id: bytes) -> StoredCredential:
|
||||
async def get_credential_by_id(credential_id: bytes) -> Credential:
|
||||
"""Get credential by credential ID."""
|
||||
async with connect() as db:
|
||||
return await db.get_credential_by_id(credential_id)
|
||||
|
||||
|
||||
async def get_user_credentials(user_id: UUID) -> list[bytes]:
|
||||
async def get_user_credentials(user_uuid: UUID) -> list[bytes]:
|
||||
"""Get all credential IDs for a user."""
|
||||
async with connect() as db:
|
||||
return await db.get_credentials_by_user_id(user_id)
|
||||
return await db.get_credentials_by_user_uuid(user_uuid)
|
||||
|
||||
|
||||
async def login_user(user_id: UUID, credential: StoredCredential) -> None:
|
||||
async def login_user(user_uuid: UUID, credential: Credential) -> None:
|
||||
"""Update the last_seen timestamp for a user and the credential record used for logging in."""
|
||||
async with connect() as db:
|
||||
await db.login(user_id, credential)
|
||||
await db.login(user_uuid, credential)
|
||||
|
||||
|
||||
async def delete_user_credential(credential_id: bytes) -> None:
|
||||
async def delete_credential(uuid: UUID, user_uuid: UUID) -> None:
|
||||
"""Delete a credential by its ID."""
|
||||
async with connect() as db:
|
||||
await db.delete_credential(credential_id)
|
||||
|
||||
|
||||
async def create_new_session(user_id: UUID, credential: StoredCredential) -> None:
|
||||
"""Create a new session for a user by incrementing visits and updating last_seen."""
|
||||
async with connect() as db:
|
||||
await db.create_new_session(user_id, credential)
|
||||
|
||||
|
||||
async def get_user_by_username(user_name: str) -> User | None:
|
||||
"""Get user by username."""
|
||||
async with connect() as db:
|
||||
return await db.get_user_by_username(user_name)
|
||||
await db.delete_credential(uuid, user_uuid)
|
||||
|
||||
|
||||
async def create_session(
|
||||
user_id: UUID,
|
||||
credential_id: int | None = None,
|
||||
token: str | None = None,
|
||||
info: dict | None = None,
|
||||
) -> str:
|
||||
"""Create a new authentication session for a user. If credential_id is None, creates a session without a specific credential."""
|
||||
user_uuid: UUID,
|
||||
key: bytes,
|
||||
expires: datetime,
|
||||
info: dict,
|
||||
credential_uuid: UUID | None = None,
|
||||
) -> bytes:
|
||||
"""Create a new authentication session for a user. If credential_uuid is None, creates a session without a specific credential."""
|
||||
async with connect() as db:
|
||||
return await db.create_session(user_id, credential_id, token, info)
|
||||
return await db.create_session(user_uuid, key, expires, info, credential_uuid)
|
||||
|
||||
|
||||
async def create_session_by_credential_id(
|
||||
user_id: UUID,
|
||||
credential_id: bytes | None,
|
||||
token: str | None = None,
|
||||
info: dict | None = None,
|
||||
) -> str:
|
||||
"""Create a new authentication session for a user using WebAuthn credential ID. If credential_id is None, creates a session without a specific credential."""
|
||||
async def get_session(key: bytes) -> Session | None:
|
||||
"""Get session by 16-byte key."""
|
||||
async with connect() as db:
|
||||
return await db.create_session_by_credential_id(
|
||||
user_id, credential_id, token, info
|
||||
)
|
||||
return await db.get_session(key)
|
||||
|
||||
|
||||
async def get_session(token: str) -> SessionModel | None:
|
||||
"""Get session by token string."""
|
||||
async def delete_session(key: bytes) -> None:
|
||||
"""Delete a session by 16-byte key."""
|
||||
async with connect() as db:
|
||||
return await db.get_session(token)
|
||||
await db.delete_session(key)
|
||||
|
||||
|
||||
async def delete_session(token: str) -> None:
|
||||
"""Delete a session by token."""
|
||||
async def update_session(key: bytes, expires: datetime, info: dict) -> None:
|
||||
"""Update session expiration time and/or info."""
|
||||
async with connect() as db:
|
||||
await db.delete_session(token)
|
||||
await db.update_session(key, expires, info)
|
||||
|
||||
|
||||
async def cleanup_expired_sessions() -> None:
|
||||
"""Remove expired sessions (older than 24 hours)."""
|
||||
"""Remove expired sessions."""
|
||||
async with connect() as db:
|
||||
await db.cleanup_expired_sessions()
|
||||
|
||||
|
||||
async def refresh_session(token: str) -> str | None:
|
||||
"""Refresh a session by updating its created_at timestamp."""
|
||||
async with connect() as db:
|
||||
return await db.refresh_session(token)
|
||||
|
||||
Reference in New Issue
Block a user