feat: magic-link auth with access requests and RBAC

This commit is contained in:
Jonas Miederer 2026-09-11 17:27:58 +02:00
parent c2125f063b
commit c8280f7078
13 changed files with 1058 additions and 2 deletions

View file

@ -0,0 +1,82 @@
"""auth tables (users, access requests, magic links, sessions)
Revision ID: a3f1c9e2b4d7
Revises: d932b6993f9a
Create Date: 2026-09-11 15:30:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import sqlmodel # noqa: F401 - autogenerate renders sqlmodel.* column types
# revision identifiers, used by Alembic.
revision: str = 'a3f1c9e2b4d7'
down_revision: Union[str, Sequence[str], None] = 'd932b6993f9a'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
op.create_table('users',
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('email', sqlmodel.sql.sqltypes.AutoString(length=320), nullable=False),
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(length=200), nullable=False),
sa.Column('role', sqlmodel.sql.sqltypes.AutoString(length=20), nullable=False),
sa.Column('tenant_id', sqlmodel.sql.sqltypes.AutoString(length=200), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
op.create_table('access_requests',
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(length=200), nullable=False),
sa.Column('email', sqlmodel.sql.sqltypes.AutoString(length=320), nullable=False),
sa.Column('reason', sqlmodel.sql.sqltypes.AutoString(length=2000), nullable=False),
sa.Column('status', sqlmodel.sql.sqltypes.AutoString(length=20), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('decided_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('decided_by', sqlmodel.sql.sqltypes.AutoString(length=320), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_access_requests_email'), 'access_requests', ['email'], unique=False)
op.create_index(op.f('ix_access_requests_status'), 'access_requests', ['status'], unique=False)
op.create_table('magic_link_tokens',
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('user_id', sa.Uuid(), nullable=False),
sa.Column('token_hash', sqlmodel.sql.sqltypes.AutoString(length=64), nullable=False),
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('used_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_magic_link_tokens_token_hash'), 'magic_link_tokens', ['token_hash'], unique=True)
op.create_index(op.f('ix_magic_link_tokens_user_id'), 'magic_link_tokens', ['user_id'], unique=False)
op.create_table('user_sessions',
sa.Column('id', sqlmodel.sql.sqltypes.AutoString(length=128), nullable=False),
sa.Column('user_id', sa.Uuid(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('revoked_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_user_sessions_user_id'), 'user_sessions', ['user_id'], unique=False)
def downgrade() -> None:
"""Downgrade schema."""
op.drop_index(op.f('ix_user_sessions_user_id'), table_name='user_sessions')
op.drop_table('user_sessions')
op.drop_index(op.f('ix_magic_link_tokens_user_id'), table_name='magic_link_tokens')
op.drop_index(op.f('ix_magic_link_tokens_token_hash'), table_name='magic_link_tokens')
op.drop_table('magic_link_tokens')
op.drop_index(op.f('ix_access_requests_status'), table_name='access_requests')
op.drop_index(op.f('ix_access_requests_email'), table_name='access_requests')
op.drop_table('access_requests')
op.drop_index(op.f('ix_users_email'), table_name='users')
op.drop_table('users')