feat: implement full admin panel with dashboard, user management, and platform settings
Adds complete super_admin panel with 9 pages and account owner categories page. Backend includes 5 new DB tables, ~25 API endpoints, settings manager with in-memory cache, and 29 integration tests. Frontend includes reusable admin components (DataTable, Pagination, ActionMenu, etc.) with code-split lazy loading. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
102
backend/alembic/versions/026_add_admin_panel_tables.py
Normal file
102
backend/alembic/versions/026_add_admin_panel_tables.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""add admin panel tables
|
||||
|
||||
Revision ID: 026
|
||||
Revises: 025
|
||||
Create Date: 2026-02-08
|
||||
|
||||
Creates tables for admin panel:
|
||||
- account_limit_overrides: Per-account plan limit overrides
|
||||
- feature_flags: Feature flag definitions
|
||||
- plan_feature_defaults: Which features each plan gets
|
||||
- account_feature_overrides: Per-account feature exceptions
|
||||
- platform_settings: Runtime configuration storage
|
||||
"""
|
||||
|
||||
revision = "026"
|
||||
down_revision = "025"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Account limit overrides
|
||||
op.create_table(
|
||||
"account_limit_overrides",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||
sa.Column("account_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("accounts.id", ondelete="CASCADE"), unique=True, nullable=False),
|
||||
sa.Column("override_max_trees", sa.Integer(), nullable=True),
|
||||
sa.Column("override_max_sessions_per_month", sa.Integer(), nullable=True),
|
||||
sa.Column("override_max_users", sa.Integer(), nullable=True),
|
||||
sa.Column("note", sa.Text(), nullable=True),
|
||||
sa.Column("created_by_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id"), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
)
|
||||
op.create_index("ix_account_limit_overrides_account_id", "account_limit_overrides", ["account_id"])
|
||||
|
||||
# Feature flags
|
||||
op.create_table(
|
||||
"feature_flags",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||
sa.Column("flag_key", sa.String(100), unique=True, nullable=False),
|
||||
sa.Column("display_name", sa.String(255), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
)
|
||||
|
||||
# Plan feature defaults
|
||||
op.create_table(
|
||||
"plan_feature_defaults",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||
sa.Column("plan", sa.String(50), sa.ForeignKey("plan_limits.plan"), nullable=False),
|
||||
sa.Column("flag_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("feature_flags.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("enabled", sa.Boolean(), server_default=sa.text("false"), nullable=False),
|
||||
sa.UniqueConstraint("plan", "flag_id", name="uq_plan_feature_defaults_plan_flag"),
|
||||
)
|
||||
op.create_index("ix_plan_feature_defaults_plan", "plan_feature_defaults", ["plan"])
|
||||
|
||||
# Account feature overrides
|
||||
op.create_table(
|
||||
"account_feature_overrides",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||
sa.Column("account_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("flag_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("feature_flags.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("enabled", sa.Boolean(), nullable=False),
|
||||
sa.Column("note", sa.Text(), nullable=True),
|
||||
sa.Column("created_by_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id"), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.UniqueConstraint("account_id", "flag_id", name="uq_account_feature_overrides_account_flag"),
|
||||
)
|
||||
op.create_index("ix_account_feature_overrides_account_id", "account_feature_overrides", ["account_id"])
|
||||
|
||||
# Platform settings
|
||||
op.create_table(
|
||||
"platform_settings",
|
||||
sa.Column("setting_key", sa.String(100), primary_key=True),
|
||||
sa.Column("setting_value", sa.Text(), nullable=True),
|
||||
sa.Column("data_type", sa.String(20), nullable=False, server_default="string"),
|
||||
sa.Column("is_sensitive", sa.Boolean(), server_default=sa.text("false"), nullable=False),
|
||||
sa.Column("updated_by_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id"), nullable=True),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
)
|
||||
|
||||
# Seed default platform settings
|
||||
op.execute(
|
||||
"INSERT INTO platform_settings (setting_key, setting_value, data_type) VALUES "
|
||||
"('maintenance_mode', 'false', 'boolean'), "
|
||||
"('maintenance_message', 'We''re performing scheduled maintenance. We''ll be back soon!', 'string')"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("platform_settings")
|
||||
op.drop_table("account_feature_overrides")
|
||||
op.drop_table("plan_feature_defaults")
|
||||
op.drop_table("feature_flags")
|
||||
op.drop_table("account_limit_overrides")
|
||||
Reference in New Issue
Block a user