5123c85397
Two tables (sessions, user_tokens) + migration; only token *hashes* are stored, so a DB leak yields no usable credential. Argon2id password hashing and token primitives in app/core/security. Config and .env.example gain session/cookie/token TTLs, app base URL, and SMTP settings (twelve-factor). Migration verified reversible (drops the token_purpose enum) and matches the models. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Paul <justin@jpaul.me>
44 lines
1.7 KiB
Python
44 lines
1.7 KiB
Python
"""Authentication state: opaque backend-issued sessions and single-use email
|
|
tokens. Only token *hashes* are stored (see app.core.security).
|
|
"""
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import DateTime, ForeignKey, String, func
|
|
from sqlalchemy import Enum as SAEnum
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from app.models.base import Base
|
|
from app.models.enums import TokenPurpose
|
|
from app.models.mixins import UUIDPrimaryKey
|
|
|
|
|
|
class Session(Base, UUIDPrimaryKey):
|
|
__tablename__ = "sessions"
|
|
|
|
user_id: Mapped[uuid.UUID] = mapped_column(
|
|
ForeignKey("users.id", ondelete="CASCADE"), index=True
|
|
)
|
|
token_hash: Mapped[str] = mapped_column(String(64), unique=True, index=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
)
|
|
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
|
|
|
|
class UserToken(Base, UUIDPrimaryKey):
|
|
__tablename__ = "user_tokens"
|
|
|
|
user_id: Mapped[uuid.UUID] = mapped_column(
|
|
ForeignKey("users.id", ondelete="CASCADE"), index=True
|
|
)
|
|
purpose: Mapped[TokenPurpose] = mapped_column(SAEnum(TokenPurpose, name="token_purpose"))
|
|
token_hash: Mapped[str] = mapped_column(String(64), unique=True, index=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
)
|
|
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|