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

+ Quick Actions +

+
+ + + Edit Admin User + + + +
+
+ + +
+ +
+
+ +
+
+

+ Admin Type +

+

+ - +

+
+
+ + +
+
+ +
+
+

+ Status +

+

+ - +

+
+
+ + +
+
+ +
+
+

+ Platforms +

+

+ 0 +

+
+
+ + +
+
+ +
+
+

+ Created +

+

+ - +

+
+
+
+ + +
+ +
+

+ Account Information +

+
+
+

Username

+

@

+
+
+

Email

+

-

+
+
+

Email Verified

+ + +
+
+
+ + +
+

+ Personal Information +

+
+
+

Full Name

+

-

+
+
+

First Name

+

-

+
+
+

Last Name

+

-

+
+
+
+
+ + + + + + + + +
+

+ Activity Information +

+
+
+

Last Login

+

-

+
+
+

Created At

+

-

+
+
+

Last Updated

+

-

+
+
+
+
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} diff --git a/app/templates/admin/admin-users.html b/app/templates/admin/admin-users.html new file mode 100644 index 00000000..f9e86712 --- /dev/null +++ b/app/templates/admin/admin-users.html @@ -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') }} + + +
+ +
+
+ +
+
+

+ Total Admins +

+

+ 0 +

+
+
+ + +
+
+ +
+
+

+ Super Admins +

+

+ 0 +

+
+
+ + +
+
+ +
+
+

+ Platform Admins +

+

+ 0 +

+
+
+ + +
+
+ +
+
+

+ Active +

+

+ 0 +

+
+
+
+ + +
+
+ +
+
+ +
+ +
+
+
+ + +
+ + + + + + + + +
+
+
+ + +
+ {% call table_wrapper() %} + {{ table_header(['Admin', 'Email', 'Type', 'Platforms', 'Status', 'Last Login', 'Actions']) }} + + + + + + + + {% endcall %} + + {{ pagination() }} +
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} diff --git a/app/templates/admin/partials/sidebar.html b/app/templates/admin/partials/sidebar.html index 56286001..ee2b4b67 100644 --- a/app/templates/admin/partials/sidebar.html +++ b/app/templates/admin/partials/sidebar.html @@ -66,12 +66,21 @@ {{ menu_item('dashboard', '/admin/dashboard', 'home', 'Dashboard') }} + + + {{ 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 %} diff --git a/app/templates/admin/select-platform.html b/app/templates/admin/select-platform.html index 215b61c6..ffdc3124 100644 --- a/app/templates/admin/select-platform.html +++ b/app/templates/admin/select-platform.html @@ -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 #} @@ -28,10 +29,7 @@
- - - - +
@@ -72,9 +70,7 @@
- - - +
@@ -82,9 +78,7 @@
- - - +

No platforms assigned

Contact your administrator to get platform access.

@@ -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" > - - - - - - + + @@ -124,6 +114,7 @@ + diff --git a/app/templates/admin/user-create.html b/app/templates/admin/user-create.html index 212d693e..3e417830 100644 --- a/app/templates/admin/user-create.html +++ b/app/templates/admin/user-create.html @@ -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') }}
@@ -74,29 +74,9 @@ - - - - - + + + @@ -188,7 +167,7 @@
Cancel @@ -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"> - Create User + Create Admin User diff --git a/static/admin/js/admin-user-detail.js b/static/admin/js/admin-user-detail.js new file mode 100644 index 00000000..97816741 --- /dev/null +++ b/static/admin/js/admin-user-detail.js @@ -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'); diff --git a/static/admin/js/admin-users.js b/static/admin/js/admin-users.js new file mode 100644 index 00000000..a07ba541 --- /dev/null +++ b/static/admin/js/admin-users.js @@ -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'); diff --git a/static/admin/js/init-alpine.js b/static/admin/js/init-alpine.js index fe7d184c..9c1c83a7 100644 --- a/static/admin/js/init-alpine.js +++ b/static/admin/js/init-alpine.js @@ -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; + } } } diff --git a/static/admin/js/login.js b/static/admin/js/login.js index 1590adbe..58ed5516 100644 --- a/static/admin/js/login.js +++ b/static/admin/js/login.js @@ -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) { diff --git a/static/admin/js/select-platform.js b/static/admin/js/select-platform.js index b29c63c2..50e6fb0a 100644 --- a/static/admin/js/select-platform.js +++ b/static/admin/js/select-platform.js @@ -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() { diff --git a/static/admin/js/user-create.js b/static/admin/js/user-create.js index aa97f917..64ae2f90 100644 --- a/static/admin/js/user-create.js +++ b/static/admin/js/user-create.js @@ -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');