feat: implement admin-users management with super admin restriction
- Add /admin/admin-users routes for managing admin users (super admin only) - Remove vendor role from user creation form (vendors created via company hierarchy) - Add admin-users.html and admin-user-detail.html templates - Add admin-users.js and admin-user-detail.js for frontend logic - Move database operations to admin_platform_service (list, get, create, delete, toggle status) - Update sidebar to show Admin Users section only for super admins - Add isSuperAdmin computed property to init-alpine.js - Fix /api/v1 prefix issues in JS files (apiClient already adds prefix) - Update architecture rule JS-012 to catch more variable patterns (url, endpoint, path) - Replace inline SVGs with $icon() helper in select-platform.html Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
# ============================================================================
|
||||
|
||||
@@ -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()
|
||||
|
||||
238
app/templates/admin/admin-user-detail.html
Normal file
238
app/templates/admin/admin-user-detail.html
Normal file
@@ -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') %}
|
||||
@<span x-text="adminUser?.username"></span>
|
||||
<span class="text-gray-400 mx-2">|</span>
|
||||
<span x-text="adminUser?.email"></span>
|
||||
{% endcall %}
|
||||
|
||||
{{ loading_state('Loading admin user details...') }}
|
||||
|
||||
{{ error_state('Error loading admin user') }}
|
||||
|
||||
<!-- Admin User Details -->
|
||||
<div x-show="!loading && adminUser">
|
||||
<!-- Quick Actions Card -->
|
||||
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
Quick Actions
|
||||
</h3>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<a
|
||||
:href="`/admin/admin-users/${userId}/edit`"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple">
|
||||
<span x-html="$icon('edit', 'w-4 h-4 mr-2')"></span>
|
||||
Edit Admin User
|
||||
</a>
|
||||
<button
|
||||
@click="toggleStatus()"
|
||||
:disabled="saving || adminUser?.id === currentUserId"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 border border-transparent rounded-lg focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:class="adminUser?.is_active ? 'bg-orange-600 hover:bg-orange-700' : 'bg-green-600 hover:bg-green-700'"
|
||||
:title="adminUser?.id === currentUserId ? 'Cannot deactivate yourself' : ''">
|
||||
<span x-html="$icon(adminUser?.is_active ? 'user-x' : 'user-check', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-text="adminUser?.is_active ? 'Deactivate' : 'Activate'"></span>
|
||||
</button>
|
||||
<button
|
||||
@click="deleteAdminUser()"
|
||||
:disabled="saving || adminUser?.id === currentUserId"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-red-600 border border-transparent rounded-lg hover:bg-red-700 focus:outline-none focus:shadow-outline-red disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:title="adminUser?.id === currentUserId ? 'Cannot delete yourself' : 'Delete admin user'">
|
||||
<span x-html="$icon('delete', 'w-4 h-4 mr-2')"></span>
|
||||
Delete Admin User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Cards -->
|
||||
<div class="grid gap-6 mb-8 md:grid-cols-4">
|
||||
<!-- Admin Type -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 rounded-full"
|
||||
:class="adminUser?.is_super_admin
|
||||
? 'text-orange-500 bg-orange-100 dark:text-orange-100 dark:bg-orange-500'
|
||||
: 'text-purple-500 bg-purple-100 dark:text-purple-100 dark:bg-purple-500'">
|
||||
<span x-html="$icon(adminUser?.is_super_admin ? 'star' : 'shield', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Admin Type
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="adminUser?.is_super_admin ? 'Super Admin' : 'Platform Admin'">
|
||||
-
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Status -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 rounded-full"
|
||||
:class="adminUser?.is_active ? 'text-green-500 bg-green-100 dark:text-green-100 dark:bg-green-500' : 'text-red-500 bg-red-100 dark:text-red-100 dark:bg-red-500'">
|
||||
<span x-html="$icon(adminUser?.is_active ? 'check-circle' : 'x-circle', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Status
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="adminUser?.is_active ? 'Active' : 'Inactive'">
|
||||
-
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Platforms Access -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full dark:text-blue-100 dark:bg-blue-500">
|
||||
<span x-html="$icon('globe-alt', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Platforms
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="adminUser?.is_super_admin ? 'All' : ((adminUser?.platforms || []).length || 0)">
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Created Date -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-purple-500 bg-purple-100 rounded-full dark:text-purple-100 dark:bg-purple-500">
|
||||
<span x-html="$icon('calendar', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Created
|
||||
</p>
|
||||
<p class="text-sm font-semibold text-gray-700 dark:text-gray-200" x-text="formatDate(adminUser?.created_at)">
|
||||
-
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Info Cards -->
|
||||
<div class="grid gap-6 mb-8 md:grid-cols-2">
|
||||
<!-- Account Information -->
|
||||
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
Account Information
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Username</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300">@<span x-text="adminUser?.username || '-'"></span></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Email</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="adminUser?.email || '-'">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Email Verified</p>
|
||||
<span
|
||||
class="inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full"
|
||||
:class="adminUser?.is_email_verified ? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100' : 'text-orange-700 bg-orange-100 dark:bg-orange-700 dark:text-orange-100'"
|
||||
x-text="adminUser?.is_email_verified ? 'Verified' : 'Not Verified'">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Personal Information -->
|
||||
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
Personal Information
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Full Name</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="adminUser?.full_name || 'Not provided'">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">First Name</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="adminUser?.first_name || 'Not provided'">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Last Name</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="adminUser?.last_name || 'Not provided'">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Platform Access (for platform admins) -->
|
||||
<template x-if="!adminUser?.is_super_admin">
|
||||
<div class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
Platform Access
|
||||
</h3>
|
||||
<template x-if="(adminUser?.platforms || []).length === 0">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">No platforms assigned. This admin cannot access any platform.</p>
|
||||
</template>
|
||||
<template x-if="(adminUser?.platforms || []).length > 0">
|
||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<template x-for="platform in (adminUser?.platforms || [])" :key="platform.id">
|
||||
<div class="flex items-center p-3 bg-gray-50 rounded-lg dark:bg-gray-700">
|
||||
<div class="p-2 mr-3 text-blue-500 bg-blue-100 rounded-full dark:text-blue-100 dark:bg-blue-600">
|
||||
<span x-html="$icon('globe-alt', 'w-4 h-4')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-700 dark:text-gray-200" x-text="platform.name"></p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400" x-text="platform.code"></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Super Admin Notice -->
|
||||
<template x-if="adminUser?.is_super_admin">
|
||||
<div class="px-4 py-3 mb-8 bg-orange-50 dark:bg-orange-900/20 rounded-lg border border-orange-200 dark:border-orange-800">
|
||||
<div class="flex items-center">
|
||||
<span x-html="$icon('star', 'w-5 h-5 text-orange-500 mr-3')"></span>
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-orange-800 dark:text-orange-300">Super Admin Access</h4>
|
||||
<p class="text-sm text-orange-600 dark:text-orange-400">
|
||||
This user has full access to all platforms and can manage other admin users.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Activity Information -->
|
||||
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
Activity Information
|
||||
</h3>
|
||||
<div class="grid gap-6 md:grid-cols-3">
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Last Login</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="adminUser?.last_login ? formatDate(adminUser.last_login) : 'Never'">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Created At</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="formatDate(adminUser?.created_at)">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Last Updated</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="formatDate(adminUser?.updated_at)">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script src="{{ url_for('static', path='admin/js/admin-user-detail.js') }}"></script>
|
||||
{% endblock %}
|
||||
262
app/templates/admin/admin-users.html
Normal file
262
app/templates/admin/admin-users.html
Normal file
@@ -0,0 +1,262 @@
|
||||
{# app/templates/admin/admin-users.html #}
|
||||
{% extends "admin/base.html" %}
|
||||
{% from 'shared/macros/pagination.html' import pagination %}
|
||||
{% from 'shared/macros/headers.html' import page_header %}
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
{% from 'shared/macros/tables.html' import table_wrapper, table_header %}
|
||||
|
||||
{% block title %}Admin Users{% endblock %}
|
||||
|
||||
{% block alpine_data %}adminUsersPage(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ page_header('Admin User Management', subtitle='Manage super admins and platform admins', action_label='Create Admin User', action_url='/admin/admin-users/create', action_icon='user-plus') }}
|
||||
|
||||
{{ loading_state('Loading admin users...') }}
|
||||
|
||||
{{ error_state('Error loading admin users') }}
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div x-show="!loading" class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
|
||||
<!-- Card: Total Admin Users -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-purple-500 bg-purple-100 rounded-full dark:text-purple-100 dark:bg-purple-500">
|
||||
<span x-html="$icon('shield', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Total Admins
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.total_admins || 0">
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Super Admins -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-orange-500 bg-orange-100 rounded-full dark:text-orange-100 dark:bg-orange-500">
|
||||
<span x-html="$icon('star', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Super Admins
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.super_admins || 0">
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Platform Admins -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full dark:text-blue-100 dark:bg-blue-500">
|
||||
<span x-html="$icon('globe-alt', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Platform Admins
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.platform_admins || 0">
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Active Admins -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-green-500 bg-green-100 rounded-full dark:text-green-100 dark:bg-green-500">
|
||||
<span x-html="$icon('user-check', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Active
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.active_admins || 0">
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filters -->
|
||||
<div x-show="!loading" class="mb-6 px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<!-- Search Bar -->
|
||||
<div class="flex-1 max-w-md">
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
x-model="filters.search"
|
||||
@input="debouncedSearch()"
|
||||
placeholder="Search by name, email, or username..."
|
||||
class="w-full pl-10 pr-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:bg-gray-700 dark:text-gray-300"
|
||||
>
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<span x-html="$icon('search', 'w-5 h-5 text-gray-400')"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<!-- Admin Type Filter -->
|
||||
<select
|
||||
x-model="filters.is_super_admin"
|
||||
@change="pagination.page = 1; loadAdminUsers()"
|
||||
class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none"
|
||||
>
|
||||
<option value="">All Admin Types</option>
|
||||
<option value="true">Super Admins</option>
|
||||
<option value="false">Platform Admins</option>
|
||||
</select>
|
||||
|
||||
<!-- Status Filter -->
|
||||
<select
|
||||
x-model="filters.is_active"
|
||||
@change="pagination.page = 1; loadAdminUsers()"
|
||||
class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none"
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="true">Active</option>
|
||||
<option value="false">Inactive</option>
|
||||
</select>
|
||||
|
||||
<!-- Refresh Button -->
|
||||
<button
|
||||
@click="loadAdminUsers(); loadStats()"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none transition-colors"
|
||||
title="Refresh admin users"
|
||||
>
|
||||
<span x-html="$icon('refresh', 'w-4 h-4 mr-2')"></span>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Admin Users Table -->
|
||||
<div x-show="!loading">
|
||||
{% call table_wrapper() %}
|
||||
{{ table_header(['Admin', 'Email', 'Type', 'Platforms', 'Status', 'Last Login', 'Actions']) }}
|
||||
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
||||
<!-- Empty State -->
|
||||
<template x-if="adminUsers.length === 0">
|
||||
<tr>
|
||||
<td colspan="7" class="px-4 py-8 text-center text-gray-600 dark:text-gray-400">
|
||||
<div class="flex flex-col items-center">
|
||||
<span x-html="$icon('shield', 'w-12 h-12 text-gray-400 mb-4')"></span>
|
||||
<p class="text-lg font-medium">No admin users found</p>
|
||||
<p class="text-sm" x-text="filters.search ? 'Try adjusting your search or filters' : 'Create your first admin user to get started'"></p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Admin User Rows -->
|
||||
<template x-for="admin in adminUsers" :key="admin.id">
|
||||
<tr class="text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<!-- Admin Info -->
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center text-sm">
|
||||
<div class="relative hidden w-8 h-8 mr-3 rounded-full md:block">
|
||||
<div class="absolute inset-0 rounded-full flex items-center justify-center text-white font-semibold text-sm"
|
||||
:class="admin.is_super_admin ? 'bg-orange-500' : 'bg-purple-500'"
|
||||
x-text="(admin.username || 'A').charAt(0).toUpperCase()">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold" x-text="admin.username"></p>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400" x-text="admin.full_name || ''"></p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Email -->
|
||||
<td class="px-4 py-3 text-sm" x-text="admin.email"></td>
|
||||
|
||||
<!-- Type -->
|
||||
<td class="px-4 py-3 text-xs">
|
||||
<span class="px-2 py-1 font-semibold leading-tight rounded-full"
|
||||
:class="admin.is_super_admin
|
||||
? 'text-orange-700 bg-orange-100 dark:bg-orange-700 dark:text-orange-100'
|
||||
: 'text-purple-700 bg-purple-100 dark:bg-purple-700 dark:text-purple-100'"
|
||||
x-text="admin.is_super_admin ? 'Super Admin' : 'Platform Admin'">
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Platforms -->
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<template x-if="admin.is_super_admin">
|
||||
<span class="text-gray-500 dark:text-gray-400 italic">All platforms</span>
|
||||
</template>
|
||||
<template x-if="!admin.is_super_admin">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<template x-for="platform in (admin.platforms || []).slice(0, 3)" :key="platform.id">
|
||||
<span class="px-2 py-0.5 text-xs bg-gray-100 dark:bg-gray-700 rounded" x-text="platform.code"></span>
|
||||
</template>
|
||||
<template x-if="(admin.platforms || []).length > 3">
|
||||
<span class="px-2 py-0.5 text-xs text-gray-500" x-text="'+' + ((admin.platforms || []).length - 3) + ' more'"></span>
|
||||
</template>
|
||||
<template x-if="(admin.platforms || []).length === 0">
|
||||
<span class="text-gray-500 dark:text-gray-400 italic">None assigned</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</td>
|
||||
|
||||
<!-- Status -->
|
||||
<td class="px-4 py-3 text-xs">
|
||||
<span class="px-2 py-1 font-semibold leading-tight rounded-full"
|
||||
:class="admin.is_active ? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100' : 'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100'"
|
||||
x-text="admin.is_active ? 'Active' : 'Inactive'">
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Last Login -->
|
||||
<td class="px-4 py-3 text-sm" x-text="admin.last_login ? formatDate(admin.last_login) : 'Never'"></td>
|
||||
|
||||
<!-- Actions -->
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center space-x-2 text-sm">
|
||||
<!-- View Button -->
|
||||
<a
|
||||
:href="'/admin/admin-users/' + admin.id"
|
||||
class="flex items-center justify-center p-2 text-blue-600 rounded-lg hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
|
||||
title="View admin user"
|
||||
>
|
||||
<span x-html="$icon('eye', 'w-5 h-5')"></span>
|
||||
</a>
|
||||
|
||||
<!-- Edit Button -->
|
||||
<a
|
||||
:href="'/admin/admin-users/' + admin.id + '/edit'"
|
||||
class="flex items-center justify-center p-2 text-purple-600 rounded-lg hover:bg-purple-50 dark:text-purple-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
|
||||
title="Edit admin user"
|
||||
>
|
||||
<span x-html="$icon('edit', 'w-5 h-5')"></span>
|
||||
</a>
|
||||
|
||||
<!-- Delete Button (disabled for self) -->
|
||||
<button
|
||||
@click="deleteAdminUser(admin)"
|
||||
:disabled="admin.id === currentUserId"
|
||||
class="flex items-center justify-center p-2 text-red-600 rounded-lg hover:bg-red-50 dark:text-red-400 dark:hover:bg-gray-700 focus:outline-none transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:title="admin.id === currentUserId ? 'Cannot delete yourself' : 'Delete admin user'"
|
||||
>
|
||||
<span x-html="$icon('delete', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
{% endcall %}
|
||||
|
||||
{{ pagination() }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script src="{{ url_for('static', path='admin/js/admin-users.js') }}"></script>
|
||||
{% endblock %}
|
||||
@@ -66,12 +66,21 @@
|
||||
{{ menu_item('dashboard', '/admin/dashboard', 'home', 'Dashboard') }}
|
||||
</ul>
|
||||
|
||||
<!-- Super Admin Section (only visible to super admins) -->
|
||||
<template x-if="isSuperAdmin">
|
||||
<div>
|
||||
{{ section_header('Super Admin', 'superAdmin') }}
|
||||
{% call section_content('superAdmin') %}
|
||||
{{ menu_item('admin-users', '/admin/admin-users', 'shield', 'Admin Users') }}
|
||||
{% endcall %}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Platform Administration Section -->
|
||||
{{ section_header('Platform Administration', 'platformAdmin') }}
|
||||
{% call section_content('platformAdmin') %}
|
||||
{{ menu_item('companies', '/admin/companies', 'office-building', 'Companies') }}
|
||||
{{ menu_item('vendors', '/admin/vendors', 'shopping-bag', 'Vendors') }}
|
||||
{{ menu_item('users', '/admin/users', 'users', 'Users') }}
|
||||
{{ menu_item('messages', '/admin/messages', 'chat-bubble-left-right', 'Messages') }}
|
||||
{% endcall %}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{# app/templates/admin/select-platform.html #}
|
||||
{# standalone - This template does not extend base.html because it's shown before platform selection #}
|
||||
<!DOCTYPE html>
|
||||
<html :class="{ 'dark': dark }" x-data="selectPlatform()" lang="en">
|
||||
<head>
|
||||
@@ -28,10 +29,7 @@
|
||||
|
||||
<!-- Loading State -->
|
||||
<div x-show="loading" class="flex justify-center py-8">
|
||||
<svg class="animate-spin h-8 w-8 text-purple-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span x-html="$icon('spinner', 'h-8 w-8 text-purple-600')"></span>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
@@ -72,9 +70,7 @@
|
||||
|
||||
<!-- Arrow -->
|
||||
<div class="flex-shrink-0 ml-4">
|
||||
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
<span x-html="$icon('chevron-right', 'w-5 h-5 text-gray-400')"></span>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
@@ -82,9 +78,7 @@
|
||||
|
||||
<!-- No Platforms -->
|
||||
<div x-show="!loading && !isSuperAdmin && platforms.length === 0" x-cloak class="text-center py-8">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
||||
</svg>
|
||||
<span x-html="$icon('exclamation', 'mx-auto h-12 w-12 text-gray-400')"></span>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-gray-200">No platforms assigned</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Contact your administrator to get platform access.</p>
|
||||
</div>
|
||||
@@ -106,12 +100,8 @@
|
||||
class="p-2 text-gray-500 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none"
|
||||
aria-label="Toggle dark mode"
|
||||
>
|
||||
<svg x-show="!dark" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
|
||||
</svg>
|
||||
<svg x-show="dark" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span x-show="!dark" x-html="$icon('moon', 'w-5 h-5')"></span>
|
||||
<span x-show="dark" x-html="$icon('sun', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,6 +114,7 @@
|
||||
<script src="{{ url_for('static', path='shared/js/log-config.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='shared/js/utils.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='shared/js/api-client.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='shared/js/icons.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='admin/js/select-platform.js') }}"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
{% extends "admin/base.html" %}
|
||||
{% from 'shared/macros/headers.html' import page_header %}
|
||||
|
||||
{% block title %}Create User{% endblock %}
|
||||
{% block title %}Create Admin User{% endblock %}
|
||||
|
||||
{% block alpine_data %}adminUserCreate(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ page_header('Create New User', subtitle='Add a new user to the platform', back_url='/admin/users', back_label='Back to Users') }}
|
||||
{{ page_header('Create Admin User', subtitle='Add a new admin user to manage platforms', back_url='/admin/admin-users', back_label='Back to Admin Users') }}
|
||||
|
||||
<!-- Create Form -->
|
||||
<form @submit.prevent="handleSubmit" class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
@@ -74,29 +74,9 @@
|
||||
<span x-show="errors.password" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="errors.password"></span>
|
||||
</label>
|
||||
|
||||
<!-- Role -->
|
||||
<label class="block mb-4 text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400">
|
||||
Role <span class="text-red-600">*</span>
|
||||
</span>
|
||||
<select
|
||||
x-model="formData.role"
|
||||
@change="onRoleChange()"
|
||||
:disabled="saving"
|
||||
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-select"
|
||||
>
|
||||
<option value="vendor">Vendor</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||
Vendor: Can own companies and manage stores. Admin: Platform management access.
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<!-- Admin-specific options -->
|
||||
<template x-if="formData.role === 'admin'">
|
||||
<div class="mt-4 p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
|
||||
<h4 class="text-sm font-medium text-purple-800 dark:text-purple-300 mb-3">Admin Settings</h4>
|
||||
<!-- Admin Settings -->
|
||||
<div class="mt-4 p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
|
||||
<h4 class="text-sm font-medium text-purple-800 dark:text-purple-300 mb-3">Admin Settings</h4>
|
||||
|
||||
<!-- Super Admin Toggle -->
|
||||
<label class="flex items-center mb-4">
|
||||
@@ -114,37 +94,36 @@
|
||||
Super admins have access to all platforms and can manage other admins.
|
||||
</p>
|
||||
|
||||
<!-- Platform Assignment (only if not super admin) -->
|
||||
<template x-if="!formData.is_super_admin">
|
||||
<div>
|
||||
<label class="block text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400">
|
||||
Assigned Platforms <span class="text-red-600">*</span>
|
||||
</span>
|
||||
<div class="mt-2 space-y-2 max-h-48 overflow-y-auto">
|
||||
<template x-for="platform in platforms" :key="platform.id">
|
||||
<label class="flex items-center p-2 rounded hover:bg-purple-100 dark:hover:bg-purple-900/30 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="platform.id"
|
||||
x-model="formData.platform_ids"
|
||||
:disabled="saving"
|
||||
class="w-4 h-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
>
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300" x-text="platform.name"></span>
|
||||
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400" x-text="'(' + platform.code + ')'"></span>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
<p x-show="platforms.length === 0" class="text-sm text-gray-500 dark:text-gray-400 mt-2">
|
||||
No platforms available. Create a platform first.
|
||||
</p>
|
||||
<span x-show="errors.platform_ids" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="errors.platform_ids"></span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Platform Assignment (only if not super admin) -->
|
||||
<template x-if="!formData.is_super_admin">
|
||||
<div>
|
||||
<label class="block text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400">
|
||||
Assigned Platforms <span class="text-red-600">*</span>
|
||||
</span>
|
||||
<div class="mt-2 space-y-2 max-h-48 overflow-y-auto">
|
||||
<template x-for="platform in platforms" :key="platform.id">
|
||||
<label class="flex items-center p-2 rounded hover:bg-purple-100 dark:hover:bg-purple-900/30 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="platform.id"
|
||||
x-model="formData.platform_ids"
|
||||
:disabled="saving"
|
||||
class="w-4 h-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
>
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300" x-text="platform.name"></span>
|
||||
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400" x-text="'(' + platform.code + ')'"></span>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
<p x-show="platforms.length === 0" class="text-sm text-gray-500 dark:text-gray-400 mt-2">
|
||||
No platforms available. Create a platform first.
|
||||
</p>
|
||||
<span x-show="errors.platform_ids" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="errors.platform_ids"></span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Personal Info -->
|
||||
@@ -188,7 +167,7 @@
|
||||
<!-- Submit Button -->
|
||||
<div class="flex items-center justify-end gap-3 pt-6 border-t dark:border-gray-700">
|
||||
<a
|
||||
href="/admin/users"
|
||||
href="/admin/admin-users"
|
||||
class="px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition-colors duration-150 bg-white border border-gray-300 rounded-lg dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800 hover:border-gray-400 focus:outline-none">
|
||||
Cancel
|
||||
</a>
|
||||
@@ -198,7 +177,7 @@
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<span x-show="!saving">
|
||||
<span x-html="$icon('user-plus', 'w-4 h-4 mr-2 inline')"></span>
|
||||
Create User
|
||||
Create Admin User
|
||||
</span>
|
||||
<span x-show="saving" class="flex items-center">
|
||||
<span x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
||||
|
||||
196
static/admin/js/admin-user-detail.js
Normal file
196
static/admin/js/admin-user-detail.js
Normal file
@@ -0,0 +1,196 @@
|
||||
// noqa: js-006 - async init pattern is safe, loadData has try/catch
|
||||
// static/admin/js/admin-user-detail.js
|
||||
|
||||
// Create custom logger for admin user detail
|
||||
const adminUserDetailLog = window.LogConfig.createLogger('ADMIN-USER-DETAIL');
|
||||
|
||||
function adminUserDetailPage() {
|
||||
return {
|
||||
// Inherit base layout functionality from init-alpine.js
|
||||
...data(),
|
||||
|
||||
// Admin user detail page specific state
|
||||
currentPage: 'admin-users',
|
||||
adminUser: null,
|
||||
loading: false,
|
||||
saving: false,
|
||||
error: null,
|
||||
userId: null,
|
||||
currentUserId: null,
|
||||
|
||||
// Initialize
|
||||
async init() {
|
||||
adminUserDetailLog.info('=== ADMIN USER DETAIL PAGE INITIALIZING ===');
|
||||
|
||||
// Prevent multiple initializations
|
||||
if (window._adminUserDetailInitialized) {
|
||||
adminUserDetailLog.warn('Admin user detail page already initialized, skipping...');
|
||||
return;
|
||||
}
|
||||
window._adminUserDetailInitialized = true;
|
||||
|
||||
// Get current user ID
|
||||
this.currentUserId = this.adminProfile?.id || null;
|
||||
|
||||
// Get user ID from URL
|
||||
const path = window.location.pathname;
|
||||
const match = path.match(/\/admin\/admin-users\/(\d+)$/);
|
||||
|
||||
if (match) {
|
||||
this.userId = match[1];
|
||||
adminUserDetailLog.info('Viewing admin user:', this.userId);
|
||||
await this.loadAdminUser();
|
||||
} else {
|
||||
adminUserDetailLog.error('No user ID in URL');
|
||||
this.error = 'Invalid admin user URL';
|
||||
Utils.showToast('Invalid admin user URL', 'error');
|
||||
}
|
||||
|
||||
adminUserDetailLog.info('=== ADMIN USER DETAIL PAGE INITIALIZATION COMPLETE ===');
|
||||
},
|
||||
|
||||
// Load admin user data
|
||||
async loadAdminUser() {
|
||||
adminUserDetailLog.info('Loading admin user details...');
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const url = `/admin/admin-users/${this.userId}`;
|
||||
window.LogConfig.logApiCall('GET', url, null, 'request');
|
||||
|
||||
const startTime = performance.now();
|
||||
const response = await apiClient.get(url);
|
||||
const duration = performance.now() - startTime;
|
||||
|
||||
window.LogConfig.logApiCall('GET', url, response, 'response');
|
||||
window.LogConfig.logPerformance('Load Admin User Details', duration);
|
||||
|
||||
// Transform API response to expected format
|
||||
this.adminUser = {
|
||||
...response,
|
||||
platforms: (response.platform_assignments || []).map(pa => ({
|
||||
id: pa.platform_id,
|
||||
code: pa.platform_code,
|
||||
name: pa.platform_name
|
||||
})),
|
||||
full_name: [response.first_name, response.last_name].filter(Boolean).join(' ') || null
|
||||
};
|
||||
|
||||
adminUserDetailLog.info(`Admin user loaded in ${duration}ms`, {
|
||||
id: this.adminUser.id,
|
||||
username: this.adminUser.username,
|
||||
is_super_admin: this.adminUser.is_super_admin,
|
||||
is_active: this.adminUser.is_active
|
||||
});
|
||||
adminUserDetailLog.debug('Full admin user data:', this.adminUser);
|
||||
|
||||
} catch (error) {
|
||||
window.LogConfig.logError(error, 'Load Admin User Details');
|
||||
this.error = error.message || 'Failed to load admin user details';
|
||||
Utils.showToast('Failed to load admin user details', 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Format date
|
||||
formatDate(dateString) {
|
||||
if (!dateString) {
|
||||
return '-';
|
||||
}
|
||||
return Utils.formatDate(dateString);
|
||||
},
|
||||
|
||||
// Toggle admin user status
|
||||
async toggleStatus() {
|
||||
const action = this.adminUser.is_active ? 'deactivate' : 'activate';
|
||||
adminUserDetailLog.info(`Toggle status: ${action}`);
|
||||
|
||||
// Prevent self-deactivation
|
||||
if (this.adminUser.id === this.currentUserId) {
|
||||
Utils.showToast('You cannot deactivate your own account', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Are you sure you want to ${action} "${this.adminUser.username}"?`)) {
|
||||
adminUserDetailLog.info('Status toggle cancelled by user');
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
const url = `/admin/admin-users/${this.userId}/status`;
|
||||
window.LogConfig.logApiCall('PUT', url, null, 'request');
|
||||
|
||||
const response = await apiClient.put(url);
|
||||
|
||||
window.LogConfig.logApiCall('PUT', url, response, 'response');
|
||||
|
||||
this.adminUser.is_active = response.is_active;
|
||||
Utils.showToast(`Admin user ${action}d successfully`, 'success');
|
||||
adminUserDetailLog.info(`Admin user ${action}d successfully`);
|
||||
|
||||
} catch (error) {
|
||||
window.LogConfig.logError(error, `Toggle Status (${action})`);
|
||||
Utils.showToast(error.message || `Failed to ${action} admin user`, 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Delete admin user
|
||||
async deleteAdminUser() {
|
||||
adminUserDetailLog.info('Delete admin user requested:', this.userId);
|
||||
|
||||
// Prevent self-deletion
|
||||
if (this.adminUser.id === this.currentUserId) {
|
||||
Utils.showToast('You cannot delete your own account', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Are you sure you want to delete admin user "${this.adminUser.username}"?\n\nThis action cannot be undone.`)) {
|
||||
adminUserDetailLog.info('Delete cancelled by user');
|
||||
return;
|
||||
}
|
||||
|
||||
// Second confirmation for safety
|
||||
if (!confirm(`FINAL CONFIRMATION\n\nAre you absolutely sure you want to delete "${this.adminUser.username}"?`)) {
|
||||
adminUserDetailLog.info('Delete cancelled by user (second confirmation)');
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
const url = `/admin/admin-users/${this.userId}`;
|
||||
window.LogConfig.logApiCall('DELETE', url, null, 'request');
|
||||
|
||||
await apiClient.delete(url);
|
||||
|
||||
window.LogConfig.logApiCall('DELETE', url, null, 'response');
|
||||
|
||||
Utils.showToast('Admin user deleted successfully', 'success');
|
||||
adminUserDetailLog.info('Admin user deleted successfully');
|
||||
|
||||
// Redirect to admin users list
|
||||
setTimeout(() => window.location.href = '/admin/admin-users', 1500);
|
||||
|
||||
} catch (error) {
|
||||
window.LogConfig.logError(error, 'Delete Admin User');
|
||||
Utils.showToast(error.message || 'Failed to delete admin user', 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Refresh admin user data
|
||||
async refresh() {
|
||||
adminUserDetailLog.info('=== ADMIN USER REFRESH TRIGGERED ===');
|
||||
await this.loadAdminUser();
|
||||
Utils.showToast('Admin user details refreshed', 'success');
|
||||
adminUserDetailLog.info('=== ADMIN USER REFRESH COMPLETE ===');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
adminUserDetailLog.info('Admin user detail module loaded');
|
||||
330
static/admin/js/admin-users.js
Normal file
330
static/admin/js/admin-users.js
Normal file
@@ -0,0 +1,330 @@
|
||||
// noqa: js-006 - async init pattern is safe, loadData has try/catch
|
||||
// static/admin/js/admin-users.js
|
||||
|
||||
// Create custom logger for admin users
|
||||
const adminUsersLog = window.LogConfig.createLogger('ADMIN-USERS');
|
||||
|
||||
function adminUsersPage() {
|
||||
return {
|
||||
// Inherit base layout functionality
|
||||
...data(),
|
||||
|
||||
// Set page identifier
|
||||
currentPage: 'admin-users',
|
||||
|
||||
// State
|
||||
adminUsers: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
currentUserId: null,
|
||||
filters: {
|
||||
search: '',
|
||||
is_super_admin: '',
|
||||
is_active: ''
|
||||
},
|
||||
stats: {
|
||||
total_admins: 0,
|
||||
super_admins: 0,
|
||||
platform_admins: 0,
|
||||
active_admins: 0
|
||||
},
|
||||
pagination: {
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 0,
|
||||
pages: 0
|
||||
},
|
||||
|
||||
// Initialization
|
||||
async init() {
|
||||
adminUsersLog.info('=== ADMIN USERS PAGE INITIALIZING ===');
|
||||
|
||||
// Prevent multiple initializations
|
||||
if (window._adminUsersInitialized) {
|
||||
adminUsersLog.warn('Admin users page already initialized, skipping...');
|
||||
return;
|
||||
}
|
||||
window._adminUsersInitialized = true;
|
||||
|
||||
// Get current user ID
|
||||
this.currentUserId = this.adminProfile?.id || null;
|
||||
|
||||
// Load platform settings for rows per page
|
||||
if (window.PlatformSettings) {
|
||||
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
|
||||
}
|
||||
|
||||
await this.loadAdminUsers();
|
||||
await this.loadStats();
|
||||
|
||||
adminUsersLog.info('=== ADMIN USERS PAGE INITIALIZATION COMPLETE ===');
|
||||
},
|
||||
|
||||
// Format date helper
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
return Utils.formatDate(dateString);
|
||||
},
|
||||
|
||||
// Computed: Total number of pages
|
||||
get totalPages() {
|
||||
return this.pagination.pages;
|
||||
},
|
||||
|
||||
// Computed: Start index for pagination display
|
||||
get startIndex() {
|
||||
if (this.pagination.total === 0) return 0;
|
||||
return (this.pagination.page - 1) * this.pagination.per_page + 1;
|
||||
},
|
||||
|
||||
// Computed: End index for pagination display
|
||||
get endIndex() {
|
||||
const end = this.pagination.page * this.pagination.per_page;
|
||||
return end > this.pagination.total ? this.pagination.total : end;
|
||||
},
|
||||
|
||||
// Computed: Generate page numbers array with ellipsis
|
||||
get pageNumbers() {
|
||||
const pages = [];
|
||||
const totalPages = this.totalPages;
|
||||
const current = this.pagination.page;
|
||||
|
||||
if (totalPages <= 7) {
|
||||
// Show all pages if 7 or fewer
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
// Always show first page
|
||||
pages.push(1);
|
||||
|
||||
if (current > 3) {
|
||||
pages.push('...');
|
||||
}
|
||||
|
||||
// Show pages around current page
|
||||
const start = Math.max(2, current - 1);
|
||||
const end = Math.min(totalPages - 1, current + 1);
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
if (current < totalPages - 2) {
|
||||
pages.push('...');
|
||||
}
|
||||
|
||||
// Always show last page
|
||||
pages.push(totalPages);
|
||||
}
|
||||
|
||||
return pages;
|
||||
},
|
||||
|
||||
// Load admin users from API
|
||||
async loadAdminUsers() {
|
||||
adminUsersLog.info('Loading admin users...');
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
// Calculate skip for pagination
|
||||
const skip = (this.pagination.page - 1) * this.pagination.per_page;
|
||||
params.append('skip', skip);
|
||||
params.append('limit', this.pagination.per_page);
|
||||
|
||||
if (this.filters.search) {
|
||||
params.append('search', this.filters.search);
|
||||
}
|
||||
if (this.filters.is_super_admin === 'false') {
|
||||
params.append('include_super_admins', 'false');
|
||||
}
|
||||
if (this.filters.is_active !== '') {
|
||||
params.append('is_active', this.filters.is_active);
|
||||
}
|
||||
|
||||
const url = `/admin/admin-users?${params}`;
|
||||
window.LogConfig.logApiCall('GET', url, null, 'request');
|
||||
|
||||
const startTime = performance.now();
|
||||
const response = await apiClient.get(url);
|
||||
const duration = performance.now() - startTime;
|
||||
|
||||
window.LogConfig.logApiCall('GET', url, response, 'response');
|
||||
window.LogConfig.logPerformance('Load Admin Users', duration);
|
||||
|
||||
// Transform API response to expected format
|
||||
let admins = response.admins || [];
|
||||
|
||||
// Apply client-side filtering for search and super admin status
|
||||
if (this.filters.search) {
|
||||
const searchLower = this.filters.search.toLowerCase();
|
||||
admins = admins.filter(admin =>
|
||||
admin.username?.toLowerCase().includes(searchLower) ||
|
||||
admin.email?.toLowerCase().includes(searchLower) ||
|
||||
admin.first_name?.toLowerCase().includes(searchLower) ||
|
||||
admin.last_name?.toLowerCase().includes(searchLower)
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by super admin status
|
||||
if (this.filters.is_super_admin === 'true') {
|
||||
admins = admins.filter(admin => admin.is_super_admin);
|
||||
}
|
||||
|
||||
// Filter by active status
|
||||
if (this.filters.is_active !== '') {
|
||||
const isActive = this.filters.is_active === 'true';
|
||||
admins = admins.filter(admin => admin.is_active === isActive);
|
||||
}
|
||||
|
||||
// Transform platform_assignments to platforms for template
|
||||
this.adminUsers = admins.map(admin => ({
|
||||
...admin,
|
||||
platforms: (admin.platform_assignments || []).map(pa => ({
|
||||
id: pa.platform_id,
|
||||
code: pa.platform_code,
|
||||
name: pa.platform_name
|
||||
})),
|
||||
full_name: [admin.first_name, admin.last_name].filter(Boolean).join(' ') || null
|
||||
}));
|
||||
|
||||
this.pagination.total = response.total || this.adminUsers.length;
|
||||
this.pagination.pages = Math.ceil(this.pagination.total / this.pagination.per_page) || 1;
|
||||
|
||||
adminUsersLog.info(`Loaded ${this.adminUsers.length} admin users`);
|
||||
} catch (error) {
|
||||
window.LogConfig.logError(error, 'Load Admin Users');
|
||||
this.error = error.message || 'Failed to load admin users';
|
||||
Utils.showToast('Failed to load admin users', 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Load statistics (computed from admin users data)
|
||||
async loadStats() {
|
||||
adminUsersLog.info('Loading admin user statistics...');
|
||||
|
||||
try {
|
||||
// Fetch all admin users to compute stats
|
||||
const url = '/admin/admin-users?skip=0&limit=1000';
|
||||
window.LogConfig.logApiCall('GET', url, null, 'request');
|
||||
|
||||
const response = await apiClient.get(url);
|
||||
|
||||
window.LogConfig.logApiCall('GET', url, response, 'response');
|
||||
|
||||
const admins = response.admins || [];
|
||||
|
||||
// Compute stats from the data
|
||||
this.stats = {
|
||||
total_admins: admins.length,
|
||||
super_admins: admins.filter(a => a.is_super_admin).length,
|
||||
platform_admins: admins.filter(a => !a.is_super_admin).length,
|
||||
active_admins: admins.filter(a => a.is_active).length
|
||||
};
|
||||
|
||||
adminUsersLog.debug('Stats computed:', this.stats);
|
||||
} catch (error) {
|
||||
window.LogConfig.logError(error, 'Load Admin Stats');
|
||||
// Stats are non-critical, don't show error toast
|
||||
}
|
||||
},
|
||||
|
||||
// Search with debounce
|
||||
debouncedSearch() {
|
||||
// Clear existing timeout
|
||||
if (this._searchTimeout) {
|
||||
clearTimeout(this._searchTimeout);
|
||||
}
|
||||
// Set new timeout
|
||||
this._searchTimeout = setTimeout(() => {
|
||||
adminUsersLog.info('Search triggered:', this.filters.search);
|
||||
this.pagination.page = 1;
|
||||
this.loadAdminUsers();
|
||||
}, 300);
|
||||
},
|
||||
|
||||
// Pagination
|
||||
nextPage() {
|
||||
if (this.pagination.page < this.pagination.pages) {
|
||||
this.pagination.page++;
|
||||
adminUsersLog.info('Next page:', this.pagination.page);
|
||||
this.loadAdminUsers();
|
||||
}
|
||||
},
|
||||
|
||||
previousPage() {
|
||||
if (this.pagination.page > 1) {
|
||||
this.pagination.page--;
|
||||
adminUsersLog.info('Previous page:', this.pagination.page);
|
||||
this.loadAdminUsers();
|
||||
}
|
||||
},
|
||||
|
||||
goToPage(pageNum) {
|
||||
if (pageNum !== '...' && pageNum >= 1 && pageNum <= this.totalPages) {
|
||||
this.pagination.page = pageNum;
|
||||
adminUsersLog.info('Go to page:', this.pagination.page);
|
||||
this.loadAdminUsers();
|
||||
}
|
||||
},
|
||||
|
||||
// Actions
|
||||
viewAdminUser(admin) {
|
||||
adminUsersLog.info('View admin user:', admin.username);
|
||||
window.location.href = `/admin/admin-users/${admin.id}`;
|
||||
},
|
||||
|
||||
editAdminUser(admin) {
|
||||
adminUsersLog.info('Edit admin user:', admin.username);
|
||||
window.location.href = `/admin/admin-users/${admin.id}/edit`;
|
||||
},
|
||||
|
||||
async deleteAdminUser(admin) {
|
||||
adminUsersLog.warn('Delete admin user requested:', admin.username);
|
||||
|
||||
// Prevent self-deletion
|
||||
if (admin.id === this.currentUserId) {
|
||||
Utils.showToast('You cannot delete your own account', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Are you sure you want to delete admin user "${admin.username}"?\n\nThis action cannot be undone.`)) {
|
||||
adminUsersLog.info('Delete cancelled by user');
|
||||
return;
|
||||
}
|
||||
|
||||
// Second confirmation for safety
|
||||
if (!confirm(`FINAL CONFIRMATION\n\nAre you absolutely sure you want to delete "${admin.username}"?`)) {
|
||||
adminUsersLog.info('Delete cancelled by user (second confirmation)');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = `/admin/admin-users/${admin.id}`;
|
||||
window.LogConfig.logApiCall('DELETE', url, null, 'request');
|
||||
|
||||
await apiClient.delete(url);
|
||||
|
||||
Utils.showToast('Admin user deleted successfully', 'success');
|
||||
adminUsersLog.info('Admin user deleted successfully');
|
||||
|
||||
await this.loadAdminUsers();
|
||||
await this.loadStats();
|
||||
} catch (error) {
|
||||
window.LogConfig.logError(error, 'Delete Admin User');
|
||||
Utils.showToast(error.message || 'Failed to delete admin user', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
openCreateModal() {
|
||||
adminUsersLog.info('Open create admin user page');
|
||||
window.location.href = '/admin/admin-users/create';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
adminUsersLog.info('Admin users module loaded');
|
||||
@@ -27,9 +27,11 @@ function data() {
|
||||
|
||||
// Default state: Platform Administration open, others closed
|
||||
const defaultSections = {
|
||||
superAdmin: true, // Super admin section (only visible to super admins)
|
||||
platformAdmin: true,
|
||||
vendorOps: false,
|
||||
marketplace: false,
|
||||
billing: false,
|
||||
contentMgmt: false,
|
||||
devTools: false,
|
||||
platformHealth: false,
|
||||
@@ -75,12 +77,28 @@ function data() {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to get admin profile from localStorage
|
||||
function getAdminProfileFromStorage() {
|
||||
try {
|
||||
// Check admin_user first (set by login), then adminProfile (legacy)
|
||||
const stored = window.localStorage.getItem('admin_user') ||
|
||||
window.localStorage.getItem('adminProfile');
|
||||
if (stored) {
|
||||
return JSON.parse(stored);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse admin profile from localStorage:', e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Map pages to their parent sections
|
||||
const pageSectionMap = {
|
||||
// Super Admin section
|
||||
'admin-users': 'superAdmin',
|
||||
// Platform Administration
|
||||
companies: 'platformAdmin',
|
||||
vendors: 'platformAdmin',
|
||||
users: 'platformAdmin',
|
||||
messages: 'platformAdmin',
|
||||
// Vendor Operations (Products, Customers, Inventory, Orders, Shipping)
|
||||
'marketplace-products': 'vendorOps',
|
||||
@@ -185,7 +203,16 @@ function data() {
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// Page identifier - will be set by individual pages
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
currentPage: ''
|
||||
currentPage: '',
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// Admin profile and super admin flag
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
adminProfile: getAdminProfileFromStorage(),
|
||||
|
||||
get isSuperAdmin() {
|
||||
return this.adminProfile?.is_super_admin === true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ function adminLogin() {
|
||||
// Check if platform selection is required
|
||||
try {
|
||||
loginLog.info('Checking accessible platforms...');
|
||||
const platformsResponse = await apiClient.get('/api/v1/admin/auth/accessible-platforms');
|
||||
const platformsResponse = await apiClient.get('/admin/auth/accessible-platforms');
|
||||
loginLog.debug('Accessible platforms response:', platformsResponse);
|
||||
|
||||
if (platformsResponse.requires_platform_selection) {
|
||||
|
||||
@@ -15,6 +15,13 @@ function selectPlatform() {
|
||||
async init() {
|
||||
platformLog.info('=== PLATFORM SELECTION PAGE INITIALIZING ===');
|
||||
|
||||
// Prevent multiple initializations
|
||||
if (window._platformSelectInitialized) {
|
||||
platformLog.warn('Platform selection page already initialized, skipping...');
|
||||
return;
|
||||
}
|
||||
window._platformSelectInitialized = true;
|
||||
|
||||
// Set theme
|
||||
this.dark = localStorage.getItem('theme') === 'dark';
|
||||
|
||||
@@ -36,7 +43,7 @@ function selectPlatform() {
|
||||
|
||||
try {
|
||||
platformLog.info('Fetching accessible platforms...');
|
||||
const response = await apiClient.get('/api/v1/admin/auth/accessible-platforms');
|
||||
const response = await apiClient.get('/admin/auth/accessible-platforms');
|
||||
platformLog.debug('Platforms response:', response);
|
||||
|
||||
this.isSuperAdmin = response.is_super_admin;
|
||||
@@ -83,7 +90,7 @@ function selectPlatform() {
|
||||
|
||||
try {
|
||||
const response = await apiClient.post(
|
||||
`/api/v1/admin/auth/select-platform?platform_id=${platform.id}`
|
||||
`/admin/auth/select-platform?platform_id=${platform.id}`
|
||||
);
|
||||
|
||||
platformLog.debug('Platform selection response:', response);
|
||||
@@ -125,25 +132,20 @@ function selectPlatform() {
|
||||
}
|
||||
},
|
||||
|
||||
logout() {
|
||||
async logout() {
|
||||
platformLog.info('Logging out...');
|
||||
|
||||
fetch('/api/v1/admin/auth/logout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('admin_token')}`
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
try {
|
||||
await apiClient.post('/admin/auth/logout');
|
||||
} catch (error) {
|
||||
platformLog.error('Logout API error:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
} finally {
|
||||
localStorage.removeItem('admin_token');
|
||||
localStorage.removeItem('admin_user');
|
||||
localStorage.removeItem('admin_platform');
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = '/admin/login';
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
toggleDarkMode() {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
// static/admin/js/user-create.js
|
||||
|
||||
// Create custom logger for user create
|
||||
const userCreateLog = window.LogConfig.createLogger('USER-CREATE');
|
||||
// Create custom logger for admin user create
|
||||
const userCreateLog = window.LogConfig.createLogger('ADMIN-USER-CREATE');
|
||||
|
||||
function adminUserCreate() {
|
||||
return {
|
||||
// Inherit base layout functionality from init-alpine.js
|
||||
...data(),
|
||||
|
||||
// User create page specific state
|
||||
currentPage: 'user-create',
|
||||
// Admin user create page specific state
|
||||
currentPage: 'admin-users',
|
||||
loading: false,
|
||||
formData: {
|
||||
username: '',
|
||||
@@ -17,7 +17,6 @@ function adminUserCreate() {
|
||||
password: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
role: 'vendor',
|
||||
is_super_admin: false,
|
||||
platform_ids: []
|
||||
},
|
||||
@@ -27,11 +26,11 @@ function adminUserCreate() {
|
||||
|
||||
// Initialize
|
||||
async init() {
|
||||
userCreateLog.info('=== USER CREATE PAGE INITIALIZING ===');
|
||||
userCreateLog.info('=== ADMIN USER CREATE PAGE INITIALIZING ===');
|
||||
|
||||
// Prevent multiple initializations
|
||||
if (window._userCreateInitialized) {
|
||||
userCreateLog.warn('User create page already initialized, skipping...');
|
||||
userCreateLog.warn('Admin user create page already initialized, skipping...');
|
||||
return;
|
||||
}
|
||||
window._userCreateInitialized = true;
|
||||
@@ -39,7 +38,7 @@ function adminUserCreate() {
|
||||
// Load platforms for admin assignment
|
||||
await this.loadPlatforms();
|
||||
|
||||
userCreateLog.info('=== USER CREATE PAGE INITIALIZATION COMPLETE ===');
|
||||
userCreateLog.info('=== ADMIN USER CREATE PAGE INITIALIZATION COMPLETE ===');
|
||||
},
|
||||
|
||||
// Load available platforms
|
||||
@@ -55,16 +54,6 @@ function adminUserCreate() {
|
||||
}
|
||||
},
|
||||
|
||||
// Handle role change
|
||||
onRoleChange() {
|
||||
userCreateLog.debug('Role changed to:', this.formData.role);
|
||||
if (this.formData.role !== 'admin') {
|
||||
// Reset admin-specific fields when switching away from admin
|
||||
this.formData.is_super_admin = false;
|
||||
this.formData.platform_ids = [];
|
||||
}
|
||||
},
|
||||
|
||||
// Validate form
|
||||
validateForm() {
|
||||
this.errors = {};
|
||||
@@ -79,8 +68,8 @@ function adminUserCreate() {
|
||||
this.errors.password = 'Password must be at least 6 characters';
|
||||
}
|
||||
|
||||
// Admin-specific validation
|
||||
if (this.formData.role === 'admin' && !this.formData.is_super_admin) {
|
||||
// Platform admin validation: must have at least one platform
|
||||
if (!this.formData.is_super_admin) {
|
||||
if (!this.formData.platform_ids || this.formData.platform_ids.length === 0) {
|
||||
this.errors.platform_ids = 'Platform admins must be assigned to at least one platform';
|
||||
}
|
||||
@@ -91,7 +80,7 @@ function adminUserCreate() {
|
||||
|
||||
// Submit form
|
||||
async handleSubmit() {
|
||||
userCreateLog.info('=== CREATING USER ===');
|
||||
userCreateLog.info('=== CREATING ADMIN USER ===');
|
||||
userCreateLog.debug('Form data:', { ...this.formData, password: '[REDACTED]' });
|
||||
|
||||
if (!this.validateForm()) {
|
||||
@@ -103,55 +92,38 @@ function adminUserCreate() {
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
let url, payload, response;
|
||||
|
||||
if (this.formData.role === 'admin') {
|
||||
// Use admin-users endpoint for creating admin users
|
||||
url = '/api/v1/admin/admin-users';
|
||||
payload = {
|
||||
email: this.formData.email,
|
||||
username: this.formData.username,
|
||||
password: this.formData.password,
|
||||
first_name: this.formData.first_name || null,
|
||||
last_name: this.formData.last_name || null,
|
||||
is_super_admin: this.formData.is_super_admin,
|
||||
platform_ids: this.formData.is_super_admin ? [] : this.formData.platform_ids.map(id => parseInt(id))
|
||||
};
|
||||
} else {
|
||||
// Use regular users endpoint for vendor users
|
||||
url = '/admin/users';
|
||||
payload = {
|
||||
email: this.formData.email,
|
||||
username: this.formData.username,
|
||||
password: this.formData.password,
|
||||
first_name: this.formData.first_name || null,
|
||||
last_name: this.formData.last_name || null,
|
||||
role: this.formData.role
|
||||
};
|
||||
}
|
||||
// Use admin-users endpoint for creating admin users
|
||||
const url = '/admin/admin-users';
|
||||
const payload = {
|
||||
email: this.formData.email,
|
||||
username: this.formData.username,
|
||||
password: this.formData.password,
|
||||
first_name: this.formData.first_name || null,
|
||||
last_name: this.formData.last_name || null,
|
||||
is_super_admin: this.formData.is_super_admin,
|
||||
platform_ids: this.formData.is_super_admin ? [] : this.formData.platform_ids.map(id => parseInt(id))
|
||||
};
|
||||
|
||||
window.LogConfig.logApiCall('POST', url, { ...payload, password: '[REDACTED]' }, 'request');
|
||||
|
||||
const startTime = performance.now();
|
||||
response = await apiClient.post(url, payload);
|
||||
const response = await apiClient.post(url, payload);
|
||||
const duration = performance.now() - startTime;
|
||||
|
||||
window.LogConfig.logApiCall('POST', url, response, 'response');
|
||||
window.LogConfig.logPerformance('Create User', duration);
|
||||
window.LogConfig.logPerformance('Create Admin User', duration);
|
||||
|
||||
const userType = this.formData.role === 'admin'
|
||||
? (this.formData.is_super_admin ? 'Super admin' : 'Platform admin')
|
||||
: 'User';
|
||||
const userType = this.formData.is_super_admin ? 'Super admin' : 'Platform admin';
|
||||
Utils.showToast(`${userType} created successfully`, 'success');
|
||||
userCreateLog.info(`${userType} created successfully in ${duration}ms`, response);
|
||||
|
||||
// Redirect to the new user's detail page
|
||||
// Redirect to the admin users list
|
||||
setTimeout(() => {
|
||||
window.location.href = `/admin/users/${response.id}`;
|
||||
window.location.href = `/admin/admin-users/${response.id}`;
|
||||
}, 1500);
|
||||
|
||||
} catch (error) {
|
||||
window.LogConfig.logError(error, 'Create User');
|
||||
window.LogConfig.logError(error, 'Create Admin User');
|
||||
|
||||
// Handle validation errors
|
||||
if (error.details && error.details.validation_errors) {
|
||||
@@ -173,13 +145,13 @@ function adminUserCreate() {
|
||||
}
|
||||
}
|
||||
|
||||
Utils.showToast(error.message || 'Failed to create user', 'error');
|
||||
Utils.showToast(error.message || 'Failed to create admin user', 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
userCreateLog.info('=== USER CREATION COMPLETE ===');
|
||||
userCreateLog.info('=== ADMIN USER CREATION COMPLETE ===');
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
userCreateLog.info('User create module loaded');
|
||||
userCreateLog.info('Admin user create module loaded');
|
||||
|
||||
Reference in New Issue
Block a user