""" Auth Admin API - Users, Groups, Permissions management """ from fastapi import APIRouter, HTTPException, status, Depends from pydantic import BaseModel, Field from app.core.auth_dependencies import require_permission from app.core.auth_service import AuthService from app.core.database import execute_query, execute_query_single, execute_insert, execute_update from app.models.schemas import UserAdminCreate, UserGroupsUpdate, GroupCreate, GroupPermissionsUpdate, UserTwoFactorResetRequest import logging logger = logging.getLogger(__name__) router = APIRouter() class UserStatusUpdateRequest(BaseModel): is_active: bool class UserPasswordResetRequest(BaseModel): new_password: str = Field(..., min_length=8, max_length=128) def _users_column_exists(column_name: str) -> bool: result = execute_query_single( """ SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'users' AND column_name = %s LIMIT 1 """, (column_name,) ) return bool(result) def _table_exists(table_name: str) -> bool: result = execute_query_single( """ SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = %s LIMIT 1 """, (table_name,) ) return bool(result) @router.get("/admin/users", dependencies=[Depends(require_permission("users.manage"))]) async def list_users(): is_2fa_expr = "u.is_2fa_enabled" if _users_column_exists("is_2fa_enabled") else "FALSE AS is_2fa_enabled" telefoni_extension_expr = "u.telefoni_extension" if _users_column_exists("telefoni_extension") else "NULL::varchar AS telefoni_extension" telefoni_active_expr = "u.telefoni_aktiv" if _users_column_exists("telefoni_aktiv") else "FALSE AS telefoni_aktiv" telefoni_ip_expr = "u.telefoni_phone_ip" if _users_column_exists("telefoni_phone_ip") else "NULL::varchar AS telefoni_phone_ip" telefoni_username_expr = "u.telefoni_phone_username" if _users_column_exists("telefoni_phone_username") else "NULL::varchar AS telefoni_phone_username" last_login_expr = "u.last_login_at" if _users_column_exists("last_login_at") else "NULL::timestamp AS last_login_at" has_user_groups = _table_exists("user_groups") has_groups = _table_exists("groups") if has_user_groups and has_groups: groups_join = "LEFT JOIN user_groups ug ON u.user_id = ug.user_id LEFT JOIN groups g ON ug.group_id = g.id" groups_select = "COALESCE(array_remove(array_agg(g.name), NULL), ARRAY[]::varchar[]) AS groups" else: groups_join = "" groups_select = "ARRAY[]::varchar[] AS groups" try: users = execute_query( f""" SELECT u.user_id, u.username, u.email, u.full_name, u.is_active, u.is_superadmin, {is_2fa_expr}, {telefoni_extension_expr}, {telefoni_active_expr}, {telefoni_ip_expr}, {telefoni_username_expr}, u.created_at, {last_login_expr}, {groups_select} FROM users u {groups_join} GROUP BY u.user_id ORDER BY u.user_id """ ) return users except Exception as exc: logger.warning("⚠️ Admin user query fallback triggered: %s", exc) try: users = execute_query( f""" SELECT u.user_id, u.username, u.email, u.full_name, u.is_active, u.is_superadmin, {is_2fa_expr}, {telefoni_extension_expr}, {telefoni_active_expr}, {telefoni_ip_expr}, {telefoni_username_expr}, u.created_at, {last_login_expr}, ARRAY[]::varchar[] AS groups FROM users u ORDER BY u.user_id """ ) return users except Exception as fallback_exc: logger.error("❌ Failed to load admin users (fallback): %s", fallback_exc) raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Could not load users") from fallback_exc @router.post("/admin/users", status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_permission("users.manage"))]) async def create_user(payload: UserAdminCreate): existing = execute_query_single( "SELECT user_id FROM users WHERE username = %s OR email = %s", (payload.username, payload.email) ) if existing: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Username or email already exists" ) try: password_hash = AuthService.hash_password(payload.password) except Exception as exc: logger.error("❌ Password hash failed: %s", exc) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Kunne ikke hashe adgangskoden" ) from exc user_id = execute_insert( """ INSERT INTO users (username, email, password_hash, full_name, is_superadmin, is_active) VALUES (%s, %s, %s, %s, %s, %s) RETURNING user_id """, (payload.username, payload.email, password_hash, payload.full_name, payload.is_superadmin, payload.is_active) ) if payload.group_ids: for group_id in payload.group_ids: execute_update( """ INSERT INTO user_groups (user_id, group_id) VALUES (%s, %s) ON CONFLICT DO NOTHING """, (user_id, group_id) ) logger.info("✅ User created via admin: %s (ID: %s)", payload.username, user_id) return {"user_id": user_id} @router.put("/admin/users/{user_id}/groups", dependencies=[Depends(require_permission("users.manage"))]) async def update_user_groups(user_id: int, payload: UserGroupsUpdate): user = execute_query_single("SELECT user_id FROM users WHERE user_id = %s", (user_id,)) if not user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") execute_update("DELETE FROM user_groups WHERE user_id = %s", (user_id,)) for group_id in payload.group_ids: execute_update( """ INSERT INTO user_groups (user_id, group_id) VALUES (%s, %s) ON CONFLICT DO NOTHING """, (user_id, group_id) ) return {"message": "Groups updated"} @router.patch("/admin/users/{user_id}", dependencies=[Depends(require_permission("users.manage"))]) async def update_user_status(user_id: int, payload: UserStatusUpdateRequest): user = execute_query_single( "SELECT user_id, username FROM users WHERE user_id = %s", (user_id,) ) if not user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") execute_update( "UPDATE users SET is_active = %s, updated_at = CURRENT_TIMESTAMP WHERE user_id = %s", (payload.is_active, user_id) ) logger.info("✅ Updated user status via admin: %s -> active=%s", user.get("username"), payload.is_active) return {"message": "User status updated", "user_id": user_id, "is_active": payload.is_active} @router.post("/admin/users/{user_id}/reset-password", dependencies=[Depends(require_permission("users.manage"))]) async def admin_reset_user_password(user_id: int, payload: UserPasswordResetRequest): user = execute_query_single( "SELECT user_id, username FROM users WHERE user_id = %s", (user_id,) ) if not user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") try: password_hash = AuthService.hash_password(payload.new_password) except Exception as exc: logger.error("❌ Password hash failed for user_id=%s: %s", user_id, exc) raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Kunne ikke hashe adgangskoden") from exc execute_update( "UPDATE users SET password_hash = %s, updated_at = CURRENT_TIMESTAMP WHERE user_id = %s", (password_hash, user_id) ) logger.info("✅ Password reset via admin for user: %s", user.get("username")) return {"message": "Password reset", "user_id": user_id} @router.post("/admin/users/{user_id}/2fa/reset") async def reset_user_2fa( user_id: int, payload: UserTwoFactorResetRequest, current_user: dict = Depends(require_permission("users.manage")) ): ok = AuthService.admin_reset_user_2fa(user_id) if not ok: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") reason = (payload.reason or "").strip() if reason: logger.info( "✅ Admin reset 2FA for user_id=%s by %s (reason: %s)", user_id, current_user.get("username"), reason ) else: logger.info( "✅ Admin reset 2FA for user_id=%s by %s", user_id, current_user.get("username") ) return {"message": "2FA reset"} @router.get("/admin/groups", dependencies=[Depends(require_permission("users.manage"))]) async def list_groups(): groups = execute_query( """ SELECT g.id, g.name, g.description, COALESCE(array_remove(array_agg(p.code), NULL), ARRAY[]::varchar[]) AS permissions FROM groups g LEFT JOIN group_permissions gp ON g.id = gp.group_id LEFT JOIN permissions p ON gp.permission_id = p.id GROUP BY g.id ORDER BY g.id """ ) return groups @router.post("/admin/groups", status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_permission("permissions.manage"))]) async def create_group(payload: GroupCreate): existing = execute_query_single("SELECT id FROM groups WHERE name = %s", (payload.name,)) if existing: raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Group already exists") group_id = execute_insert( """ INSERT INTO groups (name, description) VALUES (%s, %s) RETURNING id """, (payload.name, payload.description) ) return {"group_id": group_id} @router.put("/admin/groups/{group_id}/permissions", dependencies=[Depends(require_permission("permissions.manage"))]) async def update_group_permissions(group_id: int, payload: GroupPermissionsUpdate): group = execute_query_single("SELECT id FROM groups WHERE id = %s", (group_id,)) if not group: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Group not found") execute_update("DELETE FROM group_permissions WHERE group_id = %s", (group_id,)) for permission_id in payload.permission_ids: execute_update( """ INSERT INTO group_permissions (group_id, permission_id) VALUES (%s, %s) ON CONFLICT DO NOTHING """, (group_id, permission_id) ) return {"message": "Permissions updated"} @router.get("/admin/permissions", dependencies=[Depends(require_permission("permissions.manage"))]) async def list_permissions(): permissions = execute_query( """ SELECT id, code, description, category FROM permissions ORDER BY category, code """ ) return permissions