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

@@ -26,6 +26,7 @@ from .admin_modules import router as admin_modules_router
from .admin_platform_users import admin_platform_users_router
from .admin_platforms import admin_platforms_router
from .admin_store_domains import admin_store_domains_router
from .admin_store_roles import admin_store_roles_router
from .admin_stores import admin_stores_router
from .admin_users import admin_users_router
@@ -39,6 +40,7 @@ admin_router.include_router(admin_merchants_router, tags=["admin-merchants"])
admin_router.include_router(admin_platforms_router, tags=["admin-platforms"])
admin_router.include_router(admin_stores_router, tags=["admin-stores"])
admin_router.include_router(admin_store_domains_router, tags=["admin-store-domains"])
admin_router.include_router(admin_store_roles_router, tags=["admin-store-roles"])
admin_router.include_router(admin_merchant_domains_router, tags=["admin-merchant-domains"])
admin_router.include_router(admin_modules_router, tags=["admin-modules"])
admin_router.include_router(admin_module_config_router, tags=["admin-module-config"])

View File

@@ -0,0 +1,181 @@
# app/modules/tenancy/routes/api/admin_store_roles.py
"""
Admin store role management endpoints.
Allows super admins and platform admins to manage roles for any store
they have access to. Platform admins are scoped to stores within their
assigned platforms.
Endpoints:
GET /admin/store-roles — List roles for a store
GET /admin/store-roles/permissions/catalog — Permission catalog
POST /admin/store-roles — Create a role
PUT /admin/store-roles/{role_id} — Update a role
DELETE /admin/store-roles/{role_id} — Delete a role
"""
import logging
from fastapi import APIRouter, Depends, Query, Request
from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api
from app.core.database import get_db
from app.modules.tenancy.schemas.team import (
PermissionCatalogResponse,
RoleCreate,
RoleListResponse,
RoleResponse,
RoleUpdate,
)
from app.modules.tenancy.services.permission_discovery_service import (
permission_discovery_service,
)
from app.modules.tenancy.services.store_team_service import store_team_service
from app.utils.i18n import translate
from models.schema.auth import UserContext
admin_store_roles_router = APIRouter(prefix="/store-roles")
logger = logging.getLogger(__name__)
@admin_store_roles_router.get(
"/permissions/catalog", response_model=PermissionCatalogResponse
)
def admin_get_permission_catalog(
request: Request,
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Get the full permission catalog grouped by category.
Available to all admin users. Returns all permission definitions
with labels and descriptions for the role editor UI.
"""
categories = permission_discovery_service.get_permissions_by_category()
lang = current_admin.preferred_language or getattr(
request.state, "language", "en"
)
def _t(key: str) -> str:
"""Translate key, falling back to readable version."""
translated = translate(key, language=lang)
if translated == key:
parts = key.split(".")
return parts[-1].replace("_", " ").title()
return translated
return PermissionCatalogResponse(
categories=[
{
"id": cat.id,
"label": _t(cat.label_key),
"permissions": [
{
"id": p.id,
"label": _t(p.label_key),
"description": _t(p.description_key),
"is_owner_only": p.is_owner_only,
}
for p in cat.permissions
],
}
for cat in categories
]
)
@admin_store_roles_router.get("", response_model=RoleListResponse)
def admin_list_store_roles(
store_id: int = Query(..., description="Store ID to list roles for"),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
List all roles for a store.
Platform admins can only access stores within their assigned platforms.
Super admins can access any store.
"""
store_team_service.validate_admin_store_access(db, current_admin, store_id)
roles = store_team_service.get_store_roles(db=db, store_id=store_id)
db.commit() # Commit in case default roles were created
return RoleListResponse(roles=roles, total=len(roles))
@admin_store_roles_router.post("", response_model=RoleResponse, status_code=201)
def admin_create_store_role(
role_data: RoleCreate,
store_id: int = Query(..., description="Store ID to create role for"),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Create a custom role for a store.
Platform admins can only manage stores within their assigned platforms.
"""
store_team_service.validate_admin_store_access(db, current_admin, store_id)
role = store_team_service.create_custom_role(
db=db,
store_id=store_id,
name=role_data.name,
permissions=role_data.permissions,
actor_user_id=current_admin.id,
)
db.commit()
return role
@admin_store_roles_router.put("/{role_id}", response_model=RoleResponse)
def admin_update_store_role(
role_id: int,
role_data: RoleUpdate,
store_id: int = Query(..., description="Store ID"),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Update a role's name and/or permissions.
Platform admins can only manage stores within their assigned platforms.
"""
store_team_service.validate_admin_store_access(db, current_admin, store_id)
role = store_team_service.update_role(
db=db,
store_id=store_id,
role_id=role_id,
name=role_data.name,
permissions=role_data.permissions,
actor_user_id=current_admin.id,
)
db.commit()
return role
@admin_store_roles_router.delete("/{role_id}", status_code=204)
def admin_delete_store_role(
role_id: int,
store_id: int = Query(..., description="Store ID"),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Delete a custom role.
Preset roles cannot be deleted. Platform admins can only manage
stores within their assigned platforms.
"""
store_team_service.validate_admin_store_access(db, current_admin, store_id)
store_team_service.delete_role(
db=db,
store_id=store_id,
role_id=role_id,
actor_user_id=current_admin.id,
)
db.commit()

View File

@@ -86,6 +86,7 @@ def get_all_stores_admin(
search: str | None = Query(None, description="Search by name or store code"),
is_active: bool | None = Query(None),
is_verified: bool | None = Query(None),
merchant_id: int | None = Query(None, description="Filter by merchant ID"),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
@@ -97,6 +98,7 @@ def get_all_stores_admin(
search=search,
is_active=is_active,
is_verified=is_verified,
merchant_id=merchant_id,
)
return StoreListResponse(stores=stores, total=total, skip=skip, limit=limit)

View File

@@ -28,6 +28,7 @@ from app.modules.tenancy.schemas.team import (
InvitationAccept,
InvitationAcceptResponse,
InvitationResponse,
PermissionCatalogResponse,
RoleCreate,
RoleListResponse,
RoleResponse,
@@ -42,7 +43,11 @@ from app.modules.tenancy.schemas.team import (
# Permission IDs are now defined in module definition.py files
# and discovered by PermissionDiscoveryService
from app.modules.tenancy.services.permission_discovery_service import (
permission_discovery_service,
)
from app.modules.tenancy.services.store_team_service import store_team_service
from app.utils.i18n import translate
from models.schema.auth import UserContext
store_team_router = APIRouter(prefix="/team")
@@ -480,6 +485,57 @@ def delete_role(
# ============================================================================
@store_team_router.get(
"/permissions/catalog", response_model=PermissionCatalogResponse
)
def get_permission_catalog(
request: Request,
current_user: UserContext = Depends(
require_store_permission("team.view")
),
):
"""
Get the full permission catalog grouped by category.
**Required Permission:** `team.view`
Returns all available permissions with labels and descriptions,
grouped by category. Used by the role editor UI for displaying
permission checkboxes with human-readable names and tooltips.
"""
categories = permission_discovery_service.get_permissions_by_category()
lang = current_user.preferred_language or getattr(
request.state, "language", "en"
)
def _t(key: str) -> str:
"""Translate key, falling back to readable version."""
translated = translate(key, language=lang)
if translated == key:
parts = key.split(".")
return parts[-1].replace("_", " ").title()
return translated
return PermissionCatalogResponse(
categories=[
{
"id": cat.id,
"label": _t(cat.label_key),
"permissions": [
{
"id": p.id,
"label": _t(p.label_key),
"description": _t(p.description_key),
"is_owner_only": p.is_owner_only,
}
for p in cat.permissions
],
}
for cat in categories
]
)
@store_team_router.get("/me/permissions", response_model=UserPermissionsResponse)
def get_my_permissions(
request: Request,