"""Refresh tokens, so an app can stay signed in without keeping a password An access token here is a signed statement good for a day, and nothing consults a table before believing it — so it cannot be withdrawn, and a client that needs to survive longer than a day has only the password to fall back on. A phone must never keep that. A refresh token is a row instead of a signature: listable, withdrawable, and stored as a hash so a leaked database does not hand over live sessions. Rotated on every use, with the whole family withdrawn if a spent one comes back. Revision ID: p5f6a7b8c9d0 Revises: n4e5f6a7b8c9 """ import sqlalchemy as sa from alembic import op revision = "p5f6a7b8c9d0" down_revision = "n4e5f6a7b8c9" branch_labels = None depends_on = None def upgrade() -> None: if "refresh_tokens" in sa.inspect(op.get_bind()).get_table_names(): return op.create_table( "refresh_tokens", sa.Column("id", sa.Integer(), primary_key=True), sa.Column("user_id", sa.Integer(), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False), sa.Column("token_hash", sa.String(length=64), nullable=False), sa.Column("family", sa.String(length=32), nullable=False), sa.Column("label", sa.String(length=120)), sa.Column("created_at", sa.DateTime(), server_default=sa.func.now()), sa.Column("expires_at", sa.DateTime(), nullable=False), sa.Column("used_at", sa.DateTime()), sa.Column("revoked_at", sa.DateTime()), sa.Column("last_ip", sa.String(length=64)), ) # Unique, because presenting a token is a lookup by its hash and two rows # with the same hash would be two answers to one question. op.create_index("ix_refresh_tokens_token_hash", "refresh_tokens", ["token_hash"], unique=True) op.create_index("ix_refresh_tokens_user_id", "refresh_tokens", ["user_id"]) op.create_index("ix_refresh_tokens_family", "refresh_tokens", ["family"]) def downgrade() -> None: op.drop_table("refresh_tokens")