Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d677007
fix: user.role as str -> UserRole (enum)
HardMax71 Feb 9, 2026
14afff2
fix: removed try-catch from endpoint, conflict error is thrown by use…
HardMax71 Feb 9, 2026
b816cb8
fix: removed /verify-token - now all the stuff goes through /me in th…
HardMax71 Feb 9, 2026
b1ab356
fix: dlq/stats endpoint - removed; stats are exported out of dlq mana…
HardMax71 Feb 9, 2026
7250669
fix: e2e/conftest: - passing role as UserRole, not str
HardMax71 Feb 9, 2026
615f350
fix: dlq - only for admins, also one-liner for policy request -> retr…
HardMax71 Feb 9, 2026
b95e9ea
fix: events - model_validate instead of manual fields assignment; als…
HardMax71 Feb 9, 2026
b0aed8d
fix: exeuctions - ExecutionStatus enum instead of bare str
HardMax71 Feb 9, 2026
10f1640
fix: executions - moved business logic from endpoints to exec service…
HardMax71 Feb 10, 2026
01c3c23
fix: removed alert processing from backend side (set up stuff directl…
HardMax71 Feb 10, 2026
adc60f8
fix: health - removed ready endpoint (not used)
HardMax71 Feb 10, 2026
5b15068
fix: health - removed health objs (not used)
HardMax71 Feb 10, 2026
de1d4f6
fix: notifications - funcs returning bool now are void (cause they re…
HardMax71 Feb 10, 2026
4b1a224
fix: replay - model_validate + from_attrs=True
HardMax71 Feb 10, 2026
24717d5
fix: saga - current user through DI, not calling user service on site
HardMax71 Feb 10, 2026
cea8a65
fix: saved scripts - passing current user through DI
HardMax71 Feb 10, 2026
785fd12
fix: sse - current_user via DI and correct schemas in responses
HardMax71 Feb 10, 2026
79c5c0b
fix: user settings - better converting
HardMax71 Feb 10, 2026
64cf5c6
fix: detected issues
HardMax71 Feb 10, 2026
86fe4c3
fix: detected issues
HardMax71 Feb 10, 2026
77a0755
fix: reverted sse changes
HardMax71 Feb 10, 2026
baebf74
fix: sse response_class
HardMax71 Feb 10, 2026
920b025
fix: tests
HardMax71 Feb 10, 2026
c2343c1
fix: from+attr removed from endpoints (to schema defintions), update …
HardMax71 Feb 10, 2026
67e7d5c
fix: schema issue
HardMax71 Feb 10, 2026
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
51 changes: 12 additions & 39 deletions backend/app/api/routes/admin/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
EventStatsResponse,
)
from app.schemas_pydantic.common import ErrorResponse
from app.schemas_pydantic.execution import ExecutionResult
from app.services.admin import AdminEventsService

router = APIRouter(
Expand All @@ -33,22 +34,15 @@
@router.post("/browse")
async def browse_events(request: EventBrowseRequest, service: FromDishka[AdminEventsService]) -> EventBrowseResponse:
"""Browse events with filtering, sorting, and pagination."""
event_filter = EventFilter(**request.filters.model_dump())

result = await service.browse_events(
event_filter=event_filter,
event_filter=EventFilter.model_validate(request.filters),
skip=request.skip,
limit=request.limit,
sort_by=request.sort_by,
sort_order=request.sort_order,
)

return EventBrowseResponse(
events=result.events,
total=result.total,
skip=result.skip,
limit=result.limit,
)
return EventBrowseResponse.model_validate(result)


@router.get("/stats")
Expand Down Expand Up @@ -121,11 +115,7 @@ async def get_event_detail(event_id: str, service: FromDishka[AdminEventsService
if not result:
raise HTTPException(status_code=404, detail="Event not found")

return EventDetailResponse(
event=result.event,
related_events=result.related_events,
timeline=result.timeline,
)
return EventDetailResponse.model_validate(result)


@router.post(
Expand All @@ -140,15 +130,8 @@ async def replay_events(
) -> EventReplayResponse:
"""Replay events by filter criteria, with optional dry-run mode."""
replay_correlation_id = f"replay_{CorrelationContext.get_correlation_id()}"
replay_filter = ReplayFilter(
event_ids=request.event_ids,
correlation_id=request.correlation_id,
aggregate_id=request.aggregate_id,
start_time=request.start_time,
end_time=request.end_time,
)
result = await service.prepare_or_schedule_replay(
replay_filter=replay_filter,
replay_filter=ReplayFilter.model_validate(request),
dry_run=request.dry_run,
replay_correlation_id=replay_correlation_id,
target_service=request.target_service,
Expand All @@ -157,14 +140,7 @@ async def replay_events(
if not result.dry_run and result.session_id:
background_tasks.add_task(service.start_replay_session, result.session_id)

return EventReplayResponse(
dry_run=result.dry_run,
total_events=result.total_events,
replay_correlation_id=result.replay_correlation_id,
session_id=result.session_id,
status=result.status,
events_preview=result.events_preview,
)
return EventReplayResponse.model_validate(result)


@router.get(
Expand All @@ -178,16 +154,13 @@ async def get_replay_status(session_id: str, service: FromDishka[AdminEventsServ
if not status:
raise HTTPException(status_code=404, detail="Replay session not found")

session = status.session
estimated_completion = status.estimated_completion
execution_results = status.execution_results
return EventReplayStatusResponse(
**{
**session.model_dump(),
"status": session.status,
"estimated_completion": estimated_completion,
"execution_results": execution_results,
}
**status.session.model_dump(),
estimated_completion=status.estimated_completion,
execution_results=[
ExecutionResult.model_validate(er)
for er in status.execution_results
],
)


Expand Down
6 changes: 3 additions & 3 deletions backend/app/api/routes/admin/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ async def get_system_settings(
) -> SystemSettings:
"""Get the current system-wide settings."""
domain_settings = await service.get_system_settings(admin.username)
return SystemSettings.model_validate(domain_settings, from_attributes=True)
return SystemSettings.model_validate(domain_settings)


@router.put(
Expand All @@ -50,7 +50,7 @@ async def update_system_settings(
updated_by=admin.username,
user_id=admin.user_id,
)
return SystemSettings.model_validate(updated, from_attributes=True)
return SystemSettings.model_validate(updated)


@router.post(
Expand All @@ -64,4 +64,4 @@ async def reset_system_settings(
) -> SystemSettings:
"""Reset system-wide settings to defaults."""
reset = await service.reset_system_settings(admin.username, admin.user_id)
return SystemSettings.model_validate(reset, from_attributes=True)
return SystemSettings.model_validate(reset)
42 changes: 5 additions & 37 deletions backend/app/api/routes/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,8 @@
from app.domain.rate_limit import RateLimitRule, UserRateLimit
from app.domain.user import User
from app.domain.user import UserUpdate as DomainUserUpdate
from app.schemas_pydantic.admin_user_overview import (
AdminUserOverview,
DerivedCounts,
RateLimitSummary,
)
from app.schemas_pydantic.admin_user_overview import AdminUserOverview
from app.schemas_pydantic.common import ErrorResponse
from app.schemas_pydantic.events import EventStatistics
from app.schemas_pydantic.user import (
DeleteUserResponse,
MessageResponse,
Expand Down Expand Up @@ -53,12 +48,7 @@ async def list_users(
search=search,
role=role,
)
return UserListResponse(
users=[UserResponse.model_validate(u) for u in result.users],
total=result.total,
offset=result.offset,
limit=result.limit,
)
return UserListResponse.model_validate(result)


@router.post(
Expand Down Expand Up @@ -106,13 +96,7 @@ async def get_user_overview(
) -> AdminUserOverview:
"""Get a comprehensive overview of a user including stats and rate limits."""
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),
derived_counts=DerivedCounts.model_validate(domain.derived_counts),
rate_limit_summary=RateLimitSummary.model_validate(domain.rate_limit_summary),
recent_events=domain.recent_events,
)
return AdminUserOverview.model_validate(domain)


@router.put(
Expand All @@ -136,14 +120,7 @@ async def update_user(
if not existing_user:
raise HTTPException(status_code=404, detail="User not found")

update_dict = user_update.model_dump(exclude_unset=True)
domain_update = DomainUserUpdate(
username=update_dict.get("username"),
email=update_dict.get("email"),
role=UserRole(update_dict["role"]) if "role" in update_dict else None,
is_active=update_dict.get("is_active"),
password=update_dict.get("password"),
)
domain_update = DomainUserUpdate.model_validate(user_update)

updated_user = await admin_user_service.update_user(
admin_username=admin.username, user_id=user_id, update=domain_update
Expand Down Expand Up @@ -173,16 +150,7 @@ async def delete_user(
result = await admin_user_service.delete_user(
admin_username=admin.username, user_id=user_id, cascade=cascade
)
return DeleteUserResponse(
message=f"User {user_id} deleted successfully",
user_deleted=result.user_deleted,
executions=result.executions,
saved_scripts=result.saved_scripts,
notifications=result.notifications,
user_settings=result.user_settings,
events=result.events,
sagas=result.sagas,
)
return DeleteUserResponse.model_validate(result)


@router.post(
Expand Down
90 changes: 15 additions & 75 deletions backend/app/api/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,16 @@
from dishka.integrations.fastapi import DishkaRoute
from fastapi import APIRouter, Depends, HTTPException, Request, Response
from fastapi.security import OAuth2PasswordRequestForm
from pymongo.errors import DuplicateKeyError

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.enums import UserRole
from app.domain.user import DomainUserCreate
from app.schemas_pydantic.common import ErrorResponse
from app.schemas_pydantic.user import (
LoginResponse,
MessageResponse,
TokenValidationResponse,
UserCreate,
UserResponse,
)
Expand Down Expand Up @@ -124,7 +122,7 @@ async def login(
return LoginResponse(
message="Login successful",
username=user.username,
role="admin" if user.is_superuser else "user",
role=user.role,
csrf_token=csrf_token,
)

Expand All @@ -134,7 +132,7 @@ async def login(
response_model=UserResponse,
responses={
400: {"model": ErrorResponse, "description": "Username already registered"},
409: {"model": ErrorResponse, "description": "Email already registered"},
409: {"model": ErrorResponse, "description": "User already exists"},
},
)
async def register(
Expand Down Expand Up @@ -167,26 +165,16 @@ async def register(
)
raise HTTPException(status_code=400, detail="Username already registered")

try:
hashed_password = security_service.get_password_hash(user.password)
create_data = DomainUserCreate(
username=user.username,
email=str(user.email),
hashed_password=hashed_password,
role=user.role,
is_active=True,
is_superuser=False,
)
created_user = await user_repo.create_user(create_data)
except DuplicateKeyError as e:
logger.warning(
"Registration failed - duplicate email",
extra={
"username": user.username,
"client_ip": get_client_ip(request),
},
)
raise ConflictError("Email already registered") from e
hashed_password = security_service.get_password_hash(user.password)
create_data = DomainUserCreate(
username=user.username,
email=user.email,
hashed_password=hashed_password,
role=UserRole.USER,
is_active=True,
is_superuser=False,
)
Comment thread
HardMax71 marked this conversation as resolved.
created_user = await user_repo.create_user(create_data)

logger.info(
"Registration successful",
Expand All @@ -197,15 +185,7 @@ async def register(
},
)

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,
)
return UserResponse.model_validate(created_user)


@router.get("/me", response_model=UserResponse)
Expand All @@ -231,47 +211,7 @@ async def get_current_user_profile(
response.headers["Cache-Control"] = "no-store"
response.headers["Pragma"] = "no-cache"

return UserResponse.model_validate(current_user, from_attributes=True)


@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],
logger: FromDishka[logging.Logger],
) -> TokenValidationResponse:
"""Verify the current access token."""
current_user = await auth_service.get_current_user(request)
logger.info(
"Token verification attempt",
extra={
"username": current_user.username,
"client_ip": get_client_ip(request),
"endpoint": "/verify-token",
"user_agent": request.headers.get("user-agent"),
},
)

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,
)
return UserResponse.model_validate(current_user)


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