refactor: implement module-driven permissions and relocate business logic
File Relocations: - Delete app/config/ folder (empty after menu_registry removal) - Move feature_gate.py → app/modules/billing/dependencies/ - Move theme_presets.py → app/modules/cms/services/ Module-Driven Permissions System: - Add PermissionDefinition dataclass to app/modules/base.py - Create PermissionDiscoveryService in tenancy module - Update module definitions to declare their own permissions: - core: dashboard.view, settings.* - catalog: products.* - orders: orders.* - inventory: stock.* - customers: customers.* - tenancy: team.* - Update app/core/permissions.py to use discovery service - Role presets (owner, manager, staff, etc.) now use module permissions This follows the same pattern as module-driven menus: - Each module defines its permissions in definition.py - PermissionDiscoveryService aggregates all permissions at runtime - Tenancy module handles role-to-permission assignment Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@ from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api, get_db, require_module_access
|
||||
from app.core.feature_gate import RequireFeature
|
||||
from app.modules.billing.dependencies.feature_gate import RequireFeature
|
||||
from app.modules.analytics.services import stats_service
|
||||
from app.modules.analytics.schemas import (
|
||||
VendorAnalyticsCatalog,
|
||||
|
||||
@@ -134,6 +134,48 @@ class MenuSectionDefinition:
|
||||
is_collapsible: bool = True
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Permission Definitions
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class PermissionDefinition:
|
||||
"""
|
||||
Definition of a permission that a module exposes.
|
||||
|
||||
Permissions are granular capabilities that can be assigned to roles/users.
|
||||
Each module defines its own permissions, which are then discovered and
|
||||
aggregated by the tenancy module for role assignment.
|
||||
|
||||
Attributes:
|
||||
id: Unique identifier in format "resource.action" (e.g., "products.view")
|
||||
label_key: i18n key for the permission label
|
||||
description_key: i18n key for permission description
|
||||
category: Grouping category for UI organization (e.g., "products", "orders")
|
||||
is_owner_only: If True, only vendor owners can have this permission
|
||||
|
||||
Example:
|
||||
PermissionDefinition(
|
||||
id="products.view",
|
||||
label_key="catalog.permissions.products_view",
|
||||
description_key="catalog.permissions.products_view_desc",
|
||||
category="products",
|
||||
)
|
||||
"""
|
||||
|
||||
id: str
|
||||
label_key: str
|
||||
description_key: str = ""
|
||||
category: str = "general"
|
||||
is_owner_only: bool = False
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Scheduled Task Definitions
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScheduledTask:
|
||||
"""
|
||||
@@ -275,7 +317,7 @@ class ModuleDefinition:
|
||||
# =========================================================================
|
||||
features: list[str] = field(default_factory=list)
|
||||
menu_items: dict[FrontendType, list[str]] = field(default_factory=dict)
|
||||
permissions: list[str] = field(default_factory=list)
|
||||
permissions: list[PermissionDefinition] = field(default_factory=list)
|
||||
|
||||
# =========================================================================
|
||||
# Menu Definitions (Module-Driven Menus)
|
||||
@@ -400,9 +442,13 @@ class ModuleDefinition:
|
||||
"""Check if this module provides a specific feature."""
|
||||
return feature_code in self.features
|
||||
|
||||
def has_permission(self, permission_code: str) -> bool:
|
||||
def has_permission(self, permission_id: str) -> bool:
|
||||
"""Check if this module defines a specific permission."""
|
||||
return permission_code in self.permissions
|
||||
return any(p.id == permission_id for p in self.permissions)
|
||||
|
||||
def get_permission_ids(self) -> set[str]:
|
||||
"""Get all permission IDs defined by this module."""
|
||||
return {p.id for p in self.permissions}
|
||||
|
||||
# =========================================================================
|
||||
# Dependency Methods
|
||||
|
||||
14
app/modules/billing/dependencies/__init__.py
Normal file
14
app/modules/billing/dependencies/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# app/modules/billing/dependencies/__init__.py
|
||||
"""FastAPI dependencies for the billing module."""
|
||||
|
||||
from .feature_gate import (
|
||||
require_feature,
|
||||
RequireFeature,
|
||||
FeatureNotAvailableError,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"require_feature",
|
||||
"RequireFeature",
|
||||
"FeatureNotAvailableError",
|
||||
]
|
||||
254
app/modules/billing/dependencies/feature_gate.py
Normal file
254
app/modules/billing/dependencies/feature_gate.py
Normal file
@@ -0,0 +1,254 @@
|
||||
# app/core/feature_gate.py
|
||||
"""
|
||||
Feature gating decorator and dependencies for tier-based access control.
|
||||
|
||||
Provides:
|
||||
- @require_feature decorator for endpoints
|
||||
- RequireFeature dependency for flexible usage
|
||||
- FeatureNotAvailableError exception with upgrade info
|
||||
|
||||
Usage:
|
||||
# As decorator (simple)
|
||||
@router.get("/analytics")
|
||||
@require_feature(FeatureCode.ANALYTICS_DASHBOARD)
|
||||
def get_analytics(...):
|
||||
...
|
||||
|
||||
# As dependency (more control)
|
||||
@router.get("/analytics")
|
||||
def get_analytics(
|
||||
_: None = Depends(RequireFeature(FeatureCode.ANALYTICS_DASHBOARD)),
|
||||
...
|
||||
):
|
||||
...
|
||||
|
||||
# Multiple features (any one required)
|
||||
@require_feature(FeatureCode.ANALYTICS_DASHBOARD, FeatureCode.BASIC_REPORTS)
|
||||
def get_reports(...):
|
||||
...
|
||||
"""
|
||||
|
||||
import functools
|
||||
import logging
|
||||
from typing import Callable
|
||||
|
||||
from fastapi import Depends, HTTPException, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from app.modules.billing.services.feature_service import feature_service
|
||||
from app.modules.billing.models import FeatureCode
|
||||
from app.modules.tenancy.models import User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FeatureNotAvailableError(HTTPException):
|
||||
"""
|
||||
Exception raised when a feature is not available for the vendor's tier.
|
||||
|
||||
Includes upgrade information for the frontend to display.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
feature_code: str,
|
||||
feature_name: str | None = None,
|
||||
required_tier_code: str | None = None,
|
||||
required_tier_name: str | None = None,
|
||||
required_tier_price_cents: int | None = None,
|
||||
):
|
||||
detail = {
|
||||
"error": "feature_not_available",
|
||||
"message": f"This feature requires an upgrade to access.",
|
||||
"feature_code": feature_code,
|
||||
"feature_name": feature_name,
|
||||
"upgrade": {
|
||||
"tier_code": required_tier_code,
|
||||
"tier_name": required_tier_name,
|
||||
"price_monthly_cents": required_tier_price_cents,
|
||||
}
|
||||
if required_tier_code
|
||||
else None,
|
||||
}
|
||||
super().__init__(status_code=403, detail=detail)
|
||||
|
||||
|
||||
class RequireFeature:
|
||||
"""
|
||||
Dependency class that checks if vendor has access to a feature.
|
||||
|
||||
Can be used as a FastAPI dependency:
|
||||
@router.get("/analytics")
|
||||
def get_analytics(
|
||||
_: None = Depends(RequireFeature("analytics_dashboard")),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
...
|
||||
|
||||
Args:
|
||||
*feature_codes: One or more feature codes. Access granted if ANY is available.
|
||||
"""
|
||||
|
||||
def __init__(self, *feature_codes: str):
|
||||
if not feature_codes:
|
||||
raise ValueError("At least one feature code is required")
|
||||
self.feature_codes = feature_codes
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
) -> None:
|
||||
"""Check if vendor has access to any of the required features."""
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
# Check if vendor has ANY of the required features
|
||||
for feature_code in self.feature_codes:
|
||||
if feature_service.has_feature(db, vendor_id, feature_code):
|
||||
return None
|
||||
|
||||
# None of the features are available - get upgrade info for first one
|
||||
feature_code = self.feature_codes[0]
|
||||
upgrade_info = feature_service.get_feature_upgrade_info(db, feature_code)
|
||||
|
||||
if upgrade_info:
|
||||
raise FeatureNotAvailableError(
|
||||
feature_code=feature_code,
|
||||
feature_name=upgrade_info.feature_name,
|
||||
required_tier_code=upgrade_info.required_tier_code,
|
||||
required_tier_name=upgrade_info.required_tier_name,
|
||||
required_tier_price_cents=upgrade_info.required_tier_price_monthly_cents,
|
||||
)
|
||||
else:
|
||||
# Feature not found in registry
|
||||
raise FeatureNotAvailableError(feature_code=feature_code)
|
||||
|
||||
|
||||
def require_feature(*feature_codes: str) -> Callable:
|
||||
"""
|
||||
Decorator to require one or more features for an endpoint.
|
||||
|
||||
The decorated endpoint will return 403 with upgrade info if the vendor
|
||||
doesn't have access to ANY of the specified features.
|
||||
|
||||
Args:
|
||||
*feature_codes: One or more feature codes. Access granted if ANY is available.
|
||||
|
||||
Example:
|
||||
@router.get("/analytics/dashboard")
|
||||
@require_feature(FeatureCode.ANALYTICS_DASHBOARD)
|
||||
async def get_analytics_dashboard(
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
...
|
||||
|
||||
# Multiple features (any one is sufficient)
|
||||
@router.get("/reports")
|
||||
@require_feature(FeatureCode.ANALYTICS_DASHBOARD, FeatureCode.BASIC_REPORTS)
|
||||
async def get_reports(...):
|
||||
...
|
||||
"""
|
||||
if not feature_codes:
|
||||
raise ValueError("At least one feature code is required")
|
||||
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@functools.wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
# Extract dependencies from kwargs
|
||||
db = kwargs.get("db")
|
||||
current_user = kwargs.get("current_user")
|
||||
|
||||
if not db or not current_user:
|
||||
# Try to get from request if not in kwargs
|
||||
request = kwargs.get("request")
|
||||
if request and hasattr(request, "state"):
|
||||
db = getattr(request.state, "db", None)
|
||||
current_user = getattr(request.state, "user", None)
|
||||
|
||||
if not db or not current_user:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Feature check failed: missing db or current_user dependency",
|
||||
)
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
# Check if vendor has ANY of the required features
|
||||
for feature_code in feature_codes:
|
||||
if feature_service.has_feature(db, vendor_id, feature_code):
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
# None available - raise with upgrade info
|
||||
feature_code = feature_codes[0]
|
||||
upgrade_info = feature_service.get_feature_upgrade_info(db, feature_code)
|
||||
|
||||
if upgrade_info:
|
||||
raise FeatureNotAvailableError(
|
||||
feature_code=feature_code,
|
||||
feature_name=upgrade_info.feature_name,
|
||||
required_tier_code=upgrade_info.required_tier_code,
|
||||
required_tier_name=upgrade_info.required_tier_name,
|
||||
required_tier_price_cents=upgrade_info.required_tier_price_monthly_cents,
|
||||
)
|
||||
else:
|
||||
raise FeatureNotAvailableError(feature_code=feature_code)
|
||||
|
||||
@functools.wraps(func)
|
||||
def sync_wrapper(*args, **kwargs):
|
||||
# Extract dependencies from kwargs
|
||||
db = kwargs.get("db")
|
||||
current_user = kwargs.get("current_user")
|
||||
|
||||
if not db or not current_user:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Feature check failed: missing db or current_user dependency",
|
||||
)
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
# Check if vendor has ANY of the required features
|
||||
for feature_code in feature_codes:
|
||||
if feature_service.has_feature(db, vendor_id, feature_code):
|
||||
return func(*args, **kwargs)
|
||||
|
||||
# None available - raise with upgrade info
|
||||
feature_code = feature_codes[0]
|
||||
upgrade_info = feature_service.get_feature_upgrade_info(db, feature_code)
|
||||
|
||||
if upgrade_info:
|
||||
raise FeatureNotAvailableError(
|
||||
feature_code=feature_code,
|
||||
feature_name=upgrade_info.feature_name,
|
||||
required_tier_code=upgrade_info.required_tier_code,
|
||||
required_tier_name=upgrade_info.required_tier_name,
|
||||
required_tier_price_cents=upgrade_info.required_tier_price_monthly_cents,
|
||||
)
|
||||
else:
|
||||
raise FeatureNotAvailableError(feature_code=feature_code)
|
||||
|
||||
# Return appropriate wrapper based on whether func is async
|
||||
import asyncio
|
||||
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
return async_wrapper
|
||||
else:
|
||||
return sync_wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Convenience Exports
|
||||
# ============================================================================
|
||||
|
||||
__all__ = [
|
||||
"require_feature",
|
||||
"RequireFeature",
|
||||
"FeatureNotAvailableError",
|
||||
"FeatureCode",
|
||||
]
|
||||
@@ -1,7 +1,12 @@
|
||||
# app/modules/catalog/definition.py
|
||||
"""Catalog module definition."""
|
||||
|
||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition
|
||||
from app.modules.base import (
|
||||
MenuItemDefinition,
|
||||
MenuSectionDefinition,
|
||||
ModuleDefinition,
|
||||
PermissionDefinition,
|
||||
)
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
module = ModuleDefinition(
|
||||
@@ -11,7 +16,46 @@ module = ModuleDefinition(
|
||||
version="1.0.0",
|
||||
is_self_contained=True,
|
||||
requires=["inventory"],
|
||||
# New module-driven menu definitions
|
||||
# Module-driven permissions
|
||||
permissions=[
|
||||
PermissionDefinition(
|
||||
id="products.view",
|
||||
label_key="catalog.permissions.products_view",
|
||||
description_key="catalog.permissions.products_view_desc",
|
||||
category="products",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="products.create",
|
||||
label_key="catalog.permissions.products_create",
|
||||
description_key="catalog.permissions.products_create_desc",
|
||||
category="products",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="products.edit",
|
||||
label_key="catalog.permissions.products_edit",
|
||||
description_key="catalog.permissions.products_edit_desc",
|
||||
category="products",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="products.delete",
|
||||
label_key="catalog.permissions.products_delete",
|
||||
description_key="catalog.permissions.products_delete_desc",
|
||||
category="products",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="products.import",
|
||||
label_key="catalog.permissions.products_import",
|
||||
description_key="catalog.permissions.products_import_desc",
|
||||
category="products",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="products.export",
|
||||
label_key="catalog.permissions.products_export",
|
||||
description_key="catalog.permissions.products_export_desc",
|
||||
category="products",
|
||||
),
|
||||
],
|
||||
# Module-driven menu definitions
|
||||
menus={
|
||||
FrontendType.VENDOR: [
|
||||
MenuSectionDefinition(
|
||||
|
||||
249
app/modules/cms/services/theme_presets.py
Normal file
249
app/modules/cms/services/theme_presets.py
Normal file
@@ -0,0 +1,249 @@
|
||||
# app/core/theme_presets.py
|
||||
"""
|
||||
Theme presets for vendor shops.
|
||||
|
||||
Presets define default color schemes, fonts, and layouts that vendors can choose from.
|
||||
Each preset provides a complete theme configuration that can be customized further.
|
||||
"""
|
||||
|
||||
from app.modules.cms.models import VendorTheme
|
||||
|
||||
THEME_PRESETS = {
|
||||
"default": {
|
||||
"colors": {
|
||||
"primary": "#6366f1", # Indigo
|
||||
"secondary": "#8b5cf6", # Purple
|
||||
"accent": "#ec4899", # Pink
|
||||
"background": "#ffffff", # White
|
||||
"text": "#1f2937", # Gray-800
|
||||
"border": "#e5e7eb", # Gray-200
|
||||
},
|
||||
"fonts": {"heading": "Inter, sans-serif", "body": "Inter, sans-serif"},
|
||||
"layout": {"style": "grid", "header": "fixed", "product_card": "modern"},
|
||||
},
|
||||
"modern": {
|
||||
"colors": {
|
||||
"primary": "#6366f1", # Indigo - Modern tech look
|
||||
"secondary": "#8b5cf6", # Purple
|
||||
"accent": "#ec4899", # Pink
|
||||
"background": "#ffffff", # White
|
||||
"text": "#1f2937", # Gray-800
|
||||
"border": "#e5e7eb", # Gray-200
|
||||
},
|
||||
"fonts": {"heading": "Inter, sans-serif", "body": "Inter, sans-serif"},
|
||||
"layout": {"style": "grid", "header": "fixed", "product_card": "modern"},
|
||||
},
|
||||
"classic": {
|
||||
"colors": {
|
||||
"primary": "#1e40af", # Dark blue - Traditional
|
||||
"secondary": "#7c3aed", # Purple
|
||||
"accent": "#dc2626", # Red
|
||||
"background": "#ffffff", # White
|
||||
"text": "#1f2937", # Gray-800
|
||||
"border": "#d1d5db", # Gray-300
|
||||
},
|
||||
"fonts": {"heading": "Georgia, serif", "body": "Arial, sans-serif"},
|
||||
"layout": {"style": "list", "header": "static", "product_card": "classic"},
|
||||
},
|
||||
"minimal": {
|
||||
"colors": {
|
||||
"primary": "#000000", # Black - Ultra minimal
|
||||
"secondary": "#404040", # Dark gray
|
||||
"accent": "#666666", # Medium gray
|
||||
"background": "#ffffff", # White
|
||||
"text": "#000000", # Black
|
||||
"border": "#e5e7eb", # Light gray
|
||||
},
|
||||
"fonts": {"heading": "Helvetica, sans-serif", "body": "Helvetica, sans-serif"},
|
||||
"layout": {"style": "grid", "header": "transparent", "product_card": "minimal"},
|
||||
},
|
||||
"vibrant": {
|
||||
"colors": {
|
||||
"primary": "#f59e0b", # Orange - Bold & energetic
|
||||
"secondary": "#ef4444", # Red
|
||||
"accent": "#8b5cf6", # Purple
|
||||
"background": "#ffffff", # White
|
||||
"text": "#1f2937", # Gray-800
|
||||
"border": "#fbbf24", # Yellow
|
||||
},
|
||||
"fonts": {"heading": "Poppins, sans-serif", "body": "Open Sans, sans-serif"},
|
||||
"layout": {"style": "masonry", "header": "fixed", "product_card": "modern"},
|
||||
},
|
||||
"elegant": {
|
||||
"colors": {
|
||||
"primary": "#6b7280", # Gray - Sophisticated
|
||||
"secondary": "#374151", # Dark gray
|
||||
"accent": "#d97706", # Amber
|
||||
"background": "#ffffff", # White
|
||||
"text": "#1f2937", # Gray-800
|
||||
"border": "#e5e7eb", # Gray-200
|
||||
},
|
||||
"fonts": {"heading": "Playfair Display, serif", "body": "Lato, sans-serif"},
|
||||
"layout": {"style": "grid", "header": "fixed", "product_card": "classic"},
|
||||
},
|
||||
"nature": {
|
||||
"colors": {
|
||||
"primary": "#059669", # Green - Natural & eco
|
||||
"secondary": "#10b981", # Emerald
|
||||
"accent": "#f59e0b", # Amber
|
||||
"background": "#ffffff", # White
|
||||
"text": "#1f2937", # Gray-800
|
||||
"border": "#d1fae5", # Light green
|
||||
},
|
||||
"fonts": {"heading": "Montserrat, sans-serif", "body": "Open Sans, sans-serif"},
|
||||
"layout": {"style": "grid", "header": "fixed", "product_card": "modern"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_preset(preset_name: str) -> dict:
|
||||
"""
|
||||
Get a theme preset by name.
|
||||
|
||||
Args:
|
||||
preset_name: Name of the preset (e.g., 'modern', 'classic')
|
||||
|
||||
Returns:
|
||||
dict: Theme configuration
|
||||
|
||||
Raises:
|
||||
ValueError: If preset name is unknown
|
||||
"""
|
||||
if preset_name not in THEME_PRESETS:
|
||||
available = ", ".join(THEME_PRESETS.keys())
|
||||
raise ValueError(f"Unknown preset: {preset_name}. Available: {available}")
|
||||
|
||||
return THEME_PRESETS[preset_name]
|
||||
|
||||
|
||||
def apply_preset(theme: VendorTheme, preset_name: str) -> VendorTheme:
|
||||
"""
|
||||
Apply a preset to a vendor theme.
|
||||
|
||||
Args:
|
||||
theme: VendorTheme instance to update
|
||||
preset_name: Name of the preset to apply
|
||||
|
||||
Returns:
|
||||
VendorTheme: Updated theme instance
|
||||
|
||||
Raises:
|
||||
ValueError: If preset name is unknown
|
||||
|
||||
Example:
|
||||
theme = VendorTheme(vendor_id=1)
|
||||
apply_preset(theme, "modern")
|
||||
db.add(theme)
|
||||
db.commit()
|
||||
"""
|
||||
preset = get_preset(preset_name)
|
||||
|
||||
# Set theme name
|
||||
theme.theme_name = preset_name
|
||||
|
||||
# Apply colors (all of them!)
|
||||
theme.colors = preset["colors"]
|
||||
|
||||
# Apply fonts
|
||||
theme.font_family_heading = preset["fonts"]["heading"]
|
||||
theme.font_family_body = preset["fonts"]["body"]
|
||||
|
||||
# Apply layout settings
|
||||
theme.layout_style = preset["layout"]["style"]
|
||||
theme.header_style = preset["layout"]["header"]
|
||||
theme.product_card_style = preset["layout"]["product_card"]
|
||||
|
||||
# Mark as active
|
||||
theme.is_active = True
|
||||
|
||||
return theme
|
||||
|
||||
|
||||
def get_available_presets() -> list[str]:
|
||||
"""
|
||||
Get list of available preset names.
|
||||
|
||||
Returns:
|
||||
list: Available preset names
|
||||
"""
|
||||
return list(THEME_PRESETS.keys())
|
||||
|
||||
|
||||
def get_preset_preview(preset_name: str) -> dict:
|
||||
"""
|
||||
Get preview information for a preset (for UI display).
|
||||
|
||||
Args:
|
||||
preset_name: Name of the preset
|
||||
|
||||
Returns:
|
||||
dict: Preview info with colors, fonts, description
|
||||
"""
|
||||
preset = get_preset(preset_name)
|
||||
|
||||
descriptions = {
|
||||
"default": "Clean and professional - perfect for getting started",
|
||||
"modern": "Contemporary tech-inspired design with vibrant colors",
|
||||
"classic": "Traditional and trustworthy with serif typography",
|
||||
"minimal": "Ultra-clean black and white aesthetic",
|
||||
"vibrant": "Bold and energetic with bright accent colors",
|
||||
"elegant": "Sophisticated gray tones with refined typography",
|
||||
"nature": "Fresh and eco-friendly green color palette",
|
||||
}
|
||||
|
||||
return {
|
||||
"name": preset_name,
|
||||
"description": descriptions.get(preset_name, ""),
|
||||
"primary_color": preset["colors"]["primary"],
|
||||
"secondary_color": preset["colors"]["secondary"],
|
||||
"accent_color": preset["colors"]["accent"],
|
||||
"heading_font": preset["fonts"]["heading"],
|
||||
"body_font": preset["fonts"]["body"],
|
||||
"layout_style": preset["layout"]["style"],
|
||||
}
|
||||
|
||||
|
||||
def create_custom_preset(
|
||||
colors: dict, fonts: dict, layout: dict, name: str = "custom"
|
||||
) -> dict:
|
||||
"""
|
||||
Create a custom preset from provided settings.
|
||||
|
||||
Args:
|
||||
colors: Dict with primary, secondary, accent, background, text, border
|
||||
fonts: Dict with heading and body fonts
|
||||
layout: Dict with style, header, product_card
|
||||
name: Name for the custom preset
|
||||
|
||||
Returns:
|
||||
dict: Custom preset configuration
|
||||
|
||||
Example:
|
||||
custom = create_custom_preset(
|
||||
colors={"primary": "#ff0000", "secondary": "#00ff00", ...},
|
||||
fonts={"heading": "Arial", "body": "Arial"},
|
||||
layout={"style": "grid", "header": "fixed", "product_card": "modern"},
|
||||
name="my_custom_theme"
|
||||
)
|
||||
"""
|
||||
# Validate colors
|
||||
required_colors = ["primary", "secondary", "accent", "background", "text", "border"]
|
||||
for color_key in required_colors:
|
||||
if color_key not in colors:
|
||||
colors[color_key] = THEME_PRESETS["default"]["colors"][color_key]
|
||||
|
||||
# Validate fonts
|
||||
if "heading" not in fonts:
|
||||
fonts["heading"] = "Inter, sans-serif"
|
||||
if "body" not in fonts:
|
||||
fonts["body"] = "Inter, sans-serif"
|
||||
|
||||
# Validate layout
|
||||
if "style" not in layout:
|
||||
layout["style"] = "grid"
|
||||
if "header" not in layout:
|
||||
layout["header"] = "fixed"
|
||||
if "product_card" not in layout:
|
||||
layout["product_card"] = "modern"
|
||||
|
||||
return {"colors": colors, "fonts": fonts, "layout": layout}
|
||||
@@ -11,7 +11,7 @@ import re
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.theme_presets import (
|
||||
from app.modules.cms.services.theme_presets import (
|
||||
THEME_PRESETS,
|
||||
apply_preset,
|
||||
get_available_presets,
|
||||
|
||||
@@ -6,7 +6,12 @@ Dashboard, settings, and profile management.
|
||||
Required for basic operation - cannot be disabled.
|
||||
"""
|
||||
|
||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition
|
||||
from app.modules.base import (
|
||||
MenuItemDefinition,
|
||||
MenuSectionDefinition,
|
||||
ModuleDefinition,
|
||||
PermissionDefinition,
|
||||
)
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
core_module = ModuleDefinition(
|
||||
@@ -16,6 +21,40 @@ core_module = ModuleDefinition(
|
||||
version="1.0.0",
|
||||
is_core=True,
|
||||
is_self_contained=True,
|
||||
# Module-driven permissions
|
||||
permissions=[
|
||||
PermissionDefinition(
|
||||
id="dashboard.view",
|
||||
label_key="core.permissions.dashboard_view",
|
||||
description_key="core.permissions.dashboard_view_desc",
|
||||
category="dashboard",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="settings.view",
|
||||
label_key="core.permissions.settings_view",
|
||||
description_key="core.permissions.settings_view_desc",
|
||||
category="settings",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="settings.edit",
|
||||
label_key="core.permissions.settings_edit",
|
||||
description_key="core.permissions.settings_edit_desc",
|
||||
category="settings",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="settings.theme",
|
||||
label_key="core.permissions.settings_theme",
|
||||
description_key="core.permissions.settings_theme_desc",
|
||||
category="settings",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="settings.domains",
|
||||
label_key="core.permissions.settings_domains",
|
||||
description_key="core.permissions.settings_domains_desc",
|
||||
category="settings",
|
||||
is_owner_only=True, # Only owners can manage domains
|
||||
),
|
||||
],
|
||||
features=[
|
||||
"dashboard",
|
||||
"settings",
|
||||
|
||||
@@ -6,7 +6,12 @@ Defines the customers module including its features, menu items,
|
||||
route configurations, and self-contained module settings.
|
||||
"""
|
||||
|
||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition
|
||||
from app.modules.base import (
|
||||
MenuItemDefinition,
|
||||
MenuSectionDefinition,
|
||||
ModuleDefinition,
|
||||
PermissionDefinition,
|
||||
)
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
|
||||
@@ -30,6 +35,33 @@ customers_module = ModuleDefinition(
|
||||
name="Customer Management",
|
||||
description="Customer database, profiles, addresses, and segmentation.",
|
||||
version="1.0.0",
|
||||
# Module-driven permissions
|
||||
permissions=[
|
||||
PermissionDefinition(
|
||||
id="customers.view",
|
||||
label_key="customers.permissions.customers_view",
|
||||
description_key="customers.permissions.customers_view_desc",
|
||||
category="customers",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="customers.edit",
|
||||
label_key="customers.permissions.customers_edit",
|
||||
description_key="customers.permissions.customers_edit_desc",
|
||||
category="customers",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="customers.delete",
|
||||
label_key="customers.permissions.customers_delete",
|
||||
description_key="customers.permissions.customers_delete_desc",
|
||||
category="customers",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="customers.export",
|
||||
label_key="customers.permissions.customers_export",
|
||||
description_key="customers.permissions.customers_export_desc",
|
||||
category="customers",
|
||||
),
|
||||
],
|
||||
features=[
|
||||
"customer_view", # View customer profiles
|
||||
"customer_export", # Export customer data
|
||||
|
||||
@@ -6,7 +6,12 @@ Defines the inventory module including its features, menu items,
|
||||
route configurations, and self-contained module settings.
|
||||
"""
|
||||
|
||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition
|
||||
from app.modules.base import (
|
||||
MenuItemDefinition,
|
||||
MenuSectionDefinition,
|
||||
ModuleDefinition,
|
||||
PermissionDefinition,
|
||||
)
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
|
||||
@@ -33,6 +38,27 @@ inventory_module = ModuleDefinition(
|
||||
"transaction history, and bulk imports."
|
||||
),
|
||||
version="1.0.0",
|
||||
# Module-driven permissions
|
||||
permissions=[
|
||||
PermissionDefinition(
|
||||
id="stock.view",
|
||||
label_key="inventory.permissions.stock_view",
|
||||
description_key="inventory.permissions.stock_view_desc",
|
||||
category="stock",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="stock.edit",
|
||||
label_key="inventory.permissions.stock_edit",
|
||||
description_key="inventory.permissions.stock_edit_desc",
|
||||
category="stock",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="stock.transfer",
|
||||
label_key="inventory.permissions.stock_transfer",
|
||||
description_key="inventory.permissions.stock_transfer_desc",
|
||||
category="stock",
|
||||
),
|
||||
],
|
||||
features=[
|
||||
"inventory_basic", # Basic stock tracking
|
||||
"inventory_locations", # Multiple warehouse locations
|
||||
|
||||
@@ -6,7 +6,12 @@ Defines the orders module including its features, menu items,
|
||||
route configurations, and self-contained module settings.
|
||||
"""
|
||||
|
||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition
|
||||
from app.modules.base import (
|
||||
MenuItemDefinition,
|
||||
MenuSectionDefinition,
|
||||
ModuleDefinition,
|
||||
PermissionDefinition,
|
||||
)
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
|
||||
@@ -34,6 +39,33 @@ orders_module = ModuleDefinition(
|
||||
),
|
||||
version="1.0.0",
|
||||
requires=["payments"], # Depends on payments module for checkout
|
||||
# Module-driven permissions
|
||||
permissions=[
|
||||
PermissionDefinition(
|
||||
id="orders.view",
|
||||
label_key="orders.permissions.orders_view",
|
||||
description_key="orders.permissions.orders_view_desc",
|
||||
category="orders",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="orders.edit",
|
||||
label_key="orders.permissions.orders_edit",
|
||||
description_key="orders.permissions.orders_edit_desc",
|
||||
category="orders",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="orders.cancel",
|
||||
label_key="orders.permissions.orders_cancel",
|
||||
description_key="orders.permissions.orders_cancel_desc",
|
||||
category="orders",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="orders.refund",
|
||||
label_key="orders.permissions.orders_refund",
|
||||
description_key="orders.permissions.orders_refund_desc",
|
||||
category="orders",
|
||||
),
|
||||
],
|
||||
features=[
|
||||
"order_management", # Basic order CRUD
|
||||
"order_bulk_actions", # Bulk status updates
|
||||
|
||||
@@ -33,7 +33,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api, require_module_access
|
||||
from app.core.database import get_db
|
||||
from app.core.feature_gate import RequireFeature
|
||||
from app.modules.billing.dependencies.feature_gate import RequireFeature
|
||||
from app.modules.orders.exceptions import (
|
||||
InvoicePDFNotFoundException,
|
||||
)
|
||||
|
||||
@@ -6,7 +6,12 @@ Platform, company, vendor, and admin user management.
|
||||
Required for multi-tenant operation - cannot be disabled.
|
||||
"""
|
||||
|
||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition
|
||||
from app.modules.base import (
|
||||
MenuItemDefinition,
|
||||
MenuSectionDefinition,
|
||||
ModuleDefinition,
|
||||
PermissionDefinition,
|
||||
)
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
tenancy_module = ModuleDefinition(
|
||||
@@ -16,6 +21,36 @@ tenancy_module = ModuleDefinition(
|
||||
version="1.0.0",
|
||||
is_core=True,
|
||||
is_self_contained=True,
|
||||
# Module-driven permissions
|
||||
permissions=[
|
||||
PermissionDefinition(
|
||||
id="team.view",
|
||||
label_key="tenancy.permissions.team_view",
|
||||
description_key="tenancy.permissions.team_view_desc",
|
||||
category="team",
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="team.invite",
|
||||
label_key="tenancy.permissions.team_invite",
|
||||
description_key="tenancy.permissions.team_invite_desc",
|
||||
category="team",
|
||||
is_owner_only=True,
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="team.edit",
|
||||
label_key="tenancy.permissions.team_edit",
|
||||
description_key="tenancy.permissions.team_edit_desc",
|
||||
category="team",
|
||||
is_owner_only=True,
|
||||
),
|
||||
PermissionDefinition(
|
||||
id="team.remove",
|
||||
label_key="tenancy.permissions.team_remove",
|
||||
description_key="tenancy.permissions.team_remove_desc",
|
||||
category="team",
|
||||
is_owner_only=True,
|
||||
),
|
||||
],
|
||||
features=[
|
||||
"platform_management",
|
||||
"company_management",
|
||||
|
||||
407
app/modules/tenancy/services/permission_discovery_service.py
Normal file
407
app/modules/tenancy/services/permission_discovery_service.py
Normal file
@@ -0,0 +1,407 @@
|
||||
# app/modules/tenancy/services/permission_discovery_service.py
|
||||
"""
|
||||
Permission Discovery Service - Discovers and aggregates permissions from all modules.
|
||||
|
||||
This service implements the module-driven permission system where each module
|
||||
defines its own permissions through PermissionDefinition in its definition.py file.
|
||||
|
||||
Key Features:
|
||||
- Discovers permission definitions from all loaded modules
|
||||
- Groups permissions by category for UI organization
|
||||
- Provides role preset mappings (owner, manager, staff, etc.)
|
||||
- Supports permission checking and validation
|
||||
|
||||
Usage:
|
||||
from app.modules.tenancy.services.permission_discovery_service import (
|
||||
permission_discovery_service
|
||||
)
|
||||
|
||||
# Get all permissions
|
||||
all_perms = permission_discovery_service.get_all_permissions()
|
||||
|
||||
# Get permissions grouped by category
|
||||
grouped = permission_discovery_service.get_permissions_by_category()
|
||||
|
||||
# Get permission IDs for a role preset
|
||||
staff_perms = permission_discovery_service.get_preset_permissions("staff")
|
||||
|
||||
# Check if a permission ID is valid
|
||||
is_valid = permission_discovery_service.is_valid_permission("products.view")
|
||||
"""
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from app.modules.base import PermissionDefinition
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DiscoveredPermission:
|
||||
"""
|
||||
A permission discovered from a module, enriched with module info.
|
||||
|
||||
Extends PermissionDefinition with module context for tracking
|
||||
which module provides each permission.
|
||||
"""
|
||||
|
||||
id: str
|
||||
label_key: str
|
||||
description_key: str
|
||||
category: str
|
||||
is_owner_only: bool
|
||||
module_code: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class PermissionCategory:
|
||||
"""
|
||||
A category grouping related permissions for UI organization.
|
||||
"""
|
||||
|
||||
id: str
|
||||
label_key: str
|
||||
permissions: list[DiscoveredPermission] = field(default_factory=list)
|
||||
|
||||
|
||||
class PermissionDiscoveryService:
|
||||
"""
|
||||
Service to discover and aggregate permissions from all modules.
|
||||
|
||||
This service:
|
||||
1. Collects permission definitions from all module definition.py files
|
||||
2. Groups permissions by category for UI display
|
||||
3. Provides role preset definitions (owner, manager, staff, etc.)
|
||||
4. Validates permission IDs
|
||||
"""
|
||||
|
||||
# =========================================================================
|
||||
# Role Presets
|
||||
# =========================================================================
|
||||
# Role presets define default permission sets. These reference permission IDs
|
||||
# that should be defined by modules. The service validates these at runtime.
|
||||
|
||||
ROLE_PRESETS = {
|
||||
"owner": None, # Special: owners get ALL permissions
|
||||
"manager": {
|
||||
# Dashboard
|
||||
"dashboard.view",
|
||||
# Products
|
||||
"products.view",
|
||||
"products.create",
|
||||
"products.edit",
|
||||
"products.delete",
|
||||
"products.import",
|
||||
"products.export",
|
||||
# Stock/Inventory
|
||||
"stock.view",
|
||||
"stock.edit",
|
||||
"stock.transfer",
|
||||
# Orders
|
||||
"orders.view",
|
||||
"orders.edit",
|
||||
"orders.cancel",
|
||||
"orders.refund",
|
||||
# Customers
|
||||
"customers.view",
|
||||
"customers.edit",
|
||||
"customers.export",
|
||||
# Marketing
|
||||
"marketing.view",
|
||||
"marketing.create",
|
||||
"marketing.send",
|
||||
# Reports
|
||||
"reports.view",
|
||||
"reports.financial",
|
||||
"reports.export",
|
||||
# Settings (limited)
|
||||
"settings.view",
|
||||
"settings.theme",
|
||||
# Imports
|
||||
"imports.view",
|
||||
"imports.create",
|
||||
"imports.cancel",
|
||||
},
|
||||
"staff": {
|
||||
# Dashboard
|
||||
"dashboard.view",
|
||||
# Products
|
||||
"products.view",
|
||||
"products.create",
|
||||
"products.edit",
|
||||
# Stock
|
||||
"stock.view",
|
||||
"stock.edit",
|
||||
# Orders
|
||||
"orders.view",
|
||||
"orders.edit",
|
||||
# Customers
|
||||
"customers.view",
|
||||
"customers.edit",
|
||||
},
|
||||
"support": {
|
||||
# Dashboard
|
||||
"dashboard.view",
|
||||
# Products (view only)
|
||||
"products.view",
|
||||
# Orders
|
||||
"orders.view",
|
||||
"orders.edit",
|
||||
# Customers
|
||||
"customers.view",
|
||||
"customers.edit",
|
||||
},
|
||||
"viewer": {
|
||||
# Read-only access
|
||||
"dashboard.view",
|
||||
"products.view",
|
||||
"stock.view",
|
||||
"orders.view",
|
||||
"customers.view",
|
||||
"reports.view",
|
||||
},
|
||||
"marketing": {
|
||||
# Marketing-focused role
|
||||
"dashboard.view",
|
||||
"customers.view",
|
||||
"customers.export",
|
||||
"marketing.view",
|
||||
"marketing.create",
|
||||
"marketing.send",
|
||||
"reports.view",
|
||||
},
|
||||
}
|
||||
|
||||
# Category labels for UI grouping
|
||||
CATEGORY_LABELS = {
|
||||
"dashboard": "tenancy.permissions.category.dashboard",
|
||||
"products": "tenancy.permissions.category.products",
|
||||
"stock": "tenancy.permissions.category.stock",
|
||||
"orders": "tenancy.permissions.category.orders",
|
||||
"customers": "tenancy.permissions.category.customers",
|
||||
"marketing": "tenancy.permissions.category.marketing",
|
||||
"reports": "tenancy.permissions.category.reports",
|
||||
"settings": "tenancy.permissions.category.settings",
|
||||
"team": "tenancy.permissions.category.team",
|
||||
"imports": "tenancy.permissions.category.imports",
|
||||
"general": "tenancy.permissions.category.general",
|
||||
}
|
||||
|
||||
def get_all_permissions(self) -> list[DiscoveredPermission]:
|
||||
"""
|
||||
Discover all permissions from all loaded modules.
|
||||
|
||||
Returns:
|
||||
List of DiscoveredPermission objects from all modules
|
||||
"""
|
||||
from app.modules.registry import MODULES
|
||||
|
||||
permissions = []
|
||||
|
||||
for module_code, module_def in MODULES.items():
|
||||
for perm in module_def.permissions:
|
||||
discovered = DiscoveredPermission(
|
||||
id=perm.id,
|
||||
label_key=perm.label_key,
|
||||
description_key=perm.description_key,
|
||||
category=perm.category,
|
||||
is_owner_only=perm.is_owner_only,
|
||||
module_code=module_code,
|
||||
)
|
||||
permissions.append(discovered)
|
||||
|
||||
return sorted(permissions, key=lambda p: (p.category, p.id))
|
||||
|
||||
def get_all_permission_ids(self) -> set[str]:
|
||||
"""
|
||||
Get set of all permission IDs from all modules.
|
||||
|
||||
Returns:
|
||||
Set of permission ID strings
|
||||
"""
|
||||
from app.modules.registry import MODULES
|
||||
|
||||
ids = set()
|
||||
for module_def in MODULES.values():
|
||||
ids.update(module_def.get_permission_ids())
|
||||
|
||||
return ids
|
||||
|
||||
def get_permission(self, permission_id: str) -> DiscoveredPermission | None:
|
||||
"""
|
||||
Get a specific permission by ID.
|
||||
|
||||
Args:
|
||||
permission_id: Permission ID to look up
|
||||
|
||||
Returns:
|
||||
DiscoveredPermission if found, None otherwise
|
||||
"""
|
||||
from app.modules.registry import MODULES
|
||||
|
||||
for module_code, module_def in MODULES.items():
|
||||
for perm in module_def.permissions:
|
||||
if perm.id == permission_id:
|
||||
return DiscoveredPermission(
|
||||
id=perm.id,
|
||||
label_key=perm.label_key,
|
||||
description_key=perm.description_key,
|
||||
category=perm.category,
|
||||
is_owner_only=perm.is_owner_only,
|
||||
module_code=module_code,
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def get_permissions_by_category(self) -> list[PermissionCategory]:
|
||||
"""
|
||||
Get all permissions grouped by category.
|
||||
|
||||
Returns:
|
||||
List of PermissionCategory objects with permissions
|
||||
"""
|
||||
all_perms = self.get_all_permissions()
|
||||
|
||||
# Group by category
|
||||
categories_map: dict[str, list[DiscoveredPermission]] = {}
|
||||
for perm in all_perms:
|
||||
if perm.category not in categories_map:
|
||||
categories_map[perm.category] = []
|
||||
categories_map[perm.category].append(perm)
|
||||
|
||||
# Build category objects
|
||||
categories = []
|
||||
for cat_id, perms in sorted(categories_map.items()):
|
||||
label_key = self.CATEGORY_LABELS.get(
|
||||
cat_id, f"tenancy.permissions.category.{cat_id}"
|
||||
)
|
||||
categories.append(
|
||||
PermissionCategory(
|
||||
id=cat_id,
|
||||
label_key=label_key,
|
||||
permissions=perms,
|
||||
)
|
||||
)
|
||||
|
||||
return categories
|
||||
|
||||
def get_permissions_for_module(
|
||||
self, module_code: str
|
||||
) -> list[DiscoveredPermission]:
|
||||
"""
|
||||
Get all permissions defined by a specific module.
|
||||
|
||||
Args:
|
||||
module_code: Module code to get permissions for
|
||||
|
||||
Returns:
|
||||
List of DiscoveredPermission from the specified module
|
||||
"""
|
||||
from app.modules.registry import MODULES
|
||||
|
||||
module_def = MODULES.get(module_code)
|
||||
if not module_def:
|
||||
return []
|
||||
|
||||
return [
|
||||
DiscoveredPermission(
|
||||
id=perm.id,
|
||||
label_key=perm.label_key,
|
||||
description_key=perm.description_key,
|
||||
category=perm.category,
|
||||
is_owner_only=perm.is_owner_only,
|
||||
module_code=module_code,
|
||||
)
|
||||
for perm in module_def.permissions
|
||||
]
|
||||
|
||||
def is_valid_permission(self, permission_id: str) -> bool:
|
||||
"""
|
||||
Check if a permission ID is valid (defined by some module).
|
||||
|
||||
Args:
|
||||
permission_id: Permission ID to validate
|
||||
|
||||
Returns:
|
||||
True if permission exists, False otherwise
|
||||
"""
|
||||
return permission_id in self.get_all_permission_ids()
|
||||
|
||||
def get_preset_permissions(self, preset_name: str) -> set[str]:
|
||||
"""
|
||||
Get permission IDs for a role preset.
|
||||
|
||||
Args:
|
||||
preset_name: Name of preset (owner, manager, staff, support, viewer, marketing)
|
||||
|
||||
Returns:
|
||||
Set of permission IDs for the preset, or empty set if not found.
|
||||
For "owner", returns all permission IDs.
|
||||
"""
|
||||
preset = self.ROLE_PRESETS.get(preset_name.lower())
|
||||
|
||||
if preset_name.lower() == "owner":
|
||||
# Owners get all permissions
|
||||
return self.get_all_permission_ids()
|
||||
|
||||
if preset is None:
|
||||
logger.warning(f"Unknown role preset: {preset_name}")
|
||||
return set()
|
||||
|
||||
# Filter to only permissions that actually exist
|
||||
all_perms = self.get_all_permission_ids()
|
||||
return preset & all_perms
|
||||
|
||||
def get_available_presets(self) -> list[str]:
|
||||
"""
|
||||
Get list of available role preset names.
|
||||
|
||||
Returns:
|
||||
List of preset names
|
||||
"""
|
||||
return list(self.ROLE_PRESETS.keys())
|
||||
|
||||
def validate_permissions(self, permission_ids: list[str]) -> list[str]:
|
||||
"""
|
||||
Validate a list of permission IDs.
|
||||
|
||||
Args:
|
||||
permission_ids: List of permission IDs to validate
|
||||
|
||||
Returns:
|
||||
List of invalid permission IDs (empty if all valid)
|
||||
"""
|
||||
all_perms = self.get_all_permission_ids()
|
||||
return [pid for pid in permission_ids if pid not in all_perms]
|
||||
|
||||
def get_owner_only_permissions(self) -> set[str]:
|
||||
"""
|
||||
Get permission IDs that are owner-only.
|
||||
|
||||
These permissions cannot be assigned to team members.
|
||||
|
||||
Returns:
|
||||
Set of owner-only permission IDs
|
||||
"""
|
||||
from app.modules.registry import MODULES
|
||||
|
||||
owner_only = set()
|
||||
for module_def in MODULES.values():
|
||||
for perm in module_def.permissions:
|
||||
if perm.is_owner_only:
|
||||
owner_only.add(perm.id)
|
||||
|
||||
return owner_only
|
||||
|
||||
|
||||
# Singleton instance
|
||||
permission_discovery_service = PermissionDiscoveryService()
|
||||
|
||||
|
||||
__all__ = [
|
||||
"permission_discovery_service",
|
||||
"PermissionDiscoveryService",
|
||||
"DiscoveredPermission",
|
||||
"PermissionCategory",
|
||||
]
|
||||
Reference in New Issue
Block a user