Files
ss-tools/backend/alembic/versions/8e9f0a1b2c3d_add_session_activity_table.py

76 lines
3.4 KiB
Python

# #region Alembic.AddSessionActivityTable [C:2] [TYPE Module] [SEMANTICS alembic,migration,session,activity,auth]
# @defgroup Alembic Create the session_activity table for session timeout enforcement.
# @PRE Previous migration (f7a8b9c0d1e2) has been applied.
# @POST session_activity table is created when users exists; otherwise ORM create_all()
# creates it after the fresh Alembic upgrade.
# @SIDE_EFFECT DDL execution — creates table, index, foreign key constraint.
# @RELATION DEPENDS_ON -> [Models.Auth.SessionActivity]
# @RELATION DEPENDS_ON -> [Models.Auth.User]
# @RATIONALE users is ORM-owned and is created by Base.metadata.create_all() after
# Alembic during a fresh install, so the FK migration must be safely skipped first.
# @REJECTED Unconditionally creating the FK table was rejected — fresh installs fail
# before application startup because users does not yet exist.
"""add session_activity table
Revision ID: 8e9f0a1b2c3d
Revises: f7a8b9c0d1e2
Create Date: 2026-07-23 07:45:00.000000
"""
from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa
from sqlalchemy import inspect
# revision identifiers, used by Alembic.
revision: str = "8e9f0a1b2c3d"
down_revision: str | Sequence[str] | None = "f7a8b9c0d1e2"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
# #region Alembic.AddSessionActivityTable.TableExists [C:1] [TYPE Function] [SEMANTICS alembic,helper,table]
# @BRIEF Check if a table already exists in the database.
def _table_exists(table_name: str) -> bool:
conn = op.get_bind()
inspector = inspect(conn)
return table_name in inspector.get_table_names()
# #endregion Alembic.AddSessionActivityTable.TableExists
# #region Alembic.AddSessionActivityTable.Upgrade [C:2] [TYPE Function] [SEMANTICS alembic,upgrade]
# @BRIEF Create session_activity table for idle/absolute timeout enforcement.
# @PRE auth.users table exists in the database.
# @POST session_activity table created with jti PK, user_id FK->users.id, indexes.
# @SIDE_EFFECT Executes CREATE TABLE DDL.
def upgrade() -> None:
"""Create session_activity table for idle/absolute timeout enforcement."""
if _table_exists("session_activity") or not _table_exists("users"):
return
op.create_table(
"session_activity",
sa.Column("jti", sa.String(), nullable=False),
sa.Column("user_id", sa.String(), nullable=False, index=True),
sa.Column("issued_at", sa.DateTime(), nullable=False),
sa.Column("expires_at", sa.DateTime(), nullable=False),
sa.Column("last_activity_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
sa.Column("ip_address", sa.String(), nullable=True),
sa.Column("user_agent", sa.String(), nullable=True),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("jti"),
)
# #endregion Alembic.AddSessionActivityTable.Upgrade
# #region Alembic.AddSessionActivityTable.Downgrade [C:1] [TYPE Function] [SEMANTICS alembic,downgrade]
# @BRIEF Drop session_activity table, reverting the upgrade.
# @POST session_activity table dropped.
# @SIDE_EFFECT Executes DROP TABLE DDL.
def downgrade() -> None:
"""Drop session_activity table."""
op.drop_table("session_activity")
# #endregion Alembic.AddSessionActivityTable.Downgrade
# #endregion Alembic.AddSessionActivityTable