feat(roles): add admin store roles page, permission i18n, and menu integration
Some checks failed
Some checks failed
- 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:
@@ -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"])
|
||||
|
||||
181
app/modules/tenancy/routes/api/admin_store_roles.py
Normal file
181
app/modules/tenancy/routes/api/admin_store_roles.py
Normal 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()
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user