From 599e335fbbe7f7ca252e4164b2974e4617227317 Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Thu, 17 Jul 2025 19:03:11 +0200 Subject: [PATCH] Fix events --- services/auth/app/api/users.py | 119 ++++++++++++++++ services/auth/app/services/user_service.py | 151 +++++++++++++++++++++ shared/messaging/events.py | 121 ++++++++++++----- 3 files changed, 355 insertions(+), 36 deletions(-) create mode 100644 services/auth/app/api/users.py create mode 100644 services/auth/app/services/user_service.py diff --git a/services/auth/app/api/users.py b/services/auth/app/api/users.py new file mode 100644 index 00000000..179e847c --- /dev/null +++ b/services/auth/app/api/users.py @@ -0,0 +1,119 @@ +""" +User management API routes +""" + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +import logging + +from app.core.database import get_db +from app.schemas.auth import UserResponse, PasswordChangeRequest +from app.services.user_service import UserService +from app.core.auth import get_current_user +from app.models.users import User + +logger = logging.getLogger(__name__) +router = APIRouter() + +@router.get("/me", response_model=UserResponse) +async def get_current_user_info( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """Get current user information""" + try: + 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, + tenant_id=str(current_user.tenant_id) if current_user.tenant_id else None, + role=current_user.role, + phone=current_user.phone, + language=current_user.language, + timezone=current_user.timezone, + created_at=current_user.created_at, + last_login=current_user.last_login + ) + except Exception as e: + logger.error(f"Get current user error: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to get user information" + ) + +@router.put("/me", response_model=UserResponse) +async def update_current_user( + user_update: dict, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """Update current user information""" + try: + updated_user = await UserService.update_user(current_user.id, user_update, db) + 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, + tenant_id=str(updated_user.tenant_id) if updated_user.tenant_id else None, + role=updated_user.role, + phone=updated_user.phone, + language=updated_user.language, + timezone=updated_user.timezone, + created_at=updated_user.created_at, + last_login=updated_user.last_login + ) + 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.post("/change-password") +async def change_password( + password_data: PasswordChangeRequest, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """Change user password""" + try: + await UserService.change_password( + current_user.id, + password_data.current_password, + password_data.new_password, + db + ) + return {"message": "Password changed successfully"} + except HTTPException: + raise + except Exception as e: + logger.error(f"Password change error: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to change password" + ) + +@router.delete("/me") +async def delete_current_user( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """Delete current user account""" + try: + await UserService.delete_user(current_user.id, db) + return {"message": "User account deleted successfully"} + except HTTPException: + raise + except Exception as e: + logger.error(f"Delete user error: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to delete user account" + ) \ No newline at end of file diff --git a/services/auth/app/services/user_service.py b/services/auth/app/services/user_service.py new file mode 100644 index 00000000..69415d8e --- /dev/null +++ b/services/auth/app/services/user_service.py @@ -0,0 +1,151 @@ +""" +User service for managing user operations +""" + +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, update, delete +from fastapi import HTTPException, status +from passlib.context import CryptContext +import logging + +from app.models.users import User +from app.core.config import settings + +logger = logging.getLogger(__name__) + +# Password hashing +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +class UserService: + """Service for user management operations""" + + @staticmethod + async def get_user_by_id(user_id: str, db: AsyncSession) -> User: + """Get user by ID""" + try: + result = await db.execute( + select(User).where(User.id == user_id) + ) + user = result.scalar_one_or_none() + + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + return user + + except Exception as e: + logger.error(f"Error getting user by ID {user_id}: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to get user" + ) + + @staticmethod + async def update_user(user_id: str, user_data: dict, db: AsyncSession) -> User: + """Update user information""" + try: + # Get current user + user = await UserService.get_user_by_id(user_id, db) + + # Update fields + update_data = {} + allowed_fields = ['full_name', 'phone', 'language', 'timezone'] + + for field in allowed_fields: + if field in user_data: + update_data[field] = user_data[field] + + if update_data: + await db.execute( + update(User) + .where(User.id == user_id) + .values(**update_data) + ) + await db.commit() + + # Refresh user object + await db.refresh(user) + + return user + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error updating user {user_id}: {e}") + await db.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to update user" + ) + + @staticmethod + async def change_password( + user_id: str, + current_password: str, + new_password: str, + db: AsyncSession + ): + """Change user password""" + try: + # Get current user + user = await UserService.get_user_by_id(user_id, db) + + # Verify current password + if not pwd_context.verify(current_password, user.hashed_password): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Current password is incorrect" + ) + + # Hash new password + new_hashed_password = pwd_context.hash(new_password) + + # Update password + await db.execute( + update(User) + .where(User.id == user_id) + .values(hashed_password=new_hashed_password) + ) + await db.commit() + + logger.info(f"Password changed for user {user_id}") + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error changing password for user {user_id}: {e}") + await db.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to change password" + ) + + @staticmethod + async def delete_user(user_id: str, db: AsyncSession): + """Delete user account""" + try: + # Get current user first + user = await UserService.get_user_by_id(user_id, db) + + # Soft delete by deactivating + await db.execute( + update(User) + .where(User.id == user_id) + .values(is_active=False) + ) + await db.commit() + + logger.info(f"User {user_id} deactivated (soft delete)") + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error deleting user {user_id}: {e}") + await db.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to delete user" + ) \ No newline at end of file diff --git a/shared/messaging/events.py b/shared/messaging/events.py index cf8f8162..ffd88a26 100644 --- a/shared/messaging/events.py +++ b/shared/messaging/events.py @@ -1,73 +1,122 @@ """ Event definitions for microservices communication +- Simple class-based approach to avoid dataclass issues """ -from dataclasses import dataclass from datetime import datetime from typing import Dict, Any, Optional import uuid -@dataclass class BaseEvent: """Base event class""" - event_id: str - event_type: str - service_name: str - timestamp: datetime - data: Dict[str, Any] - correlation_id: Optional[str] = None - - def __post_init__(self): - if not self.event_id: - self.event_id = str(uuid.uuid4()) - if not self.timestamp: - self.timestamp = datetime.now(datetime.timezone.utc) + def __init__(self, service_name: str, data: Dict[str, Any], event_type: str = "", correlation_id: Optional[str] = None): + self.service_name = service_name + self.data = data + self.event_type = event_type + self.event_id = str(uuid.uuid4()) + self.timestamp = datetime.utcnow() + self.correlation_id = correlation_id # Training Events -@dataclass class TrainingStartedEvent(BaseEvent): - event_type: str = "training.started" + def __init__(self, service_name: str, data: Dict[str, Any], correlation_id: Optional[str] = None): + super().__init__( + service_name=service_name, + data=data, + event_type="training.started", + correlation_id=correlation_id + ) -@dataclass class TrainingCompletedEvent(BaseEvent): - event_type: str = "training.completed" + def __init__(self, service_name: str, data: Dict[str, Any], correlation_id: Optional[str] = None): + super().__init__( + service_name=service_name, + data=data, + event_type="training.completed", + correlation_id=correlation_id + ) -@dataclass class TrainingFailedEvent(BaseEvent): - event_type: str = "training.failed" + def __init__(self, service_name: str, data: Dict[str, Any], correlation_id: Optional[str] = None): + super().__init__( + service_name=service_name, + data=data, + event_type="training.failed", + correlation_id=correlation_id + ) # Forecasting Events -@dataclass class ForecastGeneratedEvent(BaseEvent): - event_type: str = "forecast.generated" + def __init__(self, service_name: str, data: Dict[str, Any], correlation_id: Optional[str] = None): + super().__init__( + service_name=service_name, + data=data, + event_type="forecast.generated", + correlation_id=correlation_id + ) -@dataclass class ForecastRequestedEvent(BaseEvent): - event_type: str = "forecast.requested" + def __init__(self, service_name: str, data: Dict[str, Any], correlation_id: Optional[str] = None): + super().__init__( + service_name=service_name, + data=data, + event_type="forecast.requested", + correlation_id=correlation_id + ) # User Events -@dataclass class UserRegisteredEvent(BaseEvent): - event_type: str = "user.registered" + def __init__(self, service_name: str, data: Dict[str, Any], correlation_id: Optional[str] = None): + super().__init__( + service_name=service_name, + data=data, + event_type="user.registered", + correlation_id=correlation_id + ) -@dataclass class UserLoginEvent(BaseEvent): - event_type: str = "user.login" + def __init__(self, service_name: str, data: Dict[str, Any], correlation_id: Optional[str] = None): + super().__init__( + service_name=service_name, + data=data, + event_type="user.login", + correlation_id=correlation_id + ) # Tenant Events -@dataclass class TenantCreatedEvent(BaseEvent): - event_type: str = "tenant.created" + def __init__(self, service_name: str, data: Dict[str, Any], correlation_id: Optional[str] = None): + super().__init__( + service_name=service_name, + data=data, + event_type="tenant.created", + correlation_id=correlation_id + ) -@dataclass class TenantUpdatedEvent(BaseEvent): - event_type: str = "tenant.updated" + def __init__(self, service_name: str, data: Dict[str, Any], correlation_id: Optional[str] = None): + super().__init__( + service_name=service_name, + data=data, + event_type="tenant.updated", + correlation_id=correlation_id + ) # Notification Events -@dataclass class NotificationSentEvent(BaseEvent): - event_type: str = "notification.sent" + def __init__(self, service_name: str, data: Dict[str, Any], correlation_id: Optional[str] = None): + super().__init__( + service_name=service_name, + data=data, + event_type="notification.sent", + correlation_id=correlation_id + ) -@dataclass class NotificationFailedEvent(BaseEvent): - event_type: str = "notification.failed" \ No newline at end of file + def __init__(self, service_name: str, data: Dict[str, Any], correlation_id: Optional[str] = None): + super().__init__( + service_name=service_name, + data=data, + event_type="notification.failed", + correlation_id=correlation_id + ) \ No newline at end of file