"""email+password auth foundations Revision ID: 0022_password_auth Revises: 0021_system_config Create Date: 2026-06-19 Adds password-auth columns to users (password_hash, email_verified, is_active), makes google_sub nullable (password-only accounts have no Google sub until they link), and a single-use auth_tokens table for email verification + password reset. Existing rows are all Google/demo accounts → backfilled email_verified=true (is_active defaults true). """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op revision: str = "0022_password_auth" down_revision: Union[str, None] = "0021_system_config" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: op.add_column("users", sa.Column("password_hash", sa.String(length=255), nullable=True)) op.add_column( "users", sa.Column("email_verified", sa.Boolean(), nullable=False, server_default="false"), ) op.add_column( "users", sa.Column("is_active", sa.Boolean(), nullable=False, server_default="true"), ) # Existing accounts are all Google/demo — their email is already proven. op.execute("UPDATE users SET email_verified = true") # Password-only accounts have no Google sub until they link one. op.alter_column("users", "google_sub", existing_type=sa.String(length=64), nullable=True) op.create_table( "auth_tokens", sa.Column("id", sa.Integer(), primary_key=True), sa.Column( "user_id", sa.Integer(), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True, ), sa.Column("kind", sa.String(length=16), nullable=False), sa.Column("token_hash", sa.String(length=64), nullable=False, unique=True), sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False), sa.Column("used_at", sa.DateTime(timezone=True), nullable=True), sa.Column( "created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False ), ) def downgrade() -> None: op.drop_table("auth_tokens") op.alter_column("users", "google_sub", existing_type=sa.String(length=64), nullable=False) op.drop_column("users", "is_active") op.drop_column("users", "email_verified") op.drop_column("users", "password_hash")