1fedd683e0
Stand: agent-06 (Audit-Log), agent-05 (Krankmeldung), agent-07 Phase 1 (Personalnummer), Busylight-Pull-Integration, TOTP/2FA, Abwesenheiten, Zeiterfassung, Kiosk-Grundgerüst. Migrations 0001–0023 deployed auf 192.168.1.137 + .164. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
130 lines
6.4 KiB
Python
130 lines
6.4 KiB
Python
"""initial schema - auth tables
|
||
|
||
Revision ID: 0001_initial
|
||
Revises:
|
||
Create Date: 2026-03-25
|
||
"""
|
||
from alembic import op
|
||
import sqlalchemy as sa
|
||
from sqlalchemy.dialects import postgresql
|
||
|
||
revision = "0001_initial"
|
||
down_revision = None
|
||
branch_labels = None
|
||
depends_on = None
|
||
|
||
|
||
def upgrade() -> None:
|
||
# ── companies ─────────────────────────────────────────────────────────────
|
||
op.create_table(
|
||
"companies",
|
||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||
sa.Column("name", sa.String(255), nullable=False),
|
||
sa.Column("slug", sa.String(100), nullable=False, unique=True),
|
||
sa.Column("plan", sa.String(50), server_default="trial"),
|
||
sa.Column("logo_url", sa.Text),
|
||
sa.Column("country", sa.String(10), server_default="DE"),
|
||
sa.Column("state", sa.String(10)),
|
||
sa.Column("settings", postgresql.JSONB, server_default="{}"),
|
||
)
|
||
|
||
# ── departments (before users – FK target) ─────────────────────────────────
|
||
op.create_table(
|
||
"departments",
|
||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||
sa.Column("company_id", postgresql.UUID(as_uuid=True),
|
||
sa.ForeignKey("companies.id", ondelete="CASCADE"), nullable=False),
|
||
sa.Column("name", sa.String(255), nullable=False),
|
||
sa.Column("manager_id", postgresql.UUID(as_uuid=True)), # FK added after users
|
||
)
|
||
|
||
# ── users ─────────────────────────────────────────────────────────────────
|
||
op.create_table(
|
||
"users",
|
||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||
sa.Column("company_id", postgresql.UUID(as_uuid=True),
|
||
sa.ForeignKey("companies.id", ondelete="CASCADE")),
|
||
sa.Column("department_id", postgresql.UUID(as_uuid=True),
|
||
sa.ForeignKey("departments.id", ondelete="SET NULL")),
|
||
sa.Column("email", sa.String(255), nullable=False, unique=True),
|
||
sa.Column("password_hash", sa.Text, nullable=False),
|
||
sa.Column("first_name", sa.String(100), nullable=False),
|
||
sa.Column("last_name", sa.String(100), nullable=False),
|
||
sa.Column("role", sa.Enum(
|
||
"SUPER_ADMIN", "COMPANY_ADMIN", "HR", "MANAGER", "EMPLOYEE",
|
||
name="userrole",
|
||
), nullable=False),
|
||
sa.Column("kiosk_pin_hash", sa.Text),
|
||
sa.Column("kiosk_qr_token", sa.Text, unique=True),
|
||
sa.Column("is_active", sa.Boolean, server_default="true"),
|
||
sa.Column("invite_token_hash", sa.Text),
|
||
sa.Column("invite_expires", sa.DateTime(timezone=True)),
|
||
sa.Column("last_login", sa.DateTime(timezone=True)),
|
||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||
)
|
||
op.create_index("ix_users_email", "users", ["email"])
|
||
|
||
# Now add FK for departments.manager_id → users
|
||
op.create_foreign_key(
|
||
"fk_departments_manager", "departments", "users", ["manager_id"], ["id"],
|
||
ondelete="SET NULL",
|
||
)
|
||
|
||
# ── sessions ──────────────────────────────────────────────────────────────
|
||
op.create_table(
|
||
"sessions",
|
||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||
sa.Column("user_id", postgresql.UUID(as_uuid=True),
|
||
sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False),
|
||
sa.Column("refresh_token_hash", sa.Text, nullable=False, unique=True),
|
||
sa.Column("device", sa.String(255)),
|
||
sa.Column("ip", sa.String(45)),
|
||
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
|
||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||
)
|
||
op.create_index("ix_sessions_user_id", "sessions", ["user_id"])
|
||
|
||
# ── password_resets ───────────────────────────────────────────────────────
|
||
op.create_table(
|
||
"password_resets",
|
||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||
sa.Column("user_id", postgresql.UUID(as_uuid=True),
|
||
sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False),
|
||
sa.Column("token_hash", sa.Text, nullable=False, unique=True),
|
||
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
|
||
sa.Column("used_at", sa.DateTime(timezone=True)),
|
||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||
)
|
||
op.create_index("ix_password_resets_user_id", "password_resets", ["user_id"])
|
||
|
||
# ── audit_logs ────────────────────────────────────────────────────────────
|
||
op.create_table(
|
||
"audit_logs",
|
||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||
sa.Column("company_id", postgresql.UUID(as_uuid=True),
|
||
sa.ForeignKey("companies.id", ondelete="SET NULL")),
|
||
sa.Column("user_id", postgresql.UUID(as_uuid=True),
|
||
sa.ForeignKey("users.id", ondelete="SET NULL")),
|
||
sa.Column("action", sa.String(100), nullable=False),
|
||
sa.Column("entity_type", sa.String(100)),
|
||
sa.Column("entity_id", postgresql.UUID(as_uuid=True)),
|
||
sa.Column("old_value", postgresql.JSONB),
|
||
sa.Column("new_value", postgresql.JSONB),
|
||
sa.Column("ip", sa.String(45)),
|
||
sa.Column("created_at", sa.DateTime(timezone=True),
|
||
server_default=sa.func.now(), index=True),
|
||
)
|
||
op.create_index("ix_audit_logs_company_id", "audit_logs", ["company_id"])
|
||
op.create_index("ix_audit_logs_user_id", "audit_logs", ["user_id"])
|
||
|
||
|
||
def downgrade() -> None:
|
||
op.drop_table("audit_logs")
|
||
op.drop_table("password_resets")
|
||
op.drop_table("sessions")
|
||
op.drop_constraint("fk_departments_manager", "departments", type_="foreignkey")
|
||
op.drop_table("users")
|
||
op.execute("DROP TYPE IF EXISTS userrole")
|
||
op.drop_table("departments")
|
||
op.drop_table("companies")
|