Improve the frontend modals

This commit is contained in:
Urtzi Alfaro
2025-10-27 16:33:26 +01:00
parent 61376b7a9f
commit 858d985c92
143 changed files with 9289 additions and 2306 deletions

View File

@@ -10,7 +10,6 @@ from datetime import datetime, timezone
import structlog
from shared.auth.decorators import get_current_user_dep
from shared.routing import RouteBuilder
from app.core.database import get_db
from app.services.admin_delete import AdminUserDeleteService
from app.models.users import User
@@ -21,7 +20,6 @@ import httpx
logger = structlog.get_logger()
router = APIRouter()
route_builder = RouteBuilder('auth')
class AccountDeletionRequest(BaseModel):
@@ -39,7 +37,7 @@ class DeletionScheduleResponse(BaseModel):
grace_period_days: int = 30
@router.post("/api/v1/users/me/delete/request")
@router.delete("/api/v1/auth/me/account")
async def request_account_deletion(
deletion_request: AccountDeletionRequest,
request: Request,
@@ -62,7 +60,7 @@ async def request_account_deletion(
- Current password verification
"""
try:
user_id = UUID(current_user["sub"])
user_id = UUID(current_user["user_id"])
user_email = current_user.get("email")
if deletion_request.confirm_email.lower() != user_email.lower():
@@ -149,7 +147,7 @@ async def request_account_deletion(
except Exception as e:
logger.error(
"account_deletion_failed",
user_id=current_user.get("sub"),
user_id=current_user.get("user_id"),
error=str(e)
)
raise HTTPException(
@@ -158,7 +156,7 @@ async def request_account_deletion(
)
@router.get("/api/v1/users/me/delete/info")
@router.get("/api/v1/auth/me/account/deletion-info")
async def get_deletion_info(
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
@@ -170,7 +168,7 @@ async def get_deletion_info(
account deletion. Transparency requirement under GDPR.
"""
try:
user_id = UUID(current_user["sub"])
user_id = UUID(current_user["user_id"])
deletion_service = AdminUserDeleteService(db)
preview = await deletion_service.preview_user_deletion(str(user_id))
@@ -207,7 +205,7 @@ async def get_deletion_info(
except Exception as e:
logger.error(
"deletion_info_failed",
user_id=current_user.get("sub"),
user_id=current_user.get("user_id"),
error=str(e)
)
raise HTTPException(

View File

@@ -5,6 +5,8 @@ Business logic for login, register, token refresh, password reset, and email ver
from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Dict, Any
import structlog
from app.schemas.auth import (
@@ -12,16 +14,17 @@ from app.schemas.auth import (
PasswordChange, PasswordReset, UserResponse
)
from app.services.auth_service import EnhancedAuthService
from app.models.users import User
from app.core.database import get_db
from shared.database.base import create_database_manager
from shared.monitoring.decorators import track_execution_time
from shared.monitoring.metrics import get_metrics_collector
from shared.routing import RouteBuilder
from shared.auth.decorators import get_current_user_dep
from app.core.config import settings
logger = structlog.get_logger()
router = APIRouter(tags=["auth-operations"])
security = HTTPBearer()
route_builder = RouteBuilder('auth')
def get_auth_service():
@@ -30,7 +33,7 @@ def get_auth_service():
return EnhancedAuthService(database_manager)
@router.post(route_builder.build_base_route("register", include_tenant_prefix=False), response_model=TokenResponse)
@router.post("/api/v1/auth/register", response_model=TokenResponse)
@track_execution_time("enhanced_registration_duration_seconds", "auth-service")
async def register(
user_data: UserRegistration,
@@ -100,7 +103,7 @@ async def register(
)
@router.post(route_builder.build_base_route("login", include_tenant_prefix=False), response_model=TokenResponse)
@router.post("/api/v1/auth/login", response_model=TokenResponse)
@track_execution_time("enhanced_login_duration_seconds", "auth-service")
async def login(
login_data: UserLogin,
@@ -164,7 +167,7 @@ async def login(
)
@router.post(route_builder.build_base_route("refresh", include_tenant_prefix=False))
@router.post("/api/v1/auth/refresh")
@track_execution_time("enhanced_token_refresh_duration_seconds", "auth-service")
async def refresh_token(
refresh_data: RefreshTokenRequest,
@@ -201,7 +204,7 @@ async def refresh_token(
)
@router.post(route_builder.build_base_route("verify", include_tenant_prefix=False))
@router.post("/api/v1/auth/verify")
@track_execution_time("enhanced_token_verify_duration_seconds", "auth-service")
async def verify_token(
credentials: HTTPAuthorizationCredentials = Depends(security),
@@ -249,7 +252,7 @@ async def verify_token(
)
@router.post(route_builder.build_base_route("logout", include_tenant_prefix=False))
@router.post("/api/v1/auth/logout")
@track_execution_time("enhanced_logout_duration_seconds", "auth-service")
async def logout(
refresh_data: RefreshTokenRequest,
@@ -295,7 +298,7 @@ async def logout(
)
@router.post(route_builder.build_base_route("change-password", include_tenant_prefix=False))
@router.post("/api/v1/auth/change-password")
async def change_password(
password_data: PasswordChange,
credentials: HTTPAuthorizationCredentials = Depends(security),
@@ -358,98 +361,116 @@ async def change_password(
)
@router.get(route_builder.build_base_route("profile", include_tenant_prefix=False), response_model=UserResponse)
@router.get("/api/v1/auth/me", response_model=UserResponse)
async def get_profile(
credentials: HTTPAuthorizationCredentials = Depends(security),
auth_service: EnhancedAuthService = Depends(get_auth_service)
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Get user profile using repository pattern"""
"""Get user profile - works for JWT auth AND demo sessions"""
try:
if not credentials:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication required"
)
# Verify token and get user_id
payload = await auth_service.verify_user_token(credentials.credentials)
user_id = payload.get("user_id")
user_id = current_user.get("user_id")
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
detail="Invalid user context"
)
# Get user profile using enhanced service
profile = await auth_service.get_user_profile(user_id)
if not profile:
# Fetch user from database
from app.repositories import UserRepository
user_repo = UserRepository(User, db)
user = await user_repo.get_by_id(user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User profile not found"
)
return profile
return UserResponse(
id=str(user.id),
email=user.email,
full_name=user.full_name,
is_active=user.is_active,
is_verified=user.is_verified,
phone=user.phone,
language=user.language or "es",
timezone=user.timezone or "Europe/Madrid",
created_at=user.created_at,
last_login=user.last_login,
role=user.role,
tenant_id=current_user.get("tenant_id")
)
except HTTPException:
raise
except Exception as e:
logger.error("Get profile error using repository pattern", error=str(e))
logger.error("Get profile error", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get profile"
)
@router.put(route_builder.build_base_route("profile", include_tenant_prefix=False), response_model=UserResponse)
@router.put("/api/v1/auth/me", response_model=UserResponse)
async def update_profile(
update_data: dict,
credentials: HTTPAuthorizationCredentials = Depends(security),
auth_service: EnhancedAuthService = Depends(get_auth_service)
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Update user profile using repository pattern"""
"""Update user profile - works for JWT auth AND demo sessions"""
try:
if not credentials:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication required"
)
# Verify token and get user_id
payload = await auth_service.verify_user_token(credentials.credentials)
user_id = payload.get("user_id")
user_id = current_user.get("user_id")
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
detail="Invalid user context"
)
# Update profile using enhanced service
updated_profile = await auth_service.update_user_profile(user_id, update_data)
if not updated_profile:
# Prepare update data - filter out read-only fields
from app.repositories import UserRepository
user_repo = UserRepository(User, db)
# Update user profile
updated_user = await user_repo.update(user_id, update_data)
if not updated_user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
logger.info("Profile updated using repository pattern",
logger.info("Profile updated",
user_id=user_id,
updated_fields=list(update_data.keys()))
return updated_profile
return UserResponse(
id=str(updated_user.id),
email=updated_user.email,
full_name=updated_user.full_name,
is_active=updated_user.is_active,
is_verified=updated_user.is_verified,
phone=updated_user.phone,
language=updated_user.language,
timezone=updated_user.timezone,
created_at=updated_user.created_at,
last_login=updated_user.last_login,
role=updated_user.role,
tenant_id=current_user.get("tenant_id")
)
except HTTPException:
raise
except Exception as e:
logger.error("Update profile error using repository pattern", error=str(e))
logger.error("Update profile error", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update profile"
)
@router.post(route_builder.build_base_route("verify-email", include_tenant_prefix=False))
@router.post("/api/v1/auth/verify-email")
async def verify_email(
user_id: str,
verification_token: str,
@@ -473,7 +494,7 @@ async def verify_email(
)
@router.post(route_builder.build_base_route("reset-password", include_tenant_prefix=False))
@router.post("/api/v1/auth/reset-password")
async def reset_password(
reset_data: PasswordReset,
request: Request,
@@ -504,7 +525,7 @@ async def reset_password(
)
@router.get(route_builder.build_base_route("health", include_tenant_prefix=False))
@router.get("/api/v1/auth/health")
async def health_check():
"""Health check endpoint for enhanced auth service"""
return {

View File

@@ -59,7 +59,7 @@ def hash_text(text: str) -> str:
return hashlib.sha256(text.encode()).hexdigest()
@router.post("/consent", response_model=ConsentResponse, status_code=status.HTTP_201_CREATED)
@router.post("/api/v1/auth/me/consent", response_model=ConsentResponse, status_code=status.HTTP_201_CREATED)
async def record_consent(
consent_data: ConsentRequest,
request: Request,
@@ -71,7 +71,7 @@ async def record_consent(
GDPR Article 7 - Conditions for consent
"""
try:
user_id = UUID(current_user["sub"])
user_id = UUID(current_user["user_id"])
ip_address = request.client.host if request.client else None
user_agent = request.headers.get("user-agent")
@@ -129,14 +129,14 @@ async def record_consent(
except Exception as e:
await db.rollback()
logger.error("error_recording_consent", error=str(e), user_id=current_user.get("sub"))
logger.error("error_recording_consent", error=str(e), user_id=current_user.get("user_id"))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to record consent"
)
@router.get("/consent/current", response_model=Optional[ConsentResponse])
@router.get("/api/v1/auth/me/consent/current", response_model=Optional[ConsentResponse])
async def get_current_consent(
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
@@ -145,7 +145,7 @@ async def get_current_consent(
Get current active consent for user
"""
try:
user_id = UUID(current_user["sub"])
user_id = UUID(current_user["user_id"])
query = select(UserConsent).where(
and_(
@@ -174,14 +174,14 @@ async def get_current_consent(
)
except Exception as e:
logger.error("error_getting_consent", error=str(e), user_id=current_user.get("sub"))
logger.error("error_getting_consent", error=str(e), user_id=current_user.get("user_id"))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve consent"
)
@router.get("/consent/history", response_model=List[ConsentHistoryResponse])
@router.get("/api/v1/auth/me/consent/history", response_model=List[ConsentHistoryResponse])
async def get_consent_history(
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
@@ -191,7 +191,7 @@ async def get_consent_history(
GDPR Article 7(1) - Demonstrating consent
"""
try:
user_id = UUID(current_user["sub"])
user_id = UUID(current_user["user_id"])
query = select(ConsentHistory).where(
ConsentHistory.user_id == user_id
@@ -212,14 +212,14 @@ async def get_consent_history(
]
except Exception as e:
logger.error("error_getting_consent_history", error=str(e), user_id=current_user.get("sub"))
logger.error("error_getting_consent_history", error=str(e), user_id=current_user.get("user_id"))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve consent history"
)
@router.put("/consent", response_model=ConsentResponse)
@router.put("/api/v1/auth/me/consent", response_model=ConsentResponse)
async def update_consent(
consent_data: ConsentRequest,
request: Request,
@@ -231,7 +231,7 @@ async def update_consent(
GDPR Article 7(3) - Withdrawal of consent
"""
try:
user_id = UUID(current_user["sub"])
user_id = UUID(current_user["user_id"])
query = select(UserConsent).where(
and_(
@@ -309,14 +309,14 @@ async def update_consent(
except Exception as e:
await db.rollback()
logger.error("error_updating_consent", error=str(e), user_id=current_user.get("sub"))
logger.error("error_updating_consent", error=str(e), user_id=current_user.get("user_id"))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update consent"
)
@router.post("/consent/withdraw", status_code=status.HTTP_200_OK)
@router.post("/api/v1/auth/me/consent/withdraw", status_code=status.HTTP_200_OK)
async def withdraw_consent(
request: Request,
current_user: dict = Depends(get_current_user_dep),
@@ -327,7 +327,7 @@ async def withdraw_consent(
GDPR Article 7(3) - Right to withdraw consent
"""
try:
user_id = UUID(current_user["sub"])
user_id = UUID(current_user["user_id"])
query = select(UserConsent).where(
and_(
@@ -365,7 +365,7 @@ async def withdraw_consent(
except Exception as e:
await db.rollback()
logger.error("error_withdrawing_consent", error=str(e), user_id=current_user.get("sub"))
logger.error("error_withdrawing_consent", error=str(e), user_id=current_user.get("user_id"))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to withdraw consent"

View File

@@ -9,17 +9,15 @@ from fastapi.responses import JSONResponse
import structlog
from shared.auth.decorators import get_current_user_dep
from shared.routing import RouteBuilder
from app.core.database import get_db
from app.services.data_export_service import DataExportService
logger = structlog.get_logger()
router = APIRouter()
route_builder = RouteBuilder('auth')
@router.get("/api/v1/users/me/export")
@router.get("/api/v1/auth/me/export")
async def export_my_data(
current_user: dict = Depends(get_current_user_dep),
db = Depends(get_db)
@@ -40,7 +38,7 @@ async def export_my_data(
Response is provided in JSON format for easy data portability.
"""
try:
user_id = UUID(current_user["sub"])
user_id = UUID(current_user["user_id"])
export_service = DataExportService(db)
data = await export_service.export_user_data(user_id)
@@ -63,7 +61,7 @@ async def export_my_data(
except Exception as e:
logger.error(
"data_export_failed",
user_id=current_user.get("sub"),
user_id=current_user.get("user_id"),
error=str(e)
)
raise HTTPException(
@@ -72,7 +70,7 @@ async def export_my_data(
)
@router.get("/api/v1/users/me/export/summary")
@router.get("/api/v1/auth/me/export/summary")
async def get_export_summary(
current_user: dict = Depends(get_current_user_dep),
db = Depends(get_db)
@@ -84,7 +82,7 @@ async def get_export_summary(
before they request full export.
"""
try:
user_id = UUID(current_user["sub"])
user_id = UUID(current_user["user_id"])
export_service = DataExportService(db)
data = await export_service.export_user_data(user_id)
@@ -114,7 +112,7 @@ async def get_export_summary(
except Exception as e:
logger.error(
"export_summary_failed",
user_id=current_user.get("sub"),
user_id=current_user.get("user_id"),
error=str(e)
)
raise HTTPException(

View File

@@ -13,11 +13,9 @@ from app.core.database import get_db
from app.services.user_service import UserService
from app.repositories.onboarding_repository import OnboardingRepository
from shared.auth.decorators import get_current_user_dep
from shared.routing import RouteBuilder
logger = structlog.get_logger()
router = APIRouter(tags=["onboarding"])
route_builder = RouteBuilder('auth')
# Request/Response Models
class OnboardingStepStatus(BaseModel):
@@ -356,7 +354,7 @@ class OnboardingService:
# API Routes
@router.get(route_builder.build_base_route("me/onboarding/progress", include_tenant_prefix=False), response_model=UserProgress)
@router.get("/api/v1/auth/me/onboarding/progress", response_model=UserProgress)
async def get_user_progress(
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
@@ -375,7 +373,7 @@ async def get_user_progress(
detail="Failed to get onboarding progress"
)
@router.get(route_builder.build_base_route("{user_id}/onboarding/progress", include_tenant_prefix=False), response_model=UserProgress)
@router.get("/api/v1/auth/users/{user_id}/onboarding/progress", response_model=UserProgress)
async def get_user_progress_by_id(
user_id: str,
current_user: Dict[str, Any] = Depends(get_current_user_dep),
@@ -408,7 +406,7 @@ async def get_user_progress_by_id(
detail="Failed to get onboarding progress"
)
@router.put(route_builder.build_base_route("me/onboarding/step", include_tenant_prefix=False), response_model=UserProgress)
@router.put("/api/v1/auth/me/onboarding/step", response_model=UserProgress)
async def update_onboarding_step(
update_request: UpdateStepRequest,
current_user: Dict[str, Any] = Depends(get_current_user_dep),
@@ -433,7 +431,7 @@ async def update_onboarding_step(
detail="Failed to update onboarding step"
)
@router.get(route_builder.build_base_route("me/onboarding/next-step", include_tenant_prefix=False))
@router.get("/api/v1/auth/me/onboarding/next-step")
async def get_next_step(
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
@@ -452,7 +450,7 @@ async def get_next_step(
detail="Failed to get next step"
)
@router.get(route_builder.build_base_route("me/onboarding/can-access/{step_name}", include_tenant_prefix=False))
@router.get("/api/v1/auth/me/onboarding/can-access/{step_name}")
async def can_access_step(
step_name: str,
current_user: Dict[str, Any] = Depends(get_current_user_dep),
@@ -475,7 +473,7 @@ async def can_access_step(
detail="Failed to check step access"
)
@router.post(route_builder.build_base_route("me/onboarding/complete", include_tenant_prefix=False))
@router.post("/api/v1/auth/me/onboarding/complete")
async def complete_onboarding(
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)

View File

@@ -12,7 +12,7 @@ from datetime import datetime, timezone
from app.core.database import get_db, get_background_db_session
from app.schemas.auth import UserResponse, PasswordChange
from app.schemas.users import UserUpdate, BatchUserRequest, OwnerUserCreate
from app.services.user_service import UserService
from app.services.user_service import UserService, EnhancedUserService
from app.models.users import User
from sqlalchemy.ext.asyncio import AsyncSession
@@ -24,133 +24,15 @@ from shared.auth.decorators import (
get_current_user_dep,
require_admin_role_dep
)
from shared.routing import RouteBuilder
from shared.security import create_audit_logger, AuditSeverity, AuditAction
logger = structlog.get_logger()
router = APIRouter(tags=["users"])
route_builder = RouteBuilder('auth')
# Initialize audit logger
audit_logger = create_audit_logger("auth-service")
@router.get(route_builder.build_base_route("me", include_tenant_prefix=False), response_model=UserResponse)
async def get_current_user_info(
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Get current user information - FIXED VERSION"""
try:
logger.debug(f"Getting user info for: {current_user}")
# Handle both User object (direct auth) and dict (from gateway headers)
if isinstance(current_user, dict):
# Coming from gateway headers - need to fetch user from DB
user_id = current_user.get("user_id")
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid user context"
)
# ✅ FIX: Fetch full user from database to get the real role
from app.repositories import UserRepository
user_repo = UserRepository(User, db)
user = await user_repo.get_by_id(user_id)
logger.debug(f"Fetched user from DB - Role: {user.role}, Email: {user.email}")
# ✅ FIX: Return role from database, not from JWT headers
return UserResponse(
id=str(user.id),
email=user.email,
full_name=user.full_name,
is_active=user.is_active,
is_verified=user.is_verified,
phone=user.phone,
language=user.language or "es",
timezone=user.timezone or "Europe/Madrid",
created_at=user.created_at,
last_login=user.last_login,
role=user.role, # ✅ CRITICAL: Use role from database, not headers
tenant_id=current_user.get("tenant_id")
)
else:
# Direct User object (shouldn't happen in microservice architecture)
logger.debug(f"Direct user object received - Role: {current_user.role}")
return UserResponse(
id=str(current_user.id),
email=current_user.email,
full_name=current_user.full_name,
is_active=current_user.is_active,
is_verified=current_user.is_verified,
phone=current_user.phone,
language=current_user.language or "es",
timezone=current_user.timezone or "Europe/Madrid",
created_at=current_user.created_at,
last_login=current_user.last_login,
role=current_user.role, # ✅ Use role from database
tenant_id=None
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Get user info error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get user information"
)
@router.put(route_builder.build_base_route("me", include_tenant_prefix=False), response_model=UserResponse)
async def update_current_user(
user_update: UserUpdate,
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Update current user information"""
try:
user_id = current_user.get("user_id") if isinstance(current_user, dict) else current_user.id
from app.repositories import UserRepository
user_repo = UserRepository(User, db)
# Prepare update data
update_data = {}
if user_update.full_name is not None:
update_data["full_name"] = user_update.full_name
if user_update.phone is not None:
update_data["phone"] = user_update.phone
if user_update.language is not None:
update_data["language"] = user_update.language
if user_update.timezone is not None:
update_data["timezone"] = user_update.timezone
updated_user = await user_repo.update(user_id, update_data)
return UserResponse(
id=str(updated_user.id),
email=updated_user.email,
full_name=updated_user.full_name,
is_active=updated_user.is_active,
is_verified=updated_user.is_verified,
phone=updated_user.phone,
language=updated_user.language,
timezone=updated_user.timezone,
created_at=updated_user.created_at,
last_login=updated_user.last_login,
role=updated_user.role, # ✅ Include role
tenant_id=current_user.get("tenant_id") if isinstance(current_user, dict) else None
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Update user error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update user"
)
@router.delete(route_builder.build_base_route("delete/{user_id}", include_tenant_prefix=False))
@router.delete("/api/v1/auth/users/{user_id}")
async def delete_admin_user(
background_tasks: BackgroundTasks,
user_id: str = Path(..., description="User ID"),
@@ -244,7 +126,7 @@ async def execute_admin_user_deletion(user_id: str, requesting_user_id: str):
result=result)
@router.get(route_builder.build_base_route("delete/{user_id}/deletion-preview", include_tenant_prefix=False))
@router.get("/api/v1/auth/users/{user_id}/deletion-preview")
async def preview_user_deletion(
user_id: str = Path(..., description="User ID"),
db: AsyncSession = Depends(get_db)
@@ -294,7 +176,7 @@ async def preview_user_deletion(
return preview
@router.get(route_builder.build_base_route("users/{user_id}", include_tenant_prefix=False), response_model=UserResponse)
@router.get("/api/v1/auth/users/{user_id}", response_model=UserResponse)
async def get_user_by_id(
user_id: str = Path(..., description="User ID"),
db: AsyncSession = Depends(get_db)
@@ -353,7 +235,7 @@ async def get_user_by_id(
)
@router.post(route_builder.build_base_route("users/create-by-owner", include_tenant_prefix=False), response_model=UserResponse)
@router.post("/api/v1/auth/users/create-by-owner", response_model=UserResponse)
async def create_user_by_owner(
user_data: OwnerUserCreate,
current_user: Dict[str, Any] = Depends(get_current_user_dep),
@@ -448,7 +330,7 @@ async def create_user_by_owner(
)
@router.post(route_builder.build_base_route("users/batch", include_tenant_prefix=False), response_model=Dict[str, Any])
@router.post("/api/v1/auth/users/batch", response_model=Dict[str, Any])
async def get_users_batch(
request: BatchUserRequest,
db: AsyncSession = Depends(get_db)
@@ -526,3 +408,75 @@ async def get_users_batch(
detail="Failed to fetch users"
)
@router.get("/api/v1/auth/users/{user_id}/activity")
async def get_user_activity(
user_id: str = Path(..., description="User ID"),
current_user = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""
Get user activity information.
This endpoint returns detailed activity information for a user including:
- Last login timestamp
- Account creation date
- Active session count
- Last activity timestamp
- User status information
**Permissions:** User can view their own activity, admins can view any user's activity
"""
try:
# Validate UUID format
try:
uuid.UUID(user_id)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid user ID format"
)
# Check permissions - user can view their own activity, admins can view any
if current_user["user_id"] != user_id:
# Check if current user has admin privileges
user_role = current_user.get("role", "user")
if user_role not in ["admin", "super_admin", "manager"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions to view this user's activity"
)
# Initialize enhanced user service
from app.core.config import settings
from shared.database.base import create_database_manager
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
user_service = EnhancedUserService(database_manager)
# Get user activity data
activity_data = await user_service.get_user_activity(user_id)
if "error" in activity_data:
if activity_data["error"] == "User not found":
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
else:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get user activity: {activity_data['error']}"
)
logger.debug("Retrieved user activity", user_id=user_id)
return activity_data
except HTTPException:
raise
except Exception as e:
logger.error("Get user activity error", user_id=user_id, error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get user activity information"
)