feat(roles): add admin store roles page, permission i18n, and menu integration
Some checks failed
CI / ruff (push) Successful in 9s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has started running

- Add admin store roles page with merchant→store cascading for superadmin
  and store-only selection for platform admin
- Add permission catalog API with translated labels/descriptions (en/fr/de/lb)
- Add permission translations to all 15 module locale files (60 files total)
- Add info icon tooltips for permission descriptions in role editor
- Add store roles menu item and admin menu item in module definition
- Fix store-selector.js URL construction bug when apiEndpoint has query params
- Add admin store roles API (CRUD + platform scoping)
- Add integration tests for admin store roles and permission catalog

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 23:31:27 +01:00
parent 2b55e7458b
commit f95db7c0b1
83 changed files with 3491 additions and 513 deletions

View File

@@ -476,12 +476,17 @@ class AdminService:
search: str | None = None,
is_active: bool | None = None,
is_verified: bool | None = None,
merchant_id: int | None = None,
) -> tuple[list[Store], int]:
"""Get paginated list of all stores with filtering."""
try:
# Eagerly load merchant relationship to avoid N+1 queries
query = db.query(Store).options(joinedload(Store.merchant))
# Filter by merchant
if merchant_id is not None:
query = query.filter(Store.merchant_id == merchant_id)
# Apply search filter
if search:
search_term = f"%{search}%"
@@ -501,6 +506,8 @@ class AdminService:
# Get total count (without joinedload for performance)
count_query = db.query(Store)
if merchant_id is not None:
count_query = count_query.filter(Store.merchant_id == merchant_id)
if search:
search_term = f"%{search}%"
count_query = count_query.filter(

View File

@@ -35,7 +35,7 @@ from app.modules.tenancy.exceptions import (
TeamMemberAlreadyExistsException,
UserNotFoundException,
)
from app.modules.tenancy.models import Role, Store, StoreUser, User
from app.modules.tenancy.models import Role, Store, StorePlatform, StoreUser, User
from middleware.auth import AuthManager
logger = logging.getLogger(__name__)
@@ -724,6 +724,59 @@ class StoreTeamService:
details={"role_name": role_name, "store_id": store_id},
)
# ========================================================================
# Admin Access Validation
# ========================================================================
def validate_admin_store_access(
self,
db: Session,
user_context,
store_id: int,
) -> Store:
"""
Verify an admin user can access the given store.
Super admins can access any store. Platform admins can only access
stores that belong to one of their assigned platforms.
Args:
db: Database session
user_context: UserContext of the admin user
store_id: Store ID to validate access to
Returns:
The Store object if access is granted
Raises:
InvalidRoleException: If store not found or admin lacks access
"""
store = db.query(Store).filter(Store.id == store_id).first()
if not store:
raise InvalidRoleException(f"Store {store_id} not found")
# Super admins (accessible_platform_ids is None) can access all stores
platform_ids = user_context.get_accessible_platform_ids()
if platform_ids is None:
return store
# Platform admins: store must belong to one of their platforms
store_in_platform = (
db.query(StorePlatform)
.filter(
StorePlatform.store_id == store_id,
StorePlatform.platform_id.in_(platform_ids),
StorePlatform.is_active == True,
)
.first()
)
if not store_in_platform:
raise InvalidRoleException(
"You do not have access to this store's roles"
)
return store
# Private helper methods
def _generate_invitation_token(self) -> str: