Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 19 additions & 18 deletions backend/app/api/routes/admin/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ async def export_events_json(
)


@router.get("/{event_id}", responses={404: {"model": ErrorResponse}})
@router.get("/{event_id}", responses={404: {"model": ErrorResponse, "description": "Event not found"}})
async def get_event_detail(event_id: str, service: FromDishka[AdminEventsService]) -> EventDetailResponse:
"""Get detailed information about a single event, including related events and timeline."""
result = await service.get_event_detail(event_id)
Expand All @@ -128,7 +128,13 @@ async def get_event_detail(event_id: str, service: FromDishka[AdminEventsService
)


@router.post("/replay", responses={400: {"model": ErrorResponse}, 404: {"model": ErrorResponse}})
@router.post(
"/replay",
responses={
404: {"model": ErrorResponse, "description": "No events match the replay filter"},
422: {"model": ErrorResponse, "description": "Empty filter or too many events to replay"},
},
)
async def replay_events(
request: EventReplayRequest, background_tasks: BackgroundTasks, service: FromDishka[AdminEventsService]
) -> EventReplayResponse:
Expand All @@ -141,20 +147,12 @@ async def replay_events(
start_time=request.start_time,
end_time=request.end_time,
)
try:
result = await service.prepare_or_schedule_replay(
replay_filter=replay_filter,
dry_run=request.dry_run,
replay_correlation_id=replay_correlation_id,
target_service=request.target_service,
)
except ValueError as e:
msg = str(e)
if "No events found" in msg:
raise HTTPException(status_code=404, detail=msg)
if "Too many events" in msg:
raise HTTPException(status_code=400, detail=msg)
raise
result = await service.prepare_or_schedule_replay(
replay_filter=replay_filter,
dry_run=request.dry_run,
replay_correlation_id=replay_correlation_id,
target_service=request.target_service,
)

if not result.dry_run and result.session_id:
background_tasks.add_task(service.start_replay_session, result.session_id)
Expand All @@ -169,7 +167,10 @@ async def replay_events(
)


@router.get("/replay/{session_id}/status", responses={404: {"model": ErrorResponse}})
@router.get(
"/replay/{session_id}/status",
responses={404: {"model": ErrorResponse, "description": "Replay session not found"}},
)
async def get_replay_status(session_id: str, service: FromDishka[AdminEventsService]) -> EventReplayStatusResponse:
"""Get the status and progress of a replay session."""
status = await service.get_replay_status(session_id)
Expand All @@ -190,7 +191,7 @@ async def get_replay_status(session_id: str, service: FromDishka[AdminEventsServ
)


@router.delete("/{event_id}", responses={404: {"model": ErrorResponse}})
@router.delete("/{event_id}", responses={404: {"model": ErrorResponse, "description": "Event not found"}})
async def delete_event(
event_id: str, admin: Annotated[User, Depends(admin_user)], service: FromDishka[AdminEventsService]
) -> EventDeleteResponse:
Expand Down
18 changes: 15 additions & 3 deletions backend/app/api/routes/admin/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
)


@router.get("/", response_model=SystemSettings, responses={500: {"model": ErrorResponse}})
@router.get(
"/",
response_model=SystemSettings,
responses={500: {"model": ErrorResponse, "description": "Failed to load system settings"}},
)
async def get_system_settings(
admin: Annotated[User, Depends(admin_user)],
service: FromDishka[AdminSettingsService],
Expand All @@ -28,7 +32,11 @@ async def get_system_settings(

@router.put(
"/",
responses={400: {"model": ErrorResponse}, 422: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
responses={
400: {"model": ErrorResponse, "description": "Invalid settings values"},
422: {"model": ErrorResponse, "description": "Settings validation failed"},
500: {"model": ErrorResponse, "description": "Failed to save settings"},
},
)
async def update_system_settings(
admin: Annotated[User, Depends(admin_user)],
Expand All @@ -45,7 +53,11 @@ async def update_system_settings(
return SystemSettings.model_validate(updated, from_attributes=True)


@router.post("/reset", response_model=SystemSettings, responses={500: {"model": ErrorResponse}})
@router.post(
"/reset",
response_model=SystemSettings,
responses={500: {"model": ErrorResponse, "description": "Failed to reset settings"}},
)
async def reset_system_settings(
admin: Annotated[User, Depends(admin_user)],
service: FromDishka[AdminSettingsService],
Expand Down
47 changes: 31 additions & 16 deletions backend/app/api/routes/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,22 +61,26 @@ async def list_users(
)


@router.post("/", response_model=UserResponse, responses={400: {"model": ErrorResponse}})
@router.post(
"/",
response_model=UserResponse,
responses={409: {"model": ErrorResponse, "description": "Username already exists"}},
)
async def create_user(
admin: Annotated[User, Depends(admin_user)],
user_data: UserCreate,
admin_user_service: FromDishka[AdminUserService],
) -> UserResponse:
"""Create a new user (admin only)."""
# Delegate to service; map known validation error to 400
try:
domain_user = await admin_user_service.create_user(admin_username=admin.username, user_data=user_data)
except ValueError as ve:
raise HTTPException(status_code=400, detail=str(ve))
domain_user = await admin_user_service.create_user(admin_username=admin.username, user_data=user_data)
return UserResponse.model_validate(domain_user)


@router.get("/{user_id}", response_model=UserResponse, responses={404: {"model": ErrorResponse}})
@router.get(
"/{user_id}",
response_model=UserResponse,
responses={404: {"model": ErrorResponse, "description": "User not found"}},
)
async def get_user(
admin: Annotated[User, Depends(admin_user)],
user_id: str,
Expand All @@ -90,18 +94,18 @@ async def get_user(
return UserResponse.model_validate(user)


@router.get("/{user_id}/overview", response_model=AdminUserOverview, responses={404: {"model": ErrorResponse}})
@router.get(
"/{user_id}/overview",
response_model=AdminUserOverview,
responses={404: {"model": ErrorResponse, "description": "User not found"}},
)
async def get_user_overview(
admin: Annotated[User, Depends(admin_user)],
user_id: str,
admin_user_service: FromDishka[AdminUserService],
) -> AdminUserOverview:
"""Get a comprehensive overview of a user including stats and rate limits."""
# Service raises ValueError if not found -> map to 404
try:
domain = await admin_user_service.get_user_overview(user_id=user_id, hours=24)
except ValueError:
raise HTTPException(status_code=404, detail="User not found")
domain = await admin_user_service.get_user_overview(user_id=user_id, hours=24)
return AdminUserOverview(
user=UserResponse.model_validate(domain.user),
stats=EventStatistics.model_validate(domain.stats),
Expand All @@ -114,7 +118,10 @@ async def get_user_overview(
@router.put(
"/{user_id}",
response_model=UserResponse,
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
responses={
404: {"model": ErrorResponse, "description": "User not found"},
500: {"model": ErrorResponse, "description": "Failed to update user"},
},
)
async def update_user(
admin: Annotated[User, Depends(admin_user)],
Expand Down Expand Up @@ -147,7 +154,11 @@ async def update_user(
return UserResponse.model_validate(updated_user)


@router.delete("/{user_id}", response_model=DeleteUserResponse, responses={400: {"model": ErrorResponse}})
@router.delete(
"/{user_id}",
response_model=DeleteUserResponse,
responses={400: {"model": ErrorResponse, "description": "Cannot delete your own account"}},
)
async def delete_user(
admin: Annotated[User, Depends(admin_user)],
user_id: str,
Expand All @@ -174,7 +185,11 @@ async def delete_user(
)


@router.post("/{user_id}/reset-password", response_model=MessageResponse, responses={500: {"model": ErrorResponse}})
@router.post(
"/{user_id}/reset-password",
response_model=MessageResponse,
responses={500: {"model": ErrorResponse, "description": "Failed to reset password"}},
)
async def reset_user_password(
admin: Annotated[User, Depends(admin_user)],
admin_user_service: FromDishka[AdminUserService],
Expand Down
119 changes: 50 additions & 69 deletions backend/app/api/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from app.core.security import SecurityService
from app.core.utils import get_client_ip
from app.db.repositories import UserRepository
from app.domain.exceptions import ConflictError
from app.domain.user import DomainUserCreate
from app.schemas_pydantic.common import ErrorResponse
from app.schemas_pydantic.user import (
Expand All @@ -25,7 +26,11 @@
router = APIRouter(prefix="/auth", tags=["authentication"], route_class=DishkaRoute)


@router.post("/login", response_model=LoginResponse, responses={401: {"model": ErrorResponse}})
@router.post(
"/login",
response_model=LoginResponse,
responses={401: {"model": ErrorResponse, "description": "Invalid username or password"}},
)
async def login(
request: Request,
response: Response,
Expand Down Expand Up @@ -127,7 +132,10 @@ async def login(
@router.post(
"/register",
response_model=UserResponse,
responses={400: {"model": ErrorResponse}, 409: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
responses={
400: {"model": ErrorResponse, "description": "Username already registered"},
409: {"model": ErrorResponse, "description": "Email already registered"},
},
)
async def register(
request: Request,
Expand Down Expand Up @@ -170,26 +178,6 @@ async def register(
is_superuser=False,
)
created_user = await user_repo.create_user(create_data)

logger.info(
"Registration successful",
extra={
"username": created_user.username,
"client_ip": get_client_ip(request),
"user_agent": request.headers.get("user-agent"),
},
)

return UserResponse(
user_id=created_user.user_id,
username=created_user.username,
email=created_user.email,
role=created_user.role,
is_superuser=created_user.is_superuser,
created_at=created_user.created_at,
updated_at=created_user.updated_at,
)

except DuplicateKeyError as e:
logger.warning(
"Registration failed - duplicate email",
Expand All @@ -198,20 +186,26 @@ async def register(
"client_ip": get_client_ip(request),
},
)
raise HTTPException(status_code=409, detail="Email already registered") from e
except Exception as e:
logger.error(
f"Registration failed - database error: {str(e)}",
extra={
"username": user.username,
"client_ip": get_client_ip(request),
"user_agent": request.headers.get("user-agent"),
"error_type": type(e).__name__,
"error_detail": str(e),
},
exc_info=True,
)
raise HTTPException(status_code=500, detail="Error creating user") from e
raise ConflictError("Email already registered") from e

logger.info(
"Registration successful",
extra={
"username": created_user.username,
"client_ip": get_client_ip(request),
"user_agent": request.headers.get("user-agent"),
},
)

return UserResponse(
user_id=created_user.user_id,
username=created_user.username,
email=created_user.email,
role=created_user.role,
is_superuser=created_user.is_superuser,
created_at=created_user.created_at,
updated_at=created_user.updated_at,
)


@router.get("/me", response_model=UserResponse)
Expand Down Expand Up @@ -240,7 +234,11 @@ async def get_current_user_profile(
return UserResponse.model_validate(current_user, from_attributes=True)


@router.get("/verify-token", response_model=TokenValidationResponse, responses={401: {"model": ErrorResponse}})
@router.get(
"/verify-token",
response_model=TokenValidationResponse,
responses={401: {"model": ErrorResponse, "description": "Missing or invalid access token"}},
)
async def verify_token(
request: Request,
auth_service: FromDishka[AuthService],
Expand All @@ -258,39 +256,22 @@ async def verify_token(
},
)

try:
logger.info(
"Token verification successful",
extra={
"username": current_user.username,
"client_ip": get_client_ip(request),
"user_agent": request.headers.get("user-agent"),
},
)
csrf_token = request.cookies.get("csrf_token", "")

return TokenValidationResponse(
valid=True,
username=current_user.username,
role="admin" if current_user.is_superuser else "user",
csrf_token=csrf_token,
)
logger.info(
"Token verification successful",
extra={
"username": current_user.username,
"client_ip": get_client_ip(request),
"user_agent": request.headers.get("user-agent"),
},
)
csrf_token = request.cookies.get("csrf_token", "")

except Exception as e:
logger.error(
"Token verification failed",
extra={
"client_ip": get_client_ip(request),
"user_agent": request.headers.get("user-agent"),
"error_type": type(e).__name__,
"error_detail": str(e),
},
)
raise HTTPException(
status_code=401,
detail="Invalid token",
headers={"WWW-Authenticate": "Bearer"},
) from e
return TokenValidationResponse(
valid=True,
username=current_user.username,
role="admin" if current_user.is_superuser else "user",
csrf_token=csrf_token,
)


@router.post("/logout", response_model=MessageResponse)
Expand Down
Loading
Loading