Release v0.1.6
This commit is contained in:
@@ -4,13 +4,16 @@ from dataclasses import dataclass
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.admin.service import ensure_default_roles, get_or_create_account
|
||||
from govoplan_core.core.optional import reraise_unless_missing_package
|
||||
from govoplan_core.db.base import Base
|
||||
from govoplan_core.db.models import Role, SystemRoleAssignment, Tenant, User, UserRoleAssignment
|
||||
from govoplan_core.db.session import get_database
|
||||
from govoplan_core.security.api_keys import CreatedApiKey, create_api_key
|
||||
from govoplan_core.core.module_management import load_startup_enabled_modules, startup_candidate_module_ids
|
||||
from govoplan_core.security.permissions import TENANT_SCOPES
|
||||
from govoplan_core.server.registry import available_module_manifests, build_platform_registry
|
||||
from govoplan_core.settings import settings
|
||||
from govoplan_access.backend.admin.service import ensure_default_roles, get_or_create_account
|
||||
from govoplan_access.backend.db.models import Role, SystemRoleAssignment, User, UserRoleAssignment
|
||||
from govoplan_access.backend.security.api_keys import CreatedApiKey, create_api_key
|
||||
from govoplan_tenancy.backend.db.models import Tenant
|
||||
|
||||
DEFAULT_SCOPES = sorted(TENANT_SCOPES)
|
||||
|
||||
@@ -22,29 +25,19 @@ class BootstrapResult:
|
||||
created_api_key: CreatedApiKey | None
|
||||
|
||||
|
||||
def _import_optional_models(module_name: str) -> None:
|
||||
try:
|
||||
__import__(module_name)
|
||||
except ModuleNotFoundError as exc:
|
||||
reraise_unless_missing_package(exc, module_name.split(".", 1)[0])
|
||||
|
||||
|
||||
def create_all_tables() -> None:
|
||||
# Import models so SQLAlchemy sees all tables from installed modules.
|
||||
from govoplan_core.db import models # noqa: F401
|
||||
from govoplan_core.access.db import models as access_models # noqa: F401
|
||||
from govoplan_core.access.db.base import AccessBase
|
||||
# Build the configured registry so enabled module manifests register their
|
||||
# model metadata with the shared SQLAlchemy base before create_all runs.
|
||||
from govoplan_core.admin import models as core_admin_models # noqa: F401
|
||||
|
||||
for module_name in (
|
||||
"govoplan_files.backend.db.models",
|
||||
"govoplan_mail.backend.db.models",
|
||||
"govoplan_campaign.backend.db.models",
|
||||
):
|
||||
_import_optional_models(module_name)
|
||||
raw_enabled_modules = load_startup_enabled_modules(settings.enabled_modules)
|
||||
candidate_modules = startup_candidate_module_ids(settings.enabled_modules, raw_enabled_modules)
|
||||
available_modules = available_module_manifests(enabled_modules=candidate_modules, ignore_load_errors=True)
|
||||
enabled_modules = load_startup_enabled_modules(settings.enabled_modules, available=available_modules)
|
||||
build_platform_registry(enabled_modules)
|
||||
|
||||
engine = get_database().engine
|
||||
Base.metadata.create_all(bind=engine)
|
||||
AccessBase.metadata.create_all(bind=engine)
|
||||
|
||||
|
||||
def bootstrap_dev_data(
|
||||
@@ -73,7 +66,7 @@ def bootstrap_dev_data(
|
||||
)
|
||||
# Keep development credentials deterministic when an old create_all database
|
||||
# is reused. This is guarded by the explicit dev bootstrap setting.
|
||||
from govoplan_core.security.passwords import hash_password
|
||||
from govoplan_access.backend.security.passwords import hash_password
|
||||
|
||||
if not account.password_hash:
|
||||
account.password_hash = hash_password(user_password)
|
||||
|
||||
@@ -10,8 +10,11 @@ from alembic.runtime.migration import MigrationContext
|
||||
from sqlalchemy import create_engine, inspect, text
|
||||
|
||||
from govoplan_core.core.migrations import MigrationMetadataPlan, migration_metadata_plan
|
||||
from govoplan_core.core.module_management import load_startup_enabled_modules, startup_candidate_module_ids
|
||||
from govoplan_core.db.session import configure_database
|
||||
from govoplan_core.server.default_config import get_server_config
|
||||
from govoplan_core.server.registry import build_platform_registry
|
||||
from govoplan_core.server.config import ManifestFactory
|
||||
from govoplan_core.server.registry import available_module_manifests, build_platform_registry
|
||||
from govoplan_core.settings import settings
|
||||
|
||||
# Historic development databases could be created partly through Alembic and
|
||||
@@ -133,11 +136,32 @@ class MigrationResult:
|
||||
current_revision: str | None
|
||||
|
||||
|
||||
def registered_module_migration_plan() -> MigrationMetadataPlan:
|
||||
def registered_module_migration_plan(
|
||||
database_url: str | None = None,
|
||||
*,
|
||||
enabled_modules: tuple[str, ...] | list[str] | None = None,
|
||||
manifest_factories: tuple[ManifestFactory, ...] = (),
|
||||
) -> MigrationMetadataPlan:
|
||||
server_config = get_server_config()
|
||||
active_database_url = database_url or settings.database_url
|
||||
if active_database_url:
|
||||
configure_database(active_database_url)
|
||||
active_manifest_factories = manifest_factories or tuple(server_config.manifest_factories)
|
||||
raw_enabled_modules = tuple(enabled_modules) if enabled_modules is not None else load_startup_enabled_modules(server_config.enabled_modules)
|
||||
candidate_modules = startup_candidate_module_ids(server_config.enabled_modules, raw_enabled_modules)
|
||||
available_modules = available_module_manifests(
|
||||
active_manifest_factories,
|
||||
enabled_modules=candidate_modules,
|
||||
ignore_load_errors=True,
|
||||
)
|
||||
active_enabled_modules = (
|
||||
tuple(enabled_modules)
|
||||
if enabled_modules is not None
|
||||
else load_startup_enabled_modules(server_config.enabled_modules, available=available_modules)
|
||||
)
|
||||
registry = build_platform_registry(
|
||||
server_config.enabled_modules,
|
||||
manifest_factories=server_config.manifest_factories,
|
||||
active_enabled_modules,
|
||||
manifest_factories=active_manifest_factories,
|
||||
)
|
||||
return migration_metadata_plan(registry)
|
||||
|
||||
@@ -146,18 +170,32 @@ def _repo_root() -> Path:
|
||||
return Path(__file__).resolve().parents[3]
|
||||
|
||||
|
||||
def alembic_config(*, database_url: str | None = None) -> Config:
|
||||
def alembic_config(
|
||||
*,
|
||||
database_url: str | None = None,
|
||||
enabled_modules: tuple[str, ...] | list[str] | None = None,
|
||||
manifest_factories: tuple[ManifestFactory, ...] = (),
|
||||
) -> Config:
|
||||
root = _repo_root()
|
||||
config = Config(str(root / "alembic.ini"))
|
||||
config.set_main_option("script_location", str(root / "alembic"))
|
||||
|
||||
plan = registered_module_migration_plan()
|
||||
active_database_url = database_url or settings.database_url
|
||||
plan = registered_module_migration_plan(
|
||||
active_database_url,
|
||||
enabled_modules=enabled_modules,
|
||||
manifest_factories=manifest_factories,
|
||||
)
|
||||
version_locations = [str(root / "alembic" / "versions")]
|
||||
version_locations.extend(location for location in plan.script_locations if location)
|
||||
config.set_main_option("version_locations", os.pathsep.join(dict.fromkeys(version_locations)))
|
||||
config.set_main_option("version_path_separator", "os")
|
||||
|
||||
config.attributes["database_url"] = database_url or settings.database_url
|
||||
config.attributes["database_url"] = active_database_url
|
||||
if enabled_modules is not None:
|
||||
config.attributes["enabled_modules"] = tuple(enabled_modules)
|
||||
if manifest_factories:
|
||||
config.attributes["manifest_factories"] = tuple(manifest_factories)
|
||||
return config
|
||||
|
||||
|
||||
@@ -166,7 +204,12 @@ def database_revision(database_url: str | None = None) -> str | None:
|
||||
engine = create_engine(url)
|
||||
try:
|
||||
with engine.connect() as connection:
|
||||
return MigrationContext.configure(connection).get_current_revision()
|
||||
heads = MigrationContext.configure(connection).get_current_heads()
|
||||
if not heads:
|
||||
return None
|
||||
if len(heads) == 1:
|
||||
return heads[0]
|
||||
return ",".join(sorted(heads))
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
@@ -227,7 +270,9 @@ def reconcile_legacy_create_all_schema(database_url: str | None = None) -> str |
|
||||
engine = create_engine(url)
|
||||
try:
|
||||
with engine.connect() as connection:
|
||||
current = MigrationContext.configure(connection).get_current_revision()
|
||||
heads = MigrationContext.configure(connection).get_current_heads()
|
||||
current = heads[0] if len(heads) == 1 else None
|
||||
has_no_revision = len(heads) == 0
|
||||
schema = inspect(connection)
|
||||
tables = set(schema.get_table_names())
|
||||
|
||||
@@ -259,10 +304,10 @@ def reconcile_legacy_create_all_schema(database_url: str | None = None) -> str |
|
||||
# run normally.
|
||||
_backfill_user_lock_state_for_create_all_schema(url)
|
||||
target = REVISION_HIERARCHICAL_SETTINGS
|
||||
elif current is None and has_create_all_hierarchical_schema:
|
||||
elif has_no_revision and has_create_all_hierarchical_schema:
|
||||
_backfill_user_lock_state_for_create_all_schema(url)
|
||||
target = REVISION_HIERARCHICAL_SETTINGS
|
||||
elif current is None and has_file_storage and has_file_folders:
|
||||
elif has_no_revision and has_file_storage and has_file_folders:
|
||||
# This is the other create_all-only development shape. The strict
|
||||
# column checks above ensure that we only stamp a complete known schema.
|
||||
target = REVISION_FILE_FOLDERS
|
||||
@@ -278,11 +323,20 @@ def migrate_database(
|
||||
*,
|
||||
database_url: str | None = None,
|
||||
reconcile_legacy_schema: bool = True,
|
||||
enabled_modules: tuple[str, ...] | list[str] | None = None,
|
||||
manifest_factories: tuple[ManifestFactory, ...] = (),
|
||||
) -> MigrationResult:
|
||||
url = database_url or settings.database_url
|
||||
previous = database_revision(url)
|
||||
reconciled = reconcile_legacy_create_all_schema(url) if reconcile_legacy_schema else None
|
||||
command.upgrade(alembic_config(database_url=url), "head")
|
||||
command.upgrade(
|
||||
alembic_config(
|
||||
database_url=url,
|
||||
enabled_modules=enabled_modules,
|
||||
manifest_factories=manifest_factories,
|
||||
),
|
||||
"heads",
|
||||
)
|
||||
current = database_revision(url)
|
||||
return MigrationResult(
|
||||
previous_revision=previous,
|
||||
|
||||
@@ -1,271 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, String, Text, UniqueConstraint, JSON, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from govoplan_core.db.base import Base, TimestampMixin
|
||||
|
||||
|
||||
def new_uuid() -> str:
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
class Account(Base, TimestampMixin):
|
||||
"""Global login identity shared by one or more tenant memberships."""
|
||||
|
||||
__tablename__ = "accounts"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
email: Mapped[str] = mapped_column(String(320), nullable=False)
|
||||
normalized_email: Mapped[str] = mapped_column(String(320), unique=True, nullable=False, index=True)
|
||||
display_name: Mapped[str | None] = mapped_column(String(255))
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
auth_provider: Mapped[str] = mapped_column(String(50), default="local", nullable=False)
|
||||
password_hash: Mapped[str | None] = mapped_column(String(500))
|
||||
password_reset_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
|
||||
memberships: Mapped[list[User]] = relationship(back_populates="account")
|
||||
auth_sessions: Mapped[list[AuthSession]] = relationship(back_populates="account", cascade="all, delete-orphan")
|
||||
system_role_assignments: Mapped[list[SystemRoleAssignment]] = relationship(
|
||||
back_populates="account", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
|
||||
class Tenant(Base, TimestampMixin):
|
||||
__tablename__ = "tenants"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
slug: Mapped[str] = mapped_column(String(100), unique=True, nullable=False, index=True)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
default_locale: Mapped[str] = mapped_column(String(20), default="en", nullable=False)
|
||||
settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
||||
allow_custom_groups: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
|
||||
allow_custom_roles: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
|
||||
allow_api_keys: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
|
||||
users: Mapped[list[User]] = relationship(back_populates="tenant", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class User(Base, TimestampMixin):
|
||||
__tablename__ = "users"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("tenant_id", "email", name="uq_users_tenant_email"),
|
||||
UniqueConstraint("tenant_id", "account_id", name="uq_users_tenant_account"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
account_id: Mapped[str] = mapped_column(ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
email: Mapped[str] = mapped_column(String(320), nullable=False, index=True)
|
||||
display_name: Mapped[str | None] = mapped_column(String(255))
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
is_tenant_admin: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
auth_provider: Mapped[str] = mapped_column(String(50), default="local", nullable=False)
|
||||
password_hash: Mapped[str | None] = mapped_column(String(500))
|
||||
last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
||||
mail_profile_policy: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
||||
|
||||
tenant: Mapped[Tenant] = relationship(back_populates="users")
|
||||
account: Mapped[Account] = relationship(back_populates="memberships")
|
||||
api_keys: Mapped[list[ApiKey]] = relationship(back_populates="user", cascade="all, delete-orphan")
|
||||
auth_sessions: Mapped[list[AuthSession]] = relationship(back_populates="user", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class Group(Base, TimestampMixin):
|
||||
__tablename__ = "groups"
|
||||
__table_args__ = (UniqueConstraint("tenant_id", "slug", name="uq_groups_tenant_slug"),)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
slug: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
system_template_id: Mapped[str | None] = mapped_column(ForeignKey("governance_templates.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
system_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
||||
mail_profile_policy: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
||||
|
||||
|
||||
class Role(Base, TimestampMixin):
|
||||
__tablename__ = "roles"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("tenant_id", "slug", name="uq_roles_tenant_slug"),
|
||||
Index(
|
||||
"uq_roles_system_slug",
|
||||
"slug",
|
||||
unique=True,
|
||||
sqlite_where=text("tenant_id IS NULL"),
|
||||
postgresql_where=text("tenant_id IS NULL"),
|
||||
),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
tenant_id: Mapped[str | None] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=True, index=True)
|
||||
slug: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
permissions: Mapped[list[str]] = mapped_column(JSON, default=list)
|
||||
is_builtin: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
is_assignable: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
system_template_id: Mapped[str | None] = mapped_column(ForeignKey("governance_templates.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
system_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
|
||||
|
||||
class SystemSettings(Base, TimestampMixin):
|
||||
__tablename__ = "system_settings"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default="global")
|
||||
default_locale: Mapped[str] = mapped_column(String(20), default="en", nullable=False)
|
||||
allow_tenant_custom_groups: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
allow_tenant_custom_roles: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
allow_tenant_api_keys: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
||||
|
||||
|
||||
class GovernanceTemplate(Base, TimestampMixin):
|
||||
__tablename__ = "governance_templates"
|
||||
__table_args__ = (UniqueConstraint("kind", "slug", name="uq_governance_templates_kind_slug"),)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
kind: Mapped[str] = mapped_column(String(20), nullable=False, index=True)
|
||||
slug: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
permissions: Mapped[list[str]] = mapped_column(JSON, default=list, nullable=False)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
|
||||
|
||||
class GovernanceTemplateAssignment(Base, TimestampMixin):
|
||||
__tablename__ = "governance_template_assignments"
|
||||
__table_args__ = (UniqueConstraint("template_id", "tenant_id", name="uq_governance_template_tenant"),)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
template_id: Mapped[str] = mapped_column(ForeignKey("governance_templates.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
mode: Mapped[str] = mapped_column(String(20), default="available", nullable=False)
|
||||
|
||||
|
||||
class SystemRoleAssignment(Base, TimestampMixin):
|
||||
__tablename__ = "system_role_assignments"
|
||||
__table_args__ = (UniqueConstraint("account_id", "role_id", name="uq_system_role_assignments"),)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
account_id: Mapped[str] = mapped_column(ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
role_id: Mapped[str] = mapped_column(ForeignKey("roles.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
|
||||
account: Mapped[Account] = relationship(back_populates="system_role_assignments")
|
||||
role: Mapped[Role] = relationship()
|
||||
|
||||
|
||||
class UserGroupMembership(Base, TimestampMixin):
|
||||
__tablename__ = "user_group_memberships"
|
||||
__table_args__ = (UniqueConstraint("tenant_id", "user_id", "group_id", name="uq_user_group_memberships"),)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
user_id: Mapped[str] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
group_id: Mapped[str] = mapped_column(ForeignKey("groups.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
|
||||
|
||||
class UserRoleAssignment(Base, TimestampMixin):
|
||||
__tablename__ = "user_role_assignments"
|
||||
__table_args__ = (UniqueConstraint("tenant_id", "user_id", "role_id", name="uq_user_role_assignments"),)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
user_id: Mapped[str] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
role_id: Mapped[str] = mapped_column(ForeignKey("roles.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
|
||||
|
||||
class GroupRoleAssignment(Base, TimestampMixin):
|
||||
__tablename__ = "group_role_assignments"
|
||||
__table_args__ = (UniqueConstraint("tenant_id", "group_id", "role_id", name="uq_group_role_assignments"),)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
group_id: Mapped[str] = mapped_column(ForeignKey("groups.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
role_id: Mapped[str] = mapped_column(ForeignKey("roles.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
|
||||
|
||||
class ApiKey(Base, TimestampMixin):
|
||||
__tablename__ = "api_keys"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
user_id: Mapped[str] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
prefix: Mapped[str] = mapped_column(String(16), nullable=False, index=True)
|
||||
key_hash: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
scopes: Mapped[list[str]] = mapped_column(JSON, default=list)
|
||||
expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
|
||||
user: Mapped[User] = relationship(back_populates="api_keys")
|
||||
|
||||
|
||||
|
||||
|
||||
class AuthSession(Base, TimestampMixin):
|
||||
__tablename__ = "auth_sessions"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
user_id: Mapped[str] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
account_id: Mapped[str] = mapped_column(ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
token_hash: Mapped[str] = mapped_column(String(128), nullable=False, unique=True, index=True)
|
||||
csrf_token_hash: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
|
||||
last_seen_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), index=True)
|
||||
user_agent: Mapped[str | None] = mapped_column(String(500))
|
||||
ip_address: Mapped[str | None] = mapped_column(String(100))
|
||||
|
||||
user: Mapped[User] = relationship(back_populates="auth_sessions")
|
||||
account: Mapped[Account] = relationship(back_populates="auth_sessions")
|
||||
|
||||
|
||||
class AuditLog(Base, TimestampMixin):
|
||||
__tablename__ = "audit_log"
|
||||
__table_args__ = (
|
||||
Index("ix_audit_log_scope_created_at", "scope", "created_at"),
|
||||
Index("ix_audit_log_tenant_scope_created_at", "tenant_id", "scope", "created_at"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
scope: Mapped[str] = mapped_column(String(20), default="tenant", nullable=False, index=True)
|
||||
tenant_id: Mapped[str | None] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=True, index=True)
|
||||
user_id: Mapped[str | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
api_key_id: Mapped[str | None] = mapped_column(ForeignKey("api_keys.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
action: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
|
||||
object_type: Mapped[str | None] = mapped_column(String(100), index=True)
|
||||
object_id: Mapped[str | None] = mapped_column(String(100), index=True)
|
||||
details: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Account",
|
||||
"ApiKey",
|
||||
"AuditLog",
|
||||
"AuthSession",
|
||||
"GovernanceTemplate",
|
||||
"GovernanceTemplateAssignment",
|
||||
"Group",
|
||||
"GroupRoleAssignment",
|
||||
"Role",
|
||||
"SystemRoleAssignment",
|
||||
"SystemSettings",
|
||||
"Tenant",
|
||||
"User",
|
||||
"UserGroupMembership",
|
||||
"UserRoleAssignment",
|
||||
]
|
||||
Reference in New Issue
Block a user