264 lines
9.9 KiB
Python
264 lines
9.9 KiB
Python
"""
|
|
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)
|
|
|
|
|
|
@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"
|
|
|
|
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},
|
|
COALESCE(array_remove(array_agg(g.name), NULL), ARRAY[]::varchar[]) AS groups
|
|
FROM users u
|
|
LEFT JOIN user_groups ug ON u.user_id = ug.user_id
|
|
LEFT JOIN groups g ON ug.group_id = g.id
|
|
GROUP BY u.user_id
|
|
ORDER BY u.user_id
|
|
"""
|
|
)
|
|
return users
|
|
except Exception as exc:
|
|
logger.error("❌ Failed to load admin users: %s", exc)
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Could not load users") from 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
|