mega changes

This commit is contained in:
2025-11-20 11:11:18 +01:00
parent e070c95190
commit 841d79f26b
138 changed files with 21499 additions and 8844 deletions

View File

@@ -85,16 +85,16 @@ async def list_users(
is_active: Optional[bool] = Query(None),
search: Optional[str] = Query(None),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_db),
):
"""List all users with pagination and filtering"""
# Check permissions
require_permission(current_user.get("permissions", []), "platform:users:read")
# Build query
query = select(User)
# Apply filters
if role:
query = query.where(User.role == role)
@@ -102,38 +102,42 @@ async def list_users(
query = query.where(User.is_active == is_active)
if search:
query = query.where(
(User.username.ilike(f"%{search}%")) |
(User.email.ilike(f"%{search}%")) |
(User.full_name.ilike(f"%{search}%"))
(User.username.ilike(f"%{search}%"))
| (User.email.ilike(f"%{search}%"))
| (User.full_name.ilike(f"%{search}%"))
)
# Get total count
total_query = select(User.id).select_from(query.subquery())
total_result = await db.execute(total_query)
total = len(total_result.fetchall())
# Apply pagination
offset = (page - 1) * size
query = query.offset(offset).limit(size)
# Execute query
result = await db.execute(query)
users = result.scalars().all()
# Log audit event
await log_audit_event(
db=db,
user_id=current_user['id'],
user_id=current_user["id"],
action="list_users",
resource_type="user",
details={"page": page, "size": size, "filters": {"role": role, "is_active": is_active, "search": search}}
details={
"page": page,
"size": size,
"filters": {"role": role, "is_active": is_active, "search": search},
},
)
return UserListResponse(
users=[UserResponse.model_validate(user) for user in users],
total=total,
page=page,
size=size
size=size,
)
@@ -141,34 +145,33 @@ async def list_users(
async def get_user(
user_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_db),
):
"""Get user by ID"""
# Check permissions (users can view their own profile)
if int(user_id) != current_user['id']:
if int(user_id) != current_user["id"]:
require_permission(current_user.get("permissions", []), "platform:users:read")
# Get user
query = select(User).where(User.id == int(user_id))
result = await db.execute(query)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
)
# Log audit event
await log_audit_event(
db=db,
user_id=current_user['id'],
user_id=current_user["id"],
action="get_user",
resource_type="user",
resource_id=user_id
resource_id=user_id,
)
return UserResponse.model_validate(user)
@@ -176,26 +179,26 @@ async def get_user(
async def create_user(
user_data: UserCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_db),
):
"""Create a new user"""
# Check permissions
require_permission(current_user.get("permissions", []), "platform:users:create")
# Check if user already exists
query = select(User).where(
(User.username == user_data.username) | (User.email == user_data.email)
)
result = await db.execute(query)
existing_user = result.scalar_one_or_none()
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="User with this username or email already exists"
detail="User with this username or email already exists",
)
# Create user
hashed_password = get_password_hash(user_data.password)
new_user = User(
@@ -204,25 +207,29 @@ async def create_user(
full_name=user_data.full_name,
hashed_password=hashed_password,
role=user_data.role,
is_active=user_data.is_active
is_active=user_data.is_active,
)
db.add(new_user)
await db.commit()
await db.refresh(new_user)
# Log audit event
await log_audit_event(
db=db,
user_id=current_user['id'],
user_id=current_user["id"],
action="create_user",
resource_type="user",
resource_id=str(new_user.id),
details={"username": user_data.username, "email": user_data.email, "role": user_data.role}
details={
"username": user_data.username,
"email": user_data.email,
"role": user_data.role,
},
)
logger.info(f"User created: {new_user.username} by {current_user['username']}")
return UserResponse.model_validate(new_user)
@@ -231,26 +238,25 @@ async def update_user(
user_id: str,
user_data: UserUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_db),
):
"""Update user"""
# Check permissions (users can update their own profile with limited fields)
is_self_update = int(user_id) == current_user['id']
is_self_update = int(user_id) == current_user["id"]
if not is_self_update:
require_permission(current_user.get("permissions", []), "platform:users:update")
# Get user
query = select(User).where(User.id == int(user_id))
result = await db.execute(query)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
)
# For self-updates, restrict what can be changed
if is_self_update:
allowed_fields = {"username", "email", "full_name"}
@@ -259,41 +265,41 @@ async def update_user(
if restricted_fields:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Cannot update fields: {restricted_fields}"
detail=f"Cannot update fields: {restricted_fields}",
)
# Store original values for audit
original_values = {
"username": user.username,
"email": user.email,
"role": user.role,
"is_active": user.is_active
"is_active": user.is_active,
}
# Update user fields
update_data = user_data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(user, field, value)
await db.commit()
await db.refresh(user)
# Log audit event
await log_audit_event(
db=db,
user_id=current_user['id'],
user_id=current_user["id"],
action="update_user",
resource_type="user",
resource_id=user_id,
details={
"updated_fields": list(update_data.keys()),
"before_values": original_values,
"after_values": {k: getattr(user, k) for k in update_data.keys()}
}
"after_values": {k: getattr(user, k) for k in update_data.keys()},
},
)
logger.info(f"User updated: {user.username} by {current_user['username']}")
return UserResponse.model_validate(user)
@@ -301,47 +307,46 @@ async def update_user(
async def delete_user(
user_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_db),
):
"""Delete user (soft delete by deactivating)"""
# Check permissions
require_permission(current_user.get("permissions", []), "platform:users:delete")
# Prevent self-deletion
if int(user_id) == current_user['id']:
if int(user_id) == current_user["id"]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cannot delete your own account"
detail="Cannot delete your own account",
)
# Get user
query = select(User).where(User.id == int(user_id))
result = await db.execute(query)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
)
# Soft delete by deactivating
user.is_active = False
await db.commit()
# Log audit event
await log_audit_event(
db=db,
user_id=current_user['id'],
user_id=current_user["id"],
action="delete_user",
resource_type="user",
resource_id=user_id,
details={"username": user.username, "email": user.email}
details={"username": user.username, "email": user.email},
)
logger.info(f"User deleted: {user.username} by {current_user['username']}")
return {"message": "User deleted successfully"}
@@ -350,50 +355,51 @@ async def change_password(
user_id: str,
password_data: PasswordChangeRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_db),
):
"""Change user password"""
# Users can only change their own password, or admins can change any password
is_self_update = int(user_id) == current_user['id']
is_self_update = int(user_id) == current_user["id"]
if not is_self_update:
require_permission(current_user.get("permissions", []), "platform:users:update")
# Get user
query = select(User).where(User.id == int(user_id))
result = await db.execute(query)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
)
# For self-updates, verify current password
if is_self_update:
if not verify_password(password_data.current_password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Current password is incorrect"
detail="Current password is incorrect",
)
# Update password
user.hashed_password = get_password_hash(password_data.new_password)
await db.commit()
# Log audit event
await log_audit_event(
db=db,
user_id=current_user['id'],
user_id=current_user["id"],
action="change_password",
resource_type="user",
resource_id=user_id,
details={"target_user": user.username}
details={"target_user": user.username},
)
logger.info(f"Password changed for user: {user.username} by {current_user['username']}")
logger.info(
f"Password changed for user: {user.username} by {current_user['username']}"
)
return {"message": "Password changed successfully"}
@@ -402,40 +408,41 @@ async def reset_password(
user_id: str,
password_data: PasswordResetRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_db),
):
"""Reset user password (admin only)"""
# Check permissions
require_permission(current_user.get("permissions", []), "platform:users:update")
# Get user
query = select(User).where(User.id == int(user_id))
result = await db.execute(query)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
)
# Reset password
user.hashed_password = get_password_hash(password_data.new_password)
await db.commit()
# Log audit event
await log_audit_event(
db=db,
user_id=current_user['id'],
user_id=current_user["id"],
action="reset_password",
resource_type="user",
resource_id=user_id,
details={"target_user": user.username}
details={"target_user": user.username},
)
logger.info(f"Password reset for user: {user.username} by {current_user['username']}")
logger.info(
f"Password reset for user: {user.username} by {current_user['username']}"
)
return {"message": "Password reset successfully"}
@@ -443,20 +450,22 @@ async def reset_password(
async def get_user_api_keys(
user_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_db),
):
"""Get API keys for a user"""
# Check permissions (users can view their own API keys)
is_self_request = int(user_id) == current_user['id']
is_self_request = int(user_id) == current_user["id"]
if not is_self_request:
require_permission(current_user.get("permissions", []), "platform:api-keys:read")
require_permission(
current_user.get("permissions", []), "platform:api-keys:read"
)
# Get API keys
query = select(APIKey).where(APIKey.user_id == int(user_id))
result = await db.execute(query)
api_keys = result.scalars().all()
# Return safe representation (no key values)
return [
{
@@ -466,8 +475,12 @@ async def get_user_api_keys(
"scopes": api_key.scopes,
"is_active": api_key.is_active,
"created_at": api_key.created_at.isoformat(),
"expires_at": api_key.expires_at.isoformat() if api_key.expires_at else None,
"last_used_at": api_key.last_used_at.isoformat() if api_key.last_used_at else None
"expires_at": api_key.expires_at.isoformat()
if api_key.expires_at
else None,
"last_used_at": api_key.last_used_at.isoformat()
if api_key.last_used_at
else None,
}
for api_key in api_keys
]
]