diff --git a/app/auth/backend/admin.py b/app/auth/backend/admin.py index 1d0b8b1..0a53cef 100644 --- a/app/auth/backend/admin.py +++ b/app/auth/backend/admin.py @@ -2,6 +2,7 @@ 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 @@ -13,6 +14,14 @@ 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) + + @router.get("/admin/users", dependencies=[Depends(require_permission("users.manage"))]) async def list_users(): users = execute_query( @@ -94,6 +103,48 @@ async def update_user_groups(user_id: int, payload: UserGroupsUpdate): 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, diff --git a/app/settings/backend/router.py b/app/settings/backend/router.py index 00d3912..afa46a8 100644 --- a/app/settings/backend/router.py +++ b/app/settings/backend/router.py @@ -180,7 +180,7 @@ async def sync_settings_from_env(): @router.get("/users", response_model=List[User], tags=["Users"]) async def get_users(is_active: Optional[bool] = None): """Get all users""" - query = "SELECT user_id as id, username, email, full_name, is_active, last_login, created_at FROM users" + query = "SELECT user_id as id, username, email, full_name, is_active, last_login_at as last_login, created_at FROM users" params = [] if is_active is not None: @@ -195,7 +195,7 @@ async def get_users(is_active: Optional[bool] = None): @router.get("/users/{user_id}", response_model=User, tags=["Users"]) async def get_user(user_id: int): """Get user by ID""" - query = "SELECT user_id as id, username, email, full_name, is_active, last_login, created_at FROM users WHERE user_id = %s" + query = "SELECT user_id as id, username, email, full_name, is_active, last_login_at as last_login, created_at FROM users WHERE user_id = %s" result = execute_query(query, (user_id,)) if not result: @@ -219,7 +219,7 @@ async def create_user(user: UserCreate): query = """ INSERT INTO users (username, email, password_hash, full_name, is_active) VALUES (%s, %s, %s, %s, true) - RETURNING user_id as id, username, email, full_name, is_active, last_login, created_at + RETURNING user_id as id, username, email, full_name, is_active, last_login_at as last_login, created_at """ result = execute_query(query, (user.username, user.email, password_hash, user.full_name)) @@ -260,7 +260,7 @@ async def update_user(user_id: int, user: UserUpdate): UPDATE users SET {', '.join(update_fields)}, updated_at = CURRENT_TIMESTAMP WHERE user_id = %s - RETURNING user_id as id, username, email, full_name, is_active, last_login, created_at + RETURNING user_id as id, username, email, full_name, is_active, last_login_at as last_login, created_at """ result = execute_query(query, tuple(params)) diff --git a/app/settings/frontend/settings.html b/app/settings/frontend/settings.html index f5c6956..3042139 100644 --- a/app/settings/frontend/settings.html +++ b/app/settings/frontend/settings.html @@ -764,6 +764,62 @@ + +