from __future__ import annotations from datetime import datetime, timezone from typing import Optional from sqlmodel import Field, SQLModel def _now() -> datetime: return datetime.now(timezone.utc) #: What a token is allowed to do. "read" matches the read-only user role even #: when the owner is an admin, so a monitoring script can be handed a token that #: cannot change anything. SCOPES = ["read", "admin"] class ApiToken(SQLModel, table=True): """A long-lived bearer token for scripts and CI, owned by a user. Only a hash is stored — the token itself is shown once, when it is created, and cannot be recovered afterwards. ``prefix`` is the readable front of the token (``sp_`` plus eight characters); it identifies the row in the UI and in the audit log without being enough to authenticate with. """ id: Optional[int] = Field(default=None, primary_key=True) name: str prefix: str = Field(index=True, unique=True) token_hash: str scope: str = Field(default="read") #: The account the token acts as. Its role caps the token's scope, and a #: disabled account disables its tokens. user_id: int = Field(index=True) expires_at: Optional[datetime] = None last_used_at: Optional[datetime] = None created_at: datetime = Field(default_factory=_now) # --- API schemas --- class ApiTokenCreate(SQLModel): name: str scope: str = "read" #: Days until it expires. None means it does not. expires_in_days: Optional[int] = None class ApiTokenRead(SQLModel): id: int name: str prefix: str scope: str username: str expires_at: Optional[datetime] last_used_at: Optional[datetime] created_at: datetime expired: bool class ApiTokenCreated(ApiTokenRead): """The create response, and the only time the token itself is returned.""" token: str