From df83ec42d0da0f202a3b22f9cadb163152f0a921 Mon Sep 17 00:00:00 2001 From: amee Date: Fri, 17 Apr 2026 10:34:51 +0530 Subject: [PATCH] filter functionality added on all modules. --- app/controllers/auth/role_controller.py | 8 ++ .../auth/subscription_plan_controller.py | 26 +++++- app/controllers/auth/tenant_controller.py | 14 ++- app/controllers/auth/user_controller.py | 16 +++- app/routes/admin/audit_logs.py | 39 +++++++- app/routes/auth/role.py | 8 ++ app/routes/auth/subscription_plan.py | 17 +++- app/routes/auth/tenant.py | 12 +++ app/routes/auth/user.py | 18 +++- app/services/auth/role_service.py | 21 ++++- .../auth/subscription_plan_service.py | 38 +++++++- app/services/auth/tenant_service.py | 33 ++++++- app/services/auth/user_service.py | 90 ++++++++++++++++--- 13 files changed, 311 insertions(+), 29 deletions(-) diff --git a/app/controllers/auth/role_controller.py b/app/controllers/auth/role_controller.py index 6d6c3fc..5458497 100644 --- a/app/controllers/auth/role_controller.py +++ b/app/controllers/auth/role_controller.py @@ -89,6 +89,10 @@ class RoleController: page: int = 1, page_size: int = 10, search: Optional[str] = None, + filter_role_names: Optional[List[str]] = None, + filter_tenant_ids: Optional[List[uuid.UUID]] = None, + sort_by: Optional[str] = None, + sort_order: Optional[str] = None, ) -> RolePaginatedResponse: tenant_id = None if current_user_tenant_id is None else current_user_tenant_id return RoleService.get_roles_paginated( @@ -97,4 +101,8 @@ class RoleController: page=page, page_size=page_size, search=search, + filter_role_names=filter_role_names, + filter_tenant_ids=filter_tenant_ids, + sort_by=sort_by, + sort_order=sort_order, ) \ No newline at end of file diff --git a/app/controllers/auth/subscription_plan_controller.py b/app/controllers/auth/subscription_plan_controller.py index 025bd64..df179ab 100644 --- a/app/controllers/auth/subscription_plan_controller.py +++ b/app/controllers/auth/subscription_plan_controller.py @@ -1,5 +1,5 @@ import uuid -from typing import Optional +from typing import List, Optional from sqlalchemy.orm import Session from app.schemas.auth.subscription_plan_schema import SubscriptionPlanCreate, SubscriptionPlanUpdate from app.services.auth.subscription_plan_service import SubscriptionPlanService @@ -23,8 +23,28 @@ class SubscriptionPlanController: return SubscriptionPlanService.get_all_plans(db, is_public, status) @staticmethod - def get_paginated_plans(db: Session, page: int, page_size: int, search: Optional[str]): - return SubscriptionPlanService.get_paginated_plans(db, page, page_size, search) + def get_paginated_plans( + db: Session, + page: int, + page_size: int, + search: Optional[str], + filter_names: Optional[List[str]] = None, + statuses: Optional[List[str]] = None, + visibility: Optional[List[bool]] = None, + sort_by: Optional[str] = None, + sort_order: Optional[str] = None, + ): + return SubscriptionPlanService.get_paginated_plans( + db, + page, + page_size, + search, + filter_names=filter_names, + statuses=statuses, + visibility=visibility, + sort_by=sort_by, + sort_order=sort_order, + ) @staticmethod def delete_plan(db: Session, plan_id: uuid.UUID): diff --git a/app/controllers/auth/tenant_controller.py b/app/controllers/auth/tenant_controller.py index dfdff4c..48823b0 100644 --- a/app/controllers/auth/tenant_controller.py +++ b/app/controllers/auth/tenant_controller.py @@ -2,7 +2,7 @@ from sqlalchemy.orm import Session from app.schemas.auth.tenant_schema import TenantCreate, TenantUpdate from app.services.auth.tenant_service import TenantService import uuid -from typing import Optional +from typing import List, Optional class TenantController: @@ -33,6 +33,12 @@ class TenantController: page_size: int, search: Optional[str], is_active: Optional[bool], + filter_tenant_names: Optional[List[str]] = None, + filter_tenant_domains: Optional[List[str]] = None, + filter_plan_ids: Optional[List[uuid.UUID]] = None, + statuses: Optional[List[bool]] = None, + sort_by: Optional[str] = None, + sort_order: Optional[str] = None, ): return TenantService.get_tenants_paginated( db=db, @@ -40,4 +46,10 @@ class TenantController: page_size=page_size, search=search, is_active=is_active, + filter_tenant_names=filter_tenant_names, + filter_tenant_domains=filter_tenant_domains, + filter_plan_ids=filter_plan_ids, + statuses=statuses, + sort_by=sort_by, + sort_order=sort_order, ) \ No newline at end of file diff --git a/app/controllers/auth/user_controller.py b/app/controllers/auth/user_controller.py index 6d57974..da1cc17 100644 --- a/app/controllers/auth/user_controller.py +++ b/app/controllers/auth/user_controller.py @@ -55,7 +55,13 @@ class UserController: page: int = 1, page_size: int = 10, search: Optional[str] = None, - status: Optional[str] = None, + filter_names: Optional[List[str]] = None, + filter_emails: Optional[List[str]] = None, + statuses: Optional[List[str]] = None, + filter_tenant_ids: Optional[List[uuid.UUID]] = None, + filter_role_ids: Optional[List[uuid.UUID]] = None, + sort_by: Optional[str] = None, + sort_order: Optional[str] = None, ): tenant_id = current_user.tenant_id return UserService.get_users_paginated( @@ -64,5 +70,11 @@ class UserController: page=page, page_size=page_size, search=search, - status=status, + filter_names=filter_names, + filter_emails=filter_emails, + statuses=statuses, + filter_tenant_ids=filter_tenant_ids, + filter_role_ids=filter_role_ids, + sort_by=sort_by, + sort_order=sort_order, ) \ No newline at end of file diff --git a/app/routes/admin/audit_logs.py b/app/routes/admin/audit_logs.py index 9e77e92..98c1d10 100644 --- a/app/routes/admin/audit_logs.py +++ b/app/routes/admin/audit_logs.py @@ -1,6 +1,7 @@ -from typing import Optional +from typing import List, Optional from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session +from sqlalchemy import asc, desc, or_, cast, String from app.config.database import get_db from app.middleware.auth_middleware import get_current_user, User @@ -16,6 +17,12 @@ def get_audit_logs( module_name: Optional[str] = Query(None, description="Filter by module name"), action_type: Optional[str] = Query(None, description="CREATE | UPDATE | DELETE"), performed_by_email: Optional[str] = Query(None, description="Filter by actor email"), + search: Optional[str] = Query(None, description="Search audit logs"), + module_names: Optional[List[str]] = Query(None, description="Filter by module names"), + action_types: Optional[List[str]] = Query(None, description="Filter by action types"), + performed_by_emails: Optional[List[str]] = Query(None, description="Filter by actor emails"), + sort_by: Optional[str] = Query("created_at", description="Sort by created_at, module_name, action_type, or performed_by_email"), + sort_order: Optional[str] = Query("desc", description="Sort order: asc or desc"), limit: int = Query(50, ge=1, le=500), offset: int = Query(0, ge=0), ): @@ -27,16 +34,44 @@ def get_audit_logs( if module_name: query = query.filter(AuditLog.module_name == module_name) + if module_names: + query = query.filter(AuditLog.module_name.in_(module_names)) if action_type: query = query.filter(AuditLog.action_type == action_type.upper()) + if action_types: + normalized_action_types = [item.upper() for item in action_types] + query = query.filter(AuditLog.action_type.in_(normalized_action_types)) if performed_by_email: query = query.filter( AuditLog.performed_by_email.ilike(f"%{performed_by_email}%") ) + if performed_by_emails: + query = query.filter(AuditLog.performed_by_email.in_(performed_by_emails)) + if search: + search_term = search.strip() + query = query.filter( + or_( + AuditLog.module_name.ilike(f"%{search_term}%"), + AuditLog.action_type.ilike(f"%{search_term}%"), + AuditLog.entity_name.ilike(f"%{search_term}%"), + AuditLog.description.ilike(f"%{search_term}%"), + AuditLog.performed_by_email.ilike(f"%{search_term}%"), + AuditLog.ip_address.ilike(f"%{search_term}%"), + cast(AuditLog.entity_id, String).ilike(f"%{search_term}%"), + ) + ) total = query.count() + sort_column_map = { + "created_at": AuditLog.created_at, + "module_name": AuditLog.module_name, + "action_type": AuditLog.action_type, + "performed_by_email": AuditLog.performed_by_email, + } + sort_column = sort_column_map.get(sort_by or "created_at", AuditLog.created_at) + order_fn = asc if (sort_order or "").lower() == "asc" else desc items = ( - query.order_by(AuditLog.created_at.desc()) + query.order_by(order_fn(sort_column)) .offset(offset) .limit(limit) .all() diff --git a/app/routes/auth/role.py b/app/routes/auth/role.py index 2f1e176..bb735e3 100644 --- a/app/routes/auth/role.py +++ b/app/routes/auth/role.py @@ -135,6 +135,10 @@ def list_roles( page: int = Query(1, ge=1, description="Page number"), page_size: int = Query(10, ge=1, le=100, description="Items per page"), search: Optional[str] = Query(None, description="Search in role name or ID"), + filter_role_names: Optional[List[str]] = Query(None, description="Filter by role names"), + filter_tenant_ids: Optional[List[uuid.UUID]] = Query(None, description="Filter by tenant IDs"), + sort_by: Optional[str] = Query(None, description="Sort by role name or tenant"), + sort_order: Optional[str] = Query(None, description="Sort order: asc or desc"), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), _=Depends(require_access("admin.role.read")), @@ -145,4 +149,8 @@ def list_roles( page=page, page_size=page_size, search=search, + filter_role_names=filter_role_names, + filter_tenant_ids=filter_tenant_ids, + sort_by=sort_by, + sort_order=sort_order, ) \ No newline at end of file diff --git a/app/routes/auth/subscription_plan.py b/app/routes/auth/subscription_plan.py index 3cc12f5..a54dd69 100644 --- a/app/routes/auth/subscription_plan.py +++ b/app/routes/auth/subscription_plan.py @@ -93,11 +93,26 @@ def list_plans( page: int = Query(1, ge=1), page_size: int = Query(10, ge=1, le=100), search: Optional[str] = Query(None), + filter_names: Optional[List[str]] = Query(None), + statuses: Optional[List[str]] = Query(None), + visibility: Optional[List[bool]] = Query(None, description="Filter by public/private visibility"), + sort_by: Optional[str] = Query(None, description="Sort by name, status, visibility, or price"), + sort_order: Optional[str] = Query(None, description="Sort order: asc or desc"), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), _ = Depends(require_access("superadmin.plan.read")) ): - return SubscriptionPlanController.get_paginated_plans(db, page, page_size, search) + return SubscriptionPlanController.get_paginated_plans( + db, + page, + page_size, + search, + filter_names=filter_names, + statuses=statuses, + visibility=visibility, + sort_by=sort_by, + sort_order=sort_order, + ) @router.get("/all", response_model=List[SubscriptionPlanResponse]) def all_plans( diff --git a/app/routes/auth/tenant.py b/app/routes/auth/tenant.py index 97c2b41..3ddee71 100644 --- a/app/routes/auth/tenant.py +++ b/app/routes/auth/tenant.py @@ -133,6 +133,12 @@ def list_tenants( page_size: int = Query(10, ge=1, le=100, description="Items per page"), search: Optional[str] = Query(None, description="Search in name, domain, or ID"), is_active: Optional[bool] = Query(None, description="Filter by active status"), + filter_tenant_names: Optional[List[str]] = Query(None, description="Filter by tenant names"), + filter_tenant_domains: Optional[List[str]] = Query(None, description="Filter by tenant domains"), + filter_plan_ids: Optional[List[uuid.UUID]] = Query(None, description="Filter by plan IDs"), + statuses: Optional[List[bool]] = Query(None, description="Filter by tenant statuses"), + sort_by: Optional[str] = Query(None, description="Sort by name, domain, status, or plan"), + sort_order: Optional[str] = Query(None, description="Sort order: asc or desc"), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), _ = Depends(require_access("superadmin.tenant.read")), @@ -143,4 +149,10 @@ def list_tenants( page_size=page_size, search=search, is_active=is_active, + filter_tenant_names=filter_tenant_names, + filter_tenant_domains=filter_tenant_domains, + filter_plan_ids=filter_plan_ids, + statuses=statuses, + sort_by=sort_by, + sort_order=sort_order, ) \ No newline at end of file diff --git a/app/routes/auth/user.py b/app/routes/auth/user.py index e2612f7..3c34476 100644 --- a/app/routes/auth/user.py +++ b/app/routes/auth/user.py @@ -121,7 +121,13 @@ def list_users( page: int = Query(1, ge=1, description="Page number"), page_size: int = Query(10, ge=1, le=100, description="Items per page"), search: Optional[str] = Query(None, description="Search in email, name, phone, or ID"), - status: Optional[str] = Query(None, description="Filter by user status (e.g. active, inactive)"), + filter_names: Optional[List[str]] = Query(None, description="Filter by full names"), + filter_emails: Optional[List[str]] = Query(None, description="Filter by emails"), + statuses: Optional[List[str]] = Query(None, description="Filter by user statuses"), + filter_tenant_ids: Optional[List[uuid.UUID]] = Query(None, description="Filter by tenant IDs"), + filter_role_ids: Optional[List[uuid.UUID]] = Query(None, description="Filter by role IDs"), + sort_by: Optional[str] = Query(None, description="Sort by name, email, status, tenant, or role"), + sort_order: Optional[str] = Query(None, description="Sort order: asc or desc"), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), _ = Depends(require_access("admin.user.read")), @@ -132,5 +138,11 @@ def list_users( page=page, page_size=page_size, search=search, - status=status, - ) \ No newline at end of file + filter_names=filter_names, + filter_emails=filter_emails, + statuses=statuses, + filter_tenant_ids=filter_tenant_ids, + filter_role_ids=filter_role_ids, + sort_by=sort_by, + sort_order=sort_order, + ) diff --git a/app/services/auth/role_service.py b/app/services/auth/role_service.py index 4c08b36..32ec649 100644 --- a/app/services/auth/role_service.py +++ b/app/services/auth/role_service.py @@ -3,7 +3,7 @@ import json import logging from typing import List, Optional from sqlalchemy.orm import Session -from sqlalchemy import or_, cast, String +from sqlalchemy import or_, cast, String, asc, desc from fastapi import HTTPException, status from app.models.auth.role_model import Role from app.models.auth.role_access_model import RoleAccess @@ -313,6 +313,10 @@ class RoleService: page: int = 1, page_size: int = 10, search: Optional[str] = None, + filter_role_names: Optional[List[str]] = None, + filter_tenant_ids: Optional[List[uuid.UUID]] = None, + sort_by: Optional[str] = None, + sort_order: Optional[str] = None, ) -> RolePaginatedResponse: query = db.query(Role) @@ -320,6 +324,12 @@ class RoleService: if tenant_id is not None: query = query.filter(Role.tenant_id == tenant_id) + if filter_role_names: + query = query.filter(Role.role_name.in_(filter_role_names)) + + if filter_tenant_ids: + query = query.filter(Role.tenant_id.in_(filter_tenant_ids)) + if search and search.strip(): search_term = search.strip() query = query.filter( @@ -329,6 +339,15 @@ class RoleService: ) ) + sort_column_map = { + "name": Role.role_name, + "tenant": Role.tenant_id, + } + sort_column = sort_column_map.get(sort_by or "") + if sort_column is not None: + order_fn = desc if (sort_order or "").lower() == "desc" else asc + query = query.order_by(order_fn(sort_column)) + total = query.count() offset = (page - 1) * page_size diff --git a/app/services/auth/subscription_plan_service.py b/app/services/auth/subscription_plan_service.py index 3ca0565..dd8ae61 100644 --- a/app/services/auth/subscription_plan_service.py +++ b/app/services/auth/subscription_plan_service.py @@ -1,7 +1,7 @@ import uuid -from typing import Optional +from typing import List, Optional from sqlalchemy.orm import Session, joinedload -from sqlalchemy import or_, cast, String +from sqlalchemy import or_, cast, String, asc, desc from fastapi import HTTPException, status from app.models.auth.subscription_plan_model import SubscriptionPlan @@ -103,8 +103,28 @@ class SubscriptionPlanService: return query.all() @staticmethod - def get_paginated_plans(db: Session, page: int = 1, page_size: int = 10, search: Optional[str] = None) -> SubscriptionPlanPaginatedResponse: + def get_paginated_plans( + db: Session, + page: int = 1, + page_size: int = 10, + search: Optional[str] = None, + filter_names: Optional[List[str]] = None, + statuses: Optional[List[str]] = None, + visibility: Optional[List[bool]] = None, + sort_by: Optional[str] = None, + sort_order: Optional[str] = None, + ) -> SubscriptionPlanPaginatedResponse: query = db.query(SubscriptionPlan) + + if filter_names: + query = query.filter(SubscriptionPlan.name.in_(filter_names)) + + if statuses: + query = query.filter(SubscriptionPlan.status.in_(statuses)) + + if visibility is not None and len(visibility) > 0: + query = query.filter(SubscriptionPlan.is_public.in_(visibility)) + if search: query = query.filter( or_( @@ -112,6 +132,18 @@ class SubscriptionPlanService: cast(SubscriptionPlan.id, String).ilike(f"%{search}%") ) ) + + sort_column_map = { + "name": SubscriptionPlan.name, + "price": SubscriptionPlan.price, + "status": SubscriptionPlan.status, + "visibility": SubscriptionPlan.is_public, + } + sort_column = sort_column_map.get(sort_by or "") + if sort_column is not None: + order_fn = desc if (sort_order or "").lower() == "desc" else asc + query = query.order_by(order_fn(sort_column)) + total = query.count() offset = (page - 1) * page_size plans = query.offset(offset).limit(page_size).all() diff --git a/app/services/auth/tenant_service.py b/app/services/auth/tenant_service.py index 936c413..028cb65 100644 --- a/app/services/auth/tenant_service.py +++ b/app/services/auth/tenant_service.py @@ -1,5 +1,5 @@ from sqlalchemy.orm import Session -from sqlalchemy import or_, cast, String +from sqlalchemy import or_, cast, String, asc, desc from fastapi import HTTPException, status from app.models.auth.tenant_model import Tenant from app.models.auth.tenant_module_model import TenantModule @@ -13,7 +13,7 @@ from app.schemas.auth.tenant_schema import TenantCreate, TenantUpdate, TenantPag from app.schemas.auth.role_schema import RoleCreate, RoleUpdate from app.services.auth.role_service import RoleService import uuid -from typing import Optional +from typing import List, Optional from app.services.auth.event_service import EventService import logging @@ -411,10 +411,25 @@ class TenantService: page_size: int = 10, search: Optional[str] = None, is_active: Optional[bool] = None, + filter_tenant_names: Optional[List[str]] = None, + filter_tenant_domains: Optional[List[str]] = None, + filter_plan_ids: Optional[List[uuid.UUID]] = None, + statuses: Optional[List[bool]] = None, + sort_by: Optional[str] = None, + sort_order: Optional[str] = None, ) -> TenantPaginatedResponse: query = db.query(Tenant) + if filter_tenant_names: + query = query.filter(Tenant.tenant_name.in_(filter_tenant_names)) + + if filter_tenant_domains: + query = query.filter(Tenant.tenant_domain.in_(filter_tenant_domains)) + + if filter_plan_ids: + query = query.filter(Tenant.plan_id.in_(filter_plan_ids)) + if search and search.strip(): search_term = search.strip() query = query.filter( @@ -428,6 +443,20 @@ class TenantService: if is_active is not None: query = query.filter(Tenant.is_active == is_active) + if statuses is not None and len(statuses) > 0: + query = query.filter(Tenant.is_active.in_(statuses)) + + sort_column_map = { + "name": Tenant.tenant_name, + "domain": Tenant.tenant_domain, + "status": Tenant.is_active, + "plan": Tenant.plan_id, + } + sort_column = sort_column_map.get(sort_by or "") + if sort_column is not None: + order_fn = desc if (sort_order or "").lower() == "desc" else asc + query = query.order_by(order_fn(sort_column)) + total = query.count() offset = (page - 1) * page_size diff --git a/app/services/auth/user_service.py b/app/services/auth/user_service.py index a5624b6..b30b613 100644 --- a/app/services/auth/user_service.py +++ b/app/services/auth/user_service.py @@ -1,5 +1,5 @@ from sqlalchemy.orm import Session -from sqlalchemy import or_, cast, String +from sqlalchemy import or_, cast, String, asc, desc, func from fastapi import HTTPException, status, BackgroundTasks from datetime import datetime import uuid @@ -14,6 +14,7 @@ from app.models.auth.role_module_access_model import RoleModuleAccess from app.models.auth.module_access_model import ModuleAccess from app.models.auth.tenant_module_model import TenantModule from app.models.auth.role_model import Role +from app.models.auth.tenant_model import Tenant logger = logging.getLogger(__name__) @@ -235,11 +236,17 @@ class UserService: page: int = 1, page_size: int = 10, search: Optional[str] = None, - status: Optional[str] = None, + filter_names: Optional[List[str]] = None, + filter_emails: Optional[List[str]] = None, + statuses: Optional[List[str]] = None, + filter_tenant_ids: Optional[List[uuid.UUID]] = None, + filter_role_ids: Optional[List[uuid.UUID]] = None, + sort_by: Optional[str] = None, + sort_order: Optional[str] = None, ) -> UserPaginatedResponse: - - query = db.query(User) + query = db.query(User).outerjoin(Tenant, User.tenant_id == Tenant.id).outerjoin(Role, User.role_id == Role.id) + # Scope to tenant if not superadmin if tenant_id: query = query.filter(User.tenant_id == tenant_id) @@ -256,8 +263,73 @@ class UserService: ) ) - if status is not None: - query = query.filter(User.status == status) + if filter_names: + normalized_names = [name.strip() for name in filter_names if isinstance(name, str) and name.strip()] + if normalized_names: + query = query.filter( + func.trim( + func.concat( + User.first_name, + " ", + func.coalesce(User.last_name, ""), + ) + ).in_(normalized_names) + ) + + if filter_emails: + normalized_emails = [email.strip() for email in filter_emails if isinstance(email, str) and email.strip()] + if normalized_emails: + query = query.filter(User.email.in_(normalized_emails)) + + if statuses: + normalized_statuses = [status for status in statuses if status] + if normalized_statuses: + query = query.filter(User.status.in_(normalized_statuses)) + + if filter_tenant_ids: + tenant_ids: List[uuid.UUID] = [] + for tenant_value in filter_tenant_ids: + try: + tenant_ids.append( + tenant_value if isinstance(tenant_value, uuid.UUID) else uuid.UUID(str(tenant_value)) + ) + except (ValueError, AttributeError, TypeError): + continue + if tenant_ids: + query = query.filter(User.tenant_id.in_(tenant_ids)) + + if filter_role_ids: + role_ids: List[uuid.UUID] = [] + for role_value in filter_role_ids: + try: + role_ids.append( + role_value if isinstance(role_value, uuid.UUID) else uuid.UUID(str(role_value)) + ) + except (ValueError, AttributeError, TypeError): + continue + if role_ids: + query = query.filter(User.role_id.in_(role_ids)) + + normalized_sort_by = (sort_by or "").strip().lower() + normalized_sort_order = (sort_order or "asc").strip().lower() + sort_fn = desc if normalized_sort_order == "desc" else asc + + if normalized_sort_by == "name": + query = query.order_by( + sort_fn(func.lower(User.first_name)), + sort_fn(func.lower(func.coalesce(User.last_name, ""))), + asc(User.email), + ) + elif normalized_sort_by == "email": + query = query.order_by(sort_fn(func.lower(User.email))) + elif normalized_sort_by == "status": + query = query.order_by(sort_fn(func.lower(User.status)), asc(User.email)) + elif normalized_sort_by == "tenant": + query = query.order_by(sort_fn(func.lower(func.coalesce(Tenant.tenant_name, ""))), asc(User.email)) + elif normalized_sort_by == "role": + query = query.order_by(sort_fn(func.lower(func.coalesce(Role.role_name, ""))), asc(User.email)) + else: + query = query.order_by(desc(User.created_at)) total = query.count() @@ -276,10 +348,6 @@ class UserService: @staticmethod def _resolve_targets_for_role(db: Session, role_id: uuid.UUID, tenant_id: uuid.UUID = None) -> List[Dict[str, Any]]: - """ - Helper to resolve which modules/envs a role targets. - Mirrors logic in RoleService. - """ active_modules = ( db.query(ModuleAccess.module_id) .join(RoleModuleAccess, RoleModuleAccess.module_access_id == ModuleAccess.id) @@ -298,7 +366,7 @@ class UserService: tm_assignments = db.query(TenantModule).filter( TenantModule.tenant_id == tenant_id, TenantModule.module_id.in_(module_ids), - TenantModule.is_active == True + TenantModule.is_active == True ).all() for tm in tm_assignments: env_map[str(tm.module_id)] = tm.assigned_environment_slug or "prod"