Inplement ApiKeys

This commit is contained in:
Sebastiaan
2025-06-05 19:36:35 +02:00
parent f8b15e3407
commit 2b865aa249
8 changed files with 312 additions and 14 deletions

View File

@@ -0,0 +1,107 @@
import random
from typing import TYPE_CHECKING
from sqlmodel import Session, Field, Relationship, select
from .base import (
RowId,
BaseSQLModel,
)
from . import mixin
from .user import User
# region # API Keys for access ###################################################
# Shared properties
class ApiKeyBase(mixin.IsActive, mixin.Name, BaseSQLModel):
user_id: RowId | None = Field(
foreign_key="user.id", nullable=False, ondelete="CASCADE"
)
# Properties to receive via API on creation
class ApiKeyCreate(ApiKeyBase):
pass
class ApiKeyGenerate(mixin.IsActive, mixin.Name, BaseSQLModel):
pass
# Properties to receive via API on creation
class ApiKeyUpdate(ApiKeyBase):
pass
# Database model, database table inferred from class name
class ApiKey(mixin.RowId, ApiKeyBase, table=True):
# --- database only items --------------------------------------------------
api_key: str = Field(unique=True, nullable=False, max_length=64)
# --- back_populates links -------------------------------------------------
user: User | None = Relationship(back_populates="api_keys")
# --- CRUD actions ---------------------------------------------------------
@staticmethod
def generate(size=30, chars="ABCDEFGHJKLMNPQRSTUVWXYZ23456789"):
return "".join(random.choice(chars) for _ in range(size))
@classmethod
def create(cls, *, session: Session, create_obj: ApiKeyCreate) -> "ApiKey":
# TODO: User id
data_obj = create_obj.model_dump(exclude_unset=True)
# Generate new api key
extra_data = {
"api_key": ApiKey.generate(),
}
while cls.authenticate(session=session, api_key=extra_data["api_key"]):
extra_data["api_key"] = ApiKey.generate()
db_obj = cls.model_validate(data_obj, update=extra_data)
session.add(db_obj)
session.commit()
session.refresh(db_obj)
return db_obj
@classmethod
def update(
cls, *, session: Session, db_obj: "ApiKey", in_obj: ApiKeyUpdate
) -> "ApiKey":
data_obj = in_obj.model_dump(exclude_unset=True)
db_obj.sqlmodel_update(data_obj)
session.add(db_obj)
session.commit()
session.refresh(db_obj)
return db_obj
@classmethod
def authenticate(cls, *, session: Session, api_key: str) -> "User | None":
statement = select(cls).where(cls.api_key == api_key and cls.is_active)
db_obj = session.exec(statement).first()
if not db_obj:
return None
if not db_obj.user:
return None
return db_obj.user
# Properties to return via API, id is always required
class ApiKeyCreatedPublic(mixin.RowIdPublic, ApiKeyBase):
api_key: str
# Properties to return via API, id is always required
class ApiKeyPublic(mixin.RowIdPublic, ApiKeyBase):
pass
class ApiKeysPublic(BaseSQLModel):
data: list[ApiKeyPublic]
count: int
# endregion

View File

@@ -4,12 +4,14 @@ from enum import auto as auto_enum
from sqlmodel import SQLModel
from uuid import UUID as RowId
__all__ = [
'RowId',
'DocumentedStrEnum',
'DocumentedIntFlag',
'auto_enum',
'BaseSQLModel',
"RowId",
"DocumentedStrEnum",
"DocumentedIntFlag",
"auto_enum",
"BaseSQLModel",
"Message",
]
# region SQLModel base class ###################################################

View File

@@ -1,10 +1,8 @@
import random
from typing import TYPE_CHECKING
from pydantic import EmailStr
from sqlmodel import Session, Field, Relationship, select
from app.core.config import settings
from app.core.security import get_password_hash, verify_password
from .base import (
@@ -16,8 +14,11 @@ from .base import (
)
from . import mixin
if TYPE_CHECKING:
from .apikey import ApiKey
# region User ##################################################################
# region # User ################################################################
class PermissionModule(DocumentedStrEnum):
@@ -39,7 +40,7 @@ class PermissionRight(DocumentedIntFlag):
ADMIN = CREATE | READ | UPDATE | DELETE
# #############################################################################
# ##############################################################################
# link to User (many-to-many)
class UserRoleLink(BaseSQLModel, table=True):
user_id: RowId | None = Field(
@@ -103,6 +104,7 @@ class User(mixin.RowId, UserBase, table=True):
hashed_password: str
# --- back_populates links -------------------------------------------------
api_keys: list["ApiKey"] = Relationship(back_populates="user", cascade_delete=True)
# --- many-to-many links ---------------------------------------------------
roles: list["Role"] = Relationship(back_populates="users", link_model=UserRoleLink)
@@ -215,7 +217,7 @@ class UsersPublic(BaseSQLModel):
# endregion
# region Password manager ######################################################
# region # Password manager ######################################################
# JSON payload containing access token
@@ -237,7 +239,7 @@ class NewPassword(BaseSQLModel):
# endregion
# region Permissions ###########################################################
# region # Permissions ###########################################################
# link to Roles (many-to-many)