diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py index b8bdd61..21b38f6 100644 --- a/backend/app/api/admin.py +++ b/backend/app/api/admin.py @@ -29,10 +29,17 @@ def _resolve_module_id(db: Session, system_key: str, module_key: str | None) -> if not system: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="system_not_found") - target_module_key = f"{system_key}.{module_key}" if module_key else f"{system_key}.__system__" + target_module_key = module_key if module_key else f"__system__{system_key}" module = modules_repo.get_by_key(target_module_key) + if module and module.system_key != system_key: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="module_system_mismatch") if not module: - module = modules_repo.create(module_key=target_module_key, name=target_module_key, status="active") + module = modules_repo.create( + module_key=target_module_key, + system_key=system_key, + name=target_module_key, + status="active", + ) return module.id diff --git a/backend/app/api/admin_catalog.py b/backend/app/api/admin_catalog.py index 8ebef7e..d597426 100644 --- a/backend/app/api/admin_catalog.py +++ b/backend/app/api/admin_catalog.py @@ -51,10 +51,17 @@ def _resolve_module_id(db: Session, system_key: str, module_key: str | None) -> system = systems_repo.get_by_key(system_key) if not system: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="system_not_found") - target_module_key = f"{system_key}.{module_key}" if module_key else f"{system_key}.__system__" + target_module_key = module_key if module_key else f"__system__{system_key}" module = modules_repo.get_by_key(target_module_key) + if module and module.system_key != system_key: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="module_system_mismatch") if not module: - module = modules_repo.create(module_key=target_module_key, name=target_module_key, status="active") + module = modules_repo.create( + module_key=target_module_key, + system_key=system_key, + name=target_module_key, + status="active", + ) return module.id @@ -77,8 +84,6 @@ def _resolve_scope_ids(db: Session, scope_type: str, scope_id: str) -> tuple[str def _split_module_key(payload_module: str | None) -> str: if not payload_module: return "__system__" - if "." in payload_module: - return payload_module.split(".", 1)[1] return payload_module @@ -164,11 +169,12 @@ def list_modules( items, total = modules_repo.list(limit=limit, offset=offset) out = [] for i in items: - system_key = i.module_key.split(".", 1)[0] if "." in i.module_key else None + if i.module_key.startswith("__system__"): + continue out.append( ModuleItem( id=i.id, - system_key=system_key, + system_key=i.system_key, module_key=i.module_key, name=i.name, status=i.status, @@ -188,14 +194,14 @@ def create_module( system = systems_repo.get_by_key(payload.system_key) if not system: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="system_not_found") - leaf_module_key = _generate_unique_key("MD", lambda k: modules_repo.get_by_key(f"{payload.system_key}.{k}")) - full_module_key = f"{payload.system_key}.{leaf_module_key}" + leaf_module_key = _generate_unique_key("MD", modules_repo.get_by_key) row = modules_repo.create( - module_key=full_module_key, + module_key=leaf_module_key, + system_key=payload.system_key, name=payload.name, status=payload.status, ) - return ModuleItem(id=row.id, system_key=payload.system_key, module_key=row.module_key, name=row.name, status=row.status) + return ModuleItem(id=row.id, system_key=row.system_key, module_key=row.module_key, name=row.name, status=row.status) @router.patch("/modules/{module_key}") @@ -210,8 +216,7 @@ def update_module( if not row: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="module_not_found") row = modules_repo.update(row, name=payload.name, status=payload.status) - system_key = row.module_key.split(".", 1)[0] if "." in row.module_key else None - return ModuleItem(id=row.id, system_key=system_key, module_key=row.module_key, name=row.name, status=row.status) + return ModuleItem(id=row.id, system_key=row.system_key, module_key=row.module_key, name=row.name, status=row.status) @router.get("/systems/{system_key}/groups") @@ -268,8 +273,7 @@ def list_module_groups( module = modules_repo.get_by_key(module_key) if not module: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="module_not_found") - system_key, module_name = module.module_key.split(".", 1) if "." in module.module_key else ("", module.module_key) - groups = groups_repo.list_module_groups(system_key, module_name) + groups = groups_repo.list_module_groups(module.system_key, module.module_key) return { "items": [ GroupRelationItem(group_key=g.group_key, group_name=g.name, status=g.status).model_dump() @@ -289,8 +293,7 @@ def list_module_members( module = modules_repo.get_by_key(module_key) if not module: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="module_not_found") - system_key, module_name = module.module_key.split(".", 1) if "." in module.module_key else ("", module.module_key) - members = groups_repo.list_module_members(system_key, module_name) + members = groups_repo.list_module_members(module.system_key, module.module_key) return { "items": [ MemberRelationItem( @@ -614,7 +617,7 @@ def list_permission_group_permissions( PermissionGroupPermissionItem( id=r.id, system=r.system, - module=("" if r.module == "__system__" else (r.module if "." in r.module else f"{r.system}.{r.module}")), + module="" if r.module == "__system__" else r.module, action=r.action, scope_type=r.scope_type, scope_id=r.scope_id, @@ -636,7 +639,14 @@ def get_permission_group_bindings( if not group: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="group_not_found") snapshot = repo.get_group_binding_snapshot(group.id, group_key) - return GroupBindingSnapshot(**snapshot) + return GroupBindingSnapshot( + group_key=snapshot["group_key"], + site_keys=snapshot["site_keys"], + system_keys=snapshot["system_keys"], + module_keys=[k.split("|", 1)[1] if "|" in k else k for k in snapshot["module_keys"]], + member_subs=snapshot["member_subs"], + actions=snapshot["actions"], + ) @router.put("/permission-groups/{group_key}/bindings", response_model=GroupBindingSnapshot) @@ -669,21 +679,32 @@ def replace_permission_group_bindings( if missing_systems: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"system_not_found:{','.join(missing_systems)}") - valid_modules = {m.module_key for m in modules_repo.list(limit=10000, offset=0)[0]} + all_modules = modules_repo.list(limit=10000, offset=0)[0] + valid_modules = {m.module_key for m in all_modules} + module_system_lookup = {m.module_key: m.system_key for m in all_modules} missing_modules = [k for k in module_keys if k not in valid_modules] if missing_modules: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"module_not_found:{','.join(missing_modules)}") + module_pairs = [f"{module_system_lookup[m]}|{m}" for m in module_keys] + repo.replace_group_bindings( group_id=group.id, site_keys=site_keys, system_keys=system_keys, - module_keys=module_keys, + module_keys=module_pairs, member_subs=payload.member_subs, actions=payload.actions, ) snapshot = repo.get_group_binding_snapshot(group.id, group_key) - return GroupBindingSnapshot(**snapshot) + return GroupBindingSnapshot( + group_key=snapshot["group_key"], + site_keys=snapshot["site_keys"], + system_keys=snapshot["system_keys"], + module_keys=[k.split("|", 1)[1] if "|" in k else k for k in snapshot["module_keys"]], + member_subs=snapshot["member_subs"], + actions=snapshot["actions"], + ) @router.post("/permission-groups", response_model=PermissionGroupItem) diff --git a/backend/app/models/module.py b/backend/app/models/module.py index cc8ca2f..d1ffe29 100644 --- a/backend/app/models/module.py +++ b/backend/app/models/module.py @@ -1,7 +1,7 @@ from datetime import datetime from uuid import uuid4 -from sqlalchemy import DateTime, String, func +from sqlalchemy import DateTime, ForeignKey, String, func from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column @@ -12,6 +12,9 @@ class Module(Base): __tablename__ = "modules" id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4())) + system_key: Mapped[str] = mapped_column( + String(128), ForeignKey("systems.system_key", ondelete="CASCADE"), nullable=False, index=True + ) module_key: Mapped[str] = mapped_column(String(128), unique=True, nullable=False, index=True) name: Mapped[str] = mapped_column(String(255), nullable=False) status: Mapped[str] = mapped_column(String(16), nullable=False, default="active") diff --git a/backend/app/repositories/modules_repo.py b/backend/app/repositories/modules_repo.py index 2c5e486..2dfb165 100644 --- a/backend/app/repositories/modules_repo.py +++ b/backend/app/repositories/modules_repo.py @@ -18,8 +18,8 @@ class ModulesRepository: stmt = stmt.order_by(Module.created_at.desc()).limit(limit).offset(offset) return list(self.db.scalars(stmt).all()), int(self.db.scalar(count_stmt) or 0) - def create(self, module_key: str, name: str, status: str = "active") -> Module: - item = Module(module_key=module_key, name=name, status=status) + def create(self, module_key: str, system_key: str, name: str, status: str = "active") -> Module: + item = Module(module_key=module_key, system_key=system_key, name=name, status=status) self.db.add(item) self.db.commit() self.db.refresh(item) diff --git a/backend/app/repositories/permission_groups_repo.py b/backend/app/repositories/permission_groups_repo.py index 289217b..099349c 100644 --- a/backend/app/repositories/permission_groups_repo.py +++ b/backend/app/repositories/permission_groups_repo.py @@ -1,7 +1,5 @@ from __future__ import annotations -from collections import defaultdict - from sqlalchemy import delete, func, select from sqlalchemy.orm import Session @@ -142,17 +140,16 @@ class PermissionGroupsRepository: normalized_actions = [a for a in list(dict.fromkeys(actions)) if a in {"view", "edit"}] normalized_member_subs = list(dict.fromkeys([s for s in member_subs if s])) - modules_by_system: dict[str, list[str]] = defaultdict(list) - for full_module_key in list(dict.fromkeys([m for m in module_keys if m])): - if "." not in full_module_key: - continue - system_key, module_name = full_module_key.split(".", 1) - if module_name == "__system__": - continue - modules_by_system[system_key].append(module_name) - normalized_systems = set([s for s in system_keys if s]) - normalized_systems.update(modules_by_system.keys()) + module_pairs = [] + for pair in module_keys: + if "|" not in pair: + continue + system_key, module_key = pair.split("|", 1) + if not system_key or not module_key: + continue + module_pairs.append((system_key, module_key)) + normalized_systems.add(system_key) self.db.execute(delete(PermissionGroupPermission).where(PermissionGroupPermission.group_id == group_id)) self.db.execute(delete(PermissionGroupMember).where(PermissionGroupMember.group_id == group_id)) @@ -163,7 +160,7 @@ class PermissionGroupsRepository: for site_key in normalized_sites: for action in normalized_actions: for system_key in sorted(normalized_systems): - module_names = modules_by_system.get(system_key) or ["__system__"] + module_names = [m for s, m in module_pairs if s == system_key] or ["__system__"] for module_name in module_names: self.db.add( PermissionGroupPermission( @@ -185,7 +182,7 @@ class PermissionGroupsRepository: actions = sorted({p.action for p in permissions if p.action in {"view", "edit"}}) module_keys = sorted( { - p.module if "." in p.module else f"{p.system}.{p.module}" + f"{p.system}|{p.module}" for p in permissions if p.module and p.module != "__system__" } diff --git a/backend/app/repositories/permissions_repo.py b/backend/app/repositories/permissions_repo.py index ce0a78c..6bcc693 100644 --- a/backend/app/repositories/permissions_repo.py +++ b/backend/app/repositories/permissions_repo.py @@ -21,6 +21,7 @@ class PermissionsRepository: UserScopePermission.scope_type, Company.company_key, Site.site_key, + Module.system_key, Module.module_key, UserScopePermission.action, ) @@ -55,13 +56,10 @@ class PermissionsRepository: if source == "group": _, scope_type, scope_id, system_key, module_key, action = row if module_key == "__system__": - module_key = f"{system_key}.__system__" - elif module_key and "." not in module_key: - module_key = f"{system_key}.{module_key}" + module_key = f"__system__{system_key}" else: - _, scope_type, company_key, site_key, module_key, action = row + _, scope_type, company_key, site_key, system_key, module_key, action = row scope_id = company_key if scope_type == "company" else site_key - system_key = module_key.split(".", 1)[0] if isinstance(module_key, str) and "." in module_key else None key = (scope_type, scope_id or "", system_key, module_key, action) if key in dedup: continue @@ -146,6 +144,7 @@ class PermissionsRepository: UserScopePermission.scope_type, Company.company_key, Site.site_key, + Module.system_key, Module.module_key, UserScopePermission.action, UserScopePermission.created_at, @@ -200,14 +199,14 @@ class PermissionsRepository: row_scope_type, company_key, site_key, + system_key, module_key, action, created_at, ) = row scope_id = company_key if row_scope_type == "company" else site_key - system_key = module_key.split(".", 1)[0] if isinstance(module_key, str) and "." in module_key else None - module_name = module_key.split(".", 1)[1] if isinstance(module_key, str) and "." in module_key else module_key - if module_name == "__system__": + module_name = module_key + if isinstance(module_name, str) and module_name.startswith("__system__"): module_name = None items.append( { diff --git a/backend/scripts/init_schema.sql b/backend/scripts/init_schema.sql index 23bae1c..3a74852 100644 --- a/backend/scripts/init_schema.sql +++ b/backend/scripts/init_schema.sql @@ -67,6 +67,7 @@ CREATE TABLE systems ( CREATE TABLE modules ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + system_key TEXT NOT NULL REFERENCES systems(system_key) ON DELETE CASCADE, module_key TEXT NOT NULL UNIQUE, name TEXT NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'active', @@ -156,6 +157,7 @@ CREATE INDEX idx_pgp_scope_site ON permission_group_permissions(scope_id); CREATE INDEX idx_api_clients_status ON api_clients(status); CREATE INDEX idx_api_clients_expires_at ON api_clients(expires_at); CREATE INDEX idx_systems_system_key ON systems(system_key); +CREATE INDEX idx_modules_system_key ON modules(system_key); CREATE INDEX idx_modules_module_key ON modules(module_key); COMMIT;