diff --git a/.architecture-rules/frontend.yaml b/.architecture-rules/frontend.yaml index 4f3b2f17..fb21327d 100644 --- a/.architecture-rules/frontend.yaml +++ b/.architecture-rules/frontend.yaml @@ -358,11 +358,12 @@ javascript_rules: CORRECT: apiClient.get('/admin/vendors') apiClient.post('/admin/products') - const apiEndpoint = '/admin/vendors' + const url = '/admin/vendors' WRONG (causes double prefix /api/v1/api/v1/...): apiClient.get('/api/v1/admin/vendors') - const apiEndpoint = '/api/v1/admin/vendors' + const url = '/api/v1/admin/vendors' + const endpoint = '/api/v1/admin/products' Exception: Direct fetch() calls without apiClient should use full path. @@ -371,9 +372,11 @@ javascript_rules: file_pattern: "static/**/js/**/*.js" anti_patterns: - "apiClient\\.(get|post|put|delete|patch)\\s*\\(\\s*['\"`]/api/v1" - - "apiEndpoint.*=.*['\"`]/api/v1" + - "(const|let|var)\\s+(url|endpoint|apiEndpoint|apiUrl|path)\\s*=\\s*['\"`]/api/v1" + - "\\$\\{.*\\}/api/v1" exceptions: - "init-api-client.js" + - "api-client.js" # ============================================================================ # TEMPLATE RULES (Jinja2) diff --git a/app/api/v1/admin/admin_users.py b/app/api/v1/admin/admin_users.py index 65048cb2..1cbd3de7 100644 --- a/app/api/v1/admin/admin_users.py +++ b/app/api/v1/admin/admin_users.py @@ -4,9 +4,11 @@ Admin user management endpoints (Super Admin only). This module provides endpoints for: - Listing all admin users with their platform assignments -- Creating platform admins +- Creating platform admins and super admins - Assigning/removing platform access - Promoting/demoting super admin status +- Toggling admin status +- Deleting admin users """ import logging @@ -18,6 +20,7 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_super_admin, get_current_super_admin_api from app.core.database import get_db +from app.exceptions import ValidationException from app.services.admin_platform_service import admin_platform_service from models.database.user import User @@ -65,15 +68,16 @@ class AdminUserListResponse(BaseModel): total: int -class CreatePlatformAdminRequest(BaseModel): - """Request to create a new platform admin.""" +class CreateAdminUserRequest(BaseModel): + """Request to create a new admin user (platform admin or super admin).""" email: EmailStr username: str password: str first_name: Optional[str] = None last_name: Optional[str] = None - platform_ids: list[int] + is_super_admin: bool = False + platform_ids: list[int] = [] class AssignPlatformRequest(BaseModel): @@ -89,147 +93,12 @@ class ToggleSuperAdminRequest(BaseModel): # ============================================================================ -# ENDPOINTS +# HELPER FUNCTIONS # ============================================================================ -@router.get("", response_model=AdminUserListResponse) -def list_admin_users( - skip: int = Query(0, ge=0), - limit: int = Query(100, ge=1, le=500), - include_super_admins: bool = Query(True), - db: Session = Depends(get_db), - current_admin: User = Depends(get_current_super_admin), -): - """ - List all admin users with their platform assignments. - - Super admin only. - """ - from sqlalchemy.orm import joinedload - - query = db.query(User).filter(User.role == "admin") - - if not include_super_admins: - query = query.filter(User.is_super_admin == False) - - total = query.count() - - admins = ( - query.options(joinedload(User.admin_platforms)) - .offset(skip) - .limit(limit) - .all() - ) - - admin_responses = [] - for admin in admins: - assignments = [] - if not admin.is_super_admin: - for ap in admin.admin_platforms: - if ap.is_active and ap.platform: - assignments.append( - PlatformAssignmentResponse( - platform_id=ap.platform_id, - platform_code=ap.platform.code, - platform_name=ap.platform.name, - is_active=ap.is_active, - ) - ) - - admin_responses.append( - AdminUserResponse( - id=admin.id, - email=admin.email, - username=admin.username, - first_name=admin.first_name, - last_name=admin.last_name, - is_active=admin.is_active, - is_super_admin=admin.is_super_admin, - platform_assignments=assignments, - ) - ) - - return AdminUserListResponse(admins=admin_responses, total=total) - - -@router.post("", response_model=AdminUserResponse) -def create_platform_admin( - request: CreatePlatformAdminRequest, - db: Session = Depends(get_db), - current_admin: User = Depends(get_current_super_admin_api), -): - """ - Create a new platform admin with platform assignments. - - Super admin only. - """ - user, assignments = admin_platform_service.create_platform_admin( - db=db, - email=request.email, - username=request.username, - password=request.password, - platform_ids=request.platform_ids, - created_by_user_id=current_admin.id, - first_name=request.first_name, - last_name=request.last_name, - ) - - db.commit() - - # Refresh to get relationships - db.refresh(user) - - assignment_responses = [ - PlatformAssignmentResponse( - platform_id=ap.platform_id, - platform_code=ap.platform.code if ap.platform else "", - platform_name=ap.platform.name if ap.platform else "", - is_active=ap.is_active, - ) - for ap in user.admin_platforms - if ap.is_active - ] - - logger.info(f"Created platform admin {user.username} by {current_admin.username}") - - return AdminUserResponse( - id=user.id, - email=user.email, - username=user.username, - first_name=user.first_name, - last_name=user.last_name, - is_active=user.is_active, - is_super_admin=user.is_super_admin, - platform_assignments=assignment_responses, - ) - - -@router.get("/{user_id}", response_model=AdminUserResponse) -def get_admin_user( - user_id: int = Path(...), - db: Session = Depends(get_db), - current_admin: User = Depends(get_current_super_admin), -): - """ - Get admin user details with platform assignments. - - Super admin only. - """ - from sqlalchemy.orm import joinedload - - from app.exceptions import ValidationException - - admin = ( - db.query(User) - .options(joinedload(User.admin_platforms)) - .filter(User.id == user_id, User.role == "admin") - .first() - ) - - if not admin: - raise ValidationException("Admin user not found", field="user_id") - +def _build_admin_response(admin: User) -> AdminUserResponse: + """Build AdminUserResponse from User model.""" assignments = [] if not admin.is_super_admin: for ap in admin.admin_platforms: @@ -255,6 +124,111 @@ def get_admin_user( ) +# ============================================================================ +# ENDPOINTS +# ============================================================================ + + +@router.get("", response_model=AdminUserListResponse) +def list_admin_users( + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=500), + include_super_admins: bool = Query(True), + db: Session = Depends(get_db), + current_admin: User = Depends(get_current_super_admin), +): + """ + List all admin users with their platform assignments. + + Super admin only. + """ + admins, total = admin_platform_service.list_admin_users( + db=db, + skip=skip, + limit=limit, + include_super_admins=include_super_admins, + ) + + admin_responses = [_build_admin_response(admin) for admin in admins] + + return AdminUserListResponse(admins=admin_responses, total=total) + + +@router.post("", response_model=AdminUserResponse) +def create_admin_user( + request: CreateAdminUserRequest, + db: Session = Depends(get_db), + current_admin: User = Depends(get_current_super_admin_api), +): + """ + Create a new admin user (super admin or platform admin). + + Super admin only. + """ + # Validate platform_ids required for non-super admin + if not request.is_super_admin and not request.platform_ids: + raise ValidationException( + "Platform admins must be assigned to at least one platform", + field="platform_ids", + ) + + if request.is_super_admin: + # Create super admin using service + user = admin_platform_service.create_super_admin( + db=db, + email=request.email, + username=request.username, + password=request.password, + created_by_user_id=current_admin.id, + first_name=request.first_name, + last_name=request.last_name, + ) + db.commit() + db.refresh(user) + + return AdminUserResponse( + id=user.id, + email=user.email, + username=user.username, + first_name=user.first_name, + last_name=user.last_name, + is_active=user.is_active, + is_super_admin=user.is_super_admin, + platform_assignments=[], + ) + else: + # Create platform admin with assignments using service + user, assignments = admin_platform_service.create_platform_admin( + db=db, + email=request.email, + username=request.username, + password=request.password, + platform_ids=request.platform_ids, + created_by_user_id=current_admin.id, + first_name=request.first_name, + last_name=request.last_name, + ) + db.commit() + db.refresh(user) + + return _build_admin_response(user) + + +@router.get("/{user_id}", response_model=AdminUserResponse) +def get_admin_user( + user_id: int = Path(...), + db: Session = Depends(get_db), + current_admin: User = Depends(get_current_super_admin), +): + """ + Get admin user details with platform assignments. + + Super admin only. + """ + admin = admin_platform_service.get_admin_user(db=db, user_id=user_id) + return _build_admin_response(admin) + + @router.post("/{user_id}/platforms/{platform_id}") def assign_admin_to_platform( user_id: int = Path(...), @@ -267,7 +241,7 @@ def assign_admin_to_platform( Super admin only. """ - assignment = admin_platform_service.assign_admin_to_platform( + admin_platform_service.assign_admin_to_platform( db=db, admin_user_id=user_id, platform_id=platform_id, @@ -275,10 +249,6 @@ def assign_admin_to_platform( ) db.commit() - logger.info( - f"Assigned admin {user_id} to platform {platform_id} by {current_admin.username}" - ) - return { "message": "Admin assigned to platform successfully", "platform_id": platform_id, @@ -306,10 +276,6 @@ def remove_admin_from_platform( ) db.commit() - logger.info( - f"Removed admin {user_id} from platform {platform_id} by {current_admin.username}" - ) - return { "message": "Admin removed from platform successfully", "platform_id": platform_id, @@ -338,7 +304,6 @@ def toggle_super_admin_status( db.commit() action = "promoted to" if request.is_super_admin else "demoted from" - logger.info(f"Admin {user.username} {action} super admin by {current_admin.username}") return { "message": f"Admin {action} super admin successfully", @@ -371,3 +336,54 @@ def get_admin_platforms( ], "user_id": user_id, } + + +@router.put("/{user_id}/status") +def toggle_admin_status( + user_id: int = Path(...), + db: Session = Depends(get_db), + current_admin: User = Depends(get_current_super_admin_api), +): + """ + Toggle admin user active status. + + Super admin only. Cannot deactivate yourself. + """ + admin = admin_platform_service.toggle_admin_status( + db=db, + user_id=user_id, + current_admin_id=current_admin.id, + ) + db.commit() + + action = "activated" if admin.is_active else "deactivated" + + return { + "message": f"Admin user {action} successfully", + "user_id": user_id, + "is_active": admin.is_active, + } + + +@router.delete("/{user_id}") +def delete_admin_user( + user_id: int = Path(...), + db: Session = Depends(get_db), + current_admin: User = Depends(get_current_super_admin_api), +): + """ + Delete an admin user. + + Super admin only. Cannot delete yourself. + """ + admin_platform_service.delete_admin_user( + db=db, + user_id=user_id, + current_admin_id=current_admin.id, + ) + db.commit() + + return { + "message": "Admin user deleted successfully", + "user_id": user_id, + } diff --git a/app/api/v1/admin/auth.py b/app/api/v1/admin/auth.py index 1b922dc7..85527e24 100644 --- a/app/api/v1/admin/auth.py +++ b/app/api/v1/admin/auth.py @@ -141,7 +141,7 @@ def get_accessible_platforms( - For platform admins: Only assigned platforms """ if current_user.is_super_admin: - platforms = db.query(Platform).filter(Platform.is_active == True).all() + platforms = admin_platform_service.get_all_active_platforms(db) else: platforms = admin_platform_service.get_platforms_for_admin(db, current_user.id) @@ -184,14 +184,11 @@ def select_platform( "Super admins don't need platform selection - they have global access" ) - # Verify admin has access to this platform - if not current_user.can_access_platform(platform_id): - raise InsufficientPermissionsException( - f"You don't have access to this platform" - ) + # Verify admin has access to this platform (raises exception if not) + admin_platform_service.validate_admin_platform_access(current_user, platform_id) # Load platform - platform = db.query(Platform).filter(Platform.id == platform_id).first() + platform = admin_platform_service.get_platform_by_id(db, platform_id) if not platform: raise InvalidCredentialsException("Platform not found") diff --git a/app/routes/admin_pages.py b/app/routes/admin_pages.py index 4bfc1b3a..d0aaf037 100644 --- a/app/routes/admin_pages.py +++ b/app/routes/admin_pages.py @@ -22,7 +22,10 @@ Routes: - GET /vendors/{vendor_code}/domains → Vendor domains management (auth required) - GET /vendor-themes → Vendor themes selection page (auth required) - GET /vendors/{vendor_code}/theme → Vendor theme editor (auth required) -- GET /users → User management page (auth required) +- GET /admin-users → Admin users management (super admin only) +- GET /admin-users/create → Create admin user (super admin only) +- GET /admin-users/{id} → Admin user detail (super admin only) +- GET /users → Redirects to /admin/admin-users - GET /customers → Customer management page (auth required) - GET /inventory → Inventory management page (auth required) - GET /orders → Orders management page (auth required) @@ -396,22 +399,28 @@ async def admin_vendor_theme_page( # ============================================================================ -# USER MANAGEMENT ROUTES +# ADMIN USER MANAGEMENT ROUTES (Super Admin Only) # ============================================================================ -@router.get("/users", response_class=HTMLResponse, include_in_schema=False) -async def admin_users_page( +@router.get("/admin-users", response_class=HTMLResponse, include_in_schema=False) +async def admin_users_list_page( request: Request, current_user: User = Depends(get_current_admin_from_cookie_or_header), db: Session = Depends(get_db), ): """ - Render users management page. - Shows list of all platform users. + Render admin users management page. + Shows list of all admin users (super admins and platform admins). + Super admin only. """ + from fastapi import HTTPException + + if not current_user.is_super_admin: + raise HTTPException(status_code=403, detail="Super admin access required") + return templates.TemplateResponse( - "admin/users.html", + "admin/admin-users.html", { "request": request, "user": current_user, @@ -419,15 +428,21 @@ async def admin_users_page( ) -@router.get("/users/create", response_class=HTMLResponse, include_in_schema=False) +@router.get("/admin-users/create", response_class=HTMLResponse, include_in_schema=False) async def admin_user_create_page( request: Request, current_user: User = Depends(get_current_admin_from_cookie_or_header), db: Session = Depends(get_db), ): """ - Render user creation form. + Render admin user creation form. + Super admin only. """ + from fastapi import HTTPException + + if not current_user.is_super_admin: + raise HTTPException(status_code=403, detail="Super admin access required") + return templates.TemplateResponse( "admin/user-create.html", { @@ -437,7 +452,9 @@ async def admin_user_create_page( ) -@router.get("/users/{user_id}", response_class=HTMLResponse, include_in_schema=False) +@router.get( + "/admin-users/{user_id}", response_class=HTMLResponse, include_in_schema=False +) async def admin_user_detail_page( request: Request, user_id: int = Path(..., description="User ID"), @@ -445,10 +462,16 @@ async def admin_user_detail_page( db: Session = Depends(get_db), ): """ - Render user detail view. + Render admin user detail view. + Super admin only. """ + from fastapi import HTTPException + + if not current_user.is_super_admin: + raise HTTPException(status_code=403, detail="Super admin access required") + return templates.TemplateResponse( - "admin/user-detail.html", + "admin/admin-user-detail.html", { "request": request, "user": current_user, @@ -458,7 +481,7 @@ async def admin_user_detail_page( @router.get( - "/users/{user_id}/edit", response_class=HTMLResponse, include_in_schema=False + "/admin-users/{user_id}/edit", response_class=HTMLResponse, include_in_schema=False ) async def admin_user_edit_page( request: Request, @@ -467,8 +490,14 @@ async def admin_user_edit_page( db: Session = Depends(get_db), ): """ - Render user edit form. + Render admin user edit form. + Super admin only. """ + from fastapi import HTTPException + + if not current_user.is_super_admin: + raise HTTPException(status_code=403, detail="Super admin access required") + return templates.TemplateResponse( "admin/user-edit.html", { @@ -479,6 +508,47 @@ async def admin_user_edit_page( ) +# ============================================================================ +# USER MANAGEMENT ROUTES (Legacy - Redirects) +# ============================================================================ + + +@router.get("/users", response_class=RedirectResponse, include_in_schema=False) +async def admin_users_page_redirect(): + """ + Redirect old /admin/users to /admin/admin-users. + """ + return RedirectResponse(url="/admin/admin-users", status_code=302) + + +@router.get("/users/create", response_class=RedirectResponse, include_in_schema=False) +async def admin_user_create_page_redirect(): + """ + Redirect old /admin/users/create to /admin/admin-users/create. + """ + return RedirectResponse(url="/admin/admin-users/create", status_code=302) + + +@router.get( + "/users/{user_id}", response_class=RedirectResponse, include_in_schema=False +) +async def admin_user_detail_page_redirect(user_id: int = Path(..., description="User ID")): + """ + Redirect old /admin/users/{id} to /admin/admin-users/{id}. + """ + return RedirectResponse(url=f"/admin/admin-users/{user_id}", status_code=302) + + +@router.get( + "/users/{user_id}/edit", response_class=RedirectResponse, include_in_schema=False +) +async def admin_user_edit_page_redirect(user_id: int = Path(..., description="User ID")): + """ + Redirect old /admin/users/{id}/edit to /admin/admin-users/{id}/edit. + """ + return RedirectResponse(url=f"/admin/admin-users/{user_id}/edit", status_code=302) + + # ============================================================================ # CUSTOMER MANAGEMENT ROUTES # ============================================================================ diff --git a/app/services/admin_platform_service.py b/app/services/admin_platform_service.py index a3b6b7b1..dfc70eea 100644 --- a/app/services/admin_platform_service.py +++ b/app/services/admin_platform_service.py @@ -196,6 +196,53 @@ class AdminPlatformService: return query.all() + def get_all_active_platforms(self, db: Session) -> list[Platform]: + """ + Get all active platforms (for super admin access). + + Args: + db: Database session + + Returns: + List of all active Platform objects + """ + return db.query(Platform).filter(Platform.is_active == True).all() + + def get_platform_by_id(self, db: Session, platform_id: int) -> Platform | None: + """ + Get a platform by ID. + + Args: + db: Database session + platform_id: Platform ID + + Returns: + Platform object or None if not found + """ + return db.query(Platform).filter(Platform.id == platform_id).first() + + def validate_admin_platform_access( + self, + user: User, + platform_id: int, + ) -> None: + """ + Validate that an admin has access to a platform. + + Args: + user: User object + platform_id: Platform ID to check + + Raises: + InsufficientPermissionsException: If user doesn't have access + """ + from app.exceptions import InsufficientPermissionsException + + if not user.can_access_platform(platform_id): + raise InsufficientPermissionsException( + "You don't have access to this platform" + ) + def get_admins_for_platform( self, db: Session, @@ -385,5 +432,232 @@ class AdminPlatformService: return user, assignments + # ============================================================================ + # ADMIN USER CRUD OPERATIONS + # ============================================================================ + + def list_admin_users( + self, + db: Session, + skip: int = 0, + limit: int = 100, + include_super_admins: bool = True, + is_active: bool | None = None, + search: str | None = None, + ) -> tuple[list[User], int]: + """ + List all admin users with optional filtering. + + Args: + db: Database session + skip: Number of records to skip + limit: Maximum records to return + include_super_admins: Whether to include super admins + is_active: Filter by active status + search: Search term for username/email/name + + Returns: + Tuple of (list of User objects, total count) + """ + query = db.query(User).filter(User.role == "admin") + + if not include_super_admins: + query = query.filter(User.is_super_admin == False) + + if is_active is not None: + query = query.filter(User.is_active == is_active) + + if search: + search_term = f"%{search}%" + query = query.filter( + (User.username.ilike(search_term)) + | (User.email.ilike(search_term)) + | (User.first_name.ilike(search_term)) + | (User.last_name.ilike(search_term)) + ) + + total = query.count() + + admins = ( + query.options(joinedload(User.admin_platforms)) + .offset(skip) + .limit(limit) + .all() + ) + + return admins, total + + def get_admin_user( + self, + db: Session, + user_id: int, + ) -> User: + """ + Get a single admin user by ID with platform assignments. + + Args: + db: Database session + user_id: User ID + + Returns: + User object with admin_platforms loaded + + Raises: + ValidationException: If user not found or not an admin + """ + admin = ( + db.query(User) + .options(joinedload(User.admin_platforms)) + .filter(User.id == user_id, User.role == "admin") + .first() + ) + + if not admin: + raise ValidationException("Admin user not found", field="user_id") + + return admin + + def create_super_admin( + self, + db: Session, + email: str, + username: str, + password: str, + created_by_user_id: int, + first_name: str | None = None, + last_name: str | None = None, + ) -> User: + """ + Create a new super admin user. + + Args: + db: Database session + email: Admin email + username: Admin username + password: Admin password + created_by_user_id: Super admin creating the account + first_name: Optional first name + last_name: Optional last name + + Returns: + Created User object + """ + from app.core.security import get_password_hash + + # Check for existing user + existing = ( + db.query(User) + .filter((User.email == email) | (User.username == username)) + .first() + ) + if existing: + field = "email" if existing.email == email else "username" + raise ValidationException(f"{field.capitalize()} already exists", field=field) + + user = User( + email=email, + username=username, + hashed_password=get_password_hash(password), + first_name=first_name, + last_name=last_name, + role="admin", + is_super_admin=True, + is_active=True, + ) + db.add(user) + db.flush() + db.refresh(user) + + logger.info( + f"Created super admin {username} by admin {created_by_user_id}" + ) + + return user + + def toggle_admin_status( + self, + db: Session, + user_id: int, + current_admin_id: int, + ) -> User: + """ + Toggle admin user active status. + + Args: + db: Database session + user_id: User ID to toggle + current_admin_id: Super admin making the change + + Returns: + Updated User object + + Raises: + CannotModifySelfException: If trying to deactivate self + ValidationException: If user not found or not an admin + """ + if user_id == current_admin_id: + raise CannotModifySelfException( + user_id=user_id, + operation="deactivate own account", + ) + + admin = db.query(User).filter(User.id == user_id, User.role == "admin").first() + + if not admin: + raise ValidationException("Admin user not found", field="user_id") + + admin.is_active = not admin.is_active + admin.updated_at = datetime.now(UTC) + db.flush() + db.refresh(admin) + + action = "activated" if admin.is_active else "deactivated" + logger.info( + f"Admin {admin.username} {action} by admin {current_admin_id}" + ) + + return admin + + def delete_admin_user( + self, + db: Session, + user_id: int, + current_admin_id: int, + ) -> None: + """ + Delete an admin user and their platform assignments. + + Args: + db: Database session + user_id: User ID to delete + current_admin_id: Super admin making the deletion + + Raises: + CannotModifySelfException: If trying to delete self + ValidationException: If user not found or not an admin + """ + if user_id == current_admin_id: + raise CannotModifySelfException( + user_id=user_id, + operation="delete own account", + ) + + admin = db.query(User).filter(User.id == user_id, User.role == "admin").first() + + if not admin: + raise ValidationException("Admin user not found", field="user_id") + + username = admin.username + + # Delete admin platform assignments first + db.query(AdminPlatform).filter(AdminPlatform.user_id == user_id).delete() + + # Delete the admin user + db.delete(admin) + db.flush() + + logger.info(f"Admin {username} deleted by admin {current_admin_id}") + + # Singleton instance admin_platform_service = AdminPlatformService() diff --git a/app/templates/admin/admin-user-detail.html b/app/templates/admin/admin-user-detail.html new file mode 100644 index 00000000..5f9dc7e1 --- /dev/null +++ b/app/templates/admin/admin-user-detail.html @@ -0,0 +1,238 @@ +{# app/templates/admin/admin-user-detail.html #} +{% extends "admin/base.html" %} +{% from 'shared/macros/alerts.html' import loading_state, error_state %} +{% from 'shared/macros/headers.html' import detail_page_header %} + +{% block title %}Admin User Details{% endblock %} + +{% block alpine_data %}adminUserDetailPage(){% endblock %} + +{% block content %} +{% call detail_page_header("adminUser?.full_name || adminUser?.username || 'Admin User Details'", '/admin/admin-users', subtitle_show='adminUser') %} + @ + | + +{% endcall %} + +{{ loading_state('Loading admin user details...') }} + +{{ error_state('Error loading admin user') }} + + +
+ Admin Type +
++ - +
++ Status +
++ - +
++ Platforms +
++ 0 +
++ Created +
++ - +
+Username
+@
+-
+Email Verified
+ + +Full Name
+-
+First Name
+-
+Last Name
+-
+No platforms assigned. This admin cannot access any platform.
+ + ++ This user has full access to all platforms and can manage other admin users. +
+Last Login
+-
+Created At
+-
+Last Updated
+-
++ Total Admins +
++ 0 +
++ Super Admins +
++ 0 +
++ Platform Admins +
++ 0 +
++ Active +
++ 0 +
+No admin users found
+ +Contact your administrator to get platform access.