from datetime import timedelta from typing import List from fastapi import Depends, FastAPI, HTTPException, status from fastapi.middleware.cors import CORSMiddleware from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from services.user_service.emergency_contact_model import EmergencyContact from services.user_service.emergency_contact_schema import ( EmergencyContactCreate, EmergencyContactResponse, EmergencyContactUpdate, ) from services.user_service.models import User from services.user_service.schemas import ( Token, UserCreate, UserLogin, UserResponse, UserUpdate, ) from shared.auth import ( create_access_token, get_current_user_from_token, get_password_hash, verify_password, ) from shared.config import settings from shared.database import get_db app = FastAPI(title="User Service", version="1.0.0") # CORS middleware app.add_middleware( CORSMiddleware, allow_origins=["*"], # В продакшене ограничить конкретными доменами allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) async def get_current_user( user_data: dict = Depends(get_current_user_from_token), db: AsyncSession = Depends(get_db), ): """Get current user from token via auth dependency.""" # Get full user object from database result = await db.execute(select(User).filter(User.id == user_data["user_id"])) user = result.scalars().first() if user is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found" ) return user @app.get("/health") async def health_check(): """Health check endpoint""" return {"status": "healthy", "service": "user_service"} @app.post("/api/v1/auth/register", response_model=UserResponse) @app.post("/api/v1/users/register", response_model=UserResponse) async def register_user(user_data: UserCreate, db: AsyncSession = Depends(get_db)): """Register a new user""" # Check if user already exists by email result = await db.execute(select(User).filter(User.email == user_data.email)) if result.scalars().first(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered" ) # Check if username provided and already exists if user_data.username: result = await db.execute(select(User).filter(User.username == user_data.username)) if result.scalars().first(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Username already taken" ) # Create new user try: hashed_password = get_password_hash(user_data.password) except ValueError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Password validation error: {str(e)}" ) # Используем phone_number как запасной вариант для phone phone = user_data.phone or user_data.phone_number # Определяем username на основе email, если он не указан username = user_data.username if not username: username = user_data.email.split('@')[0] # Определяем first_name и last_name first_name = user_data.first_name last_name = user_data.last_name # Если есть full_name, разделяем его на first_name и last_name if user_data.full_name: parts = user_data.full_name.split(" ", 1) first_name = parts[0] last_name = parts[1] if len(parts) > 1 else "" db_user = User( username=username, email=user_data.email, phone=phone, password_hash=hashed_password, first_name=first_name or "", # Устанавливаем пустую строку, если None last_name=last_name or "", # Устанавливаем пустую строку, если None date_of_birth=user_data.date_of_birth, bio=user_data.bio, ) db.add(db_user) await db.commit() await db.refresh(db_user) return UserResponse.model_validate(db_user) @app.post("/api/v1/auth/login", response_model=Token) async def login(user_credentials: UserLogin, db: AsyncSession = Depends(get_db)): """Authenticate user and return token""" print(f"Login attempt: email={user_credentials.email}, username={user_credentials.username}") # Проверка валидности входных данных if not user_credentials.email and not user_credentials.username: print("Error: Neither email nor username provided") raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Either email or username must be provided", ) if not user_credentials.password: print("Error: Password not provided") raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Password is required", ) # Определяем, по какому полю ищем пользователя user = None try: if user_credentials.email: print(f"Looking up user by email: {user_credentials.email}") result = await db.execute(select(User).filter(User.email == user_credentials.email)) user = result.scalars().first() elif user_credentials.username: print(f"Looking up user by username: {user_credentials.username}") result = await db.execute(select(User).filter(User.username == user_credentials.username)) user = result.scalars().first() except Exception as e: print(f"Database error during user lookup: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Database error during authentication", ) # Проверяем наличие пользователя и правильность пароля if not user: print(f"User not found: email={user_credentials.email}, username={user_credentials.username}") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect email or password", ) print(f"User found: id={user.id}, email={user.email}") # Проверка пароля try: password_valid = verify_password(user_credentials.password, str(user.password_hash)) print(f"Password verification result: {password_valid}") if not password_valid: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect email or password", ) except HTTPException: raise except Exception as e: # Если произошла ошибка при проверке пароля, то считаем, что пароль неверный print(f"Password verification error: {str(e)}") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect email or password", ) # Проверка активности аккаунта try: is_active = bool(user.is_active) print(f"User active status: {is_active}") if not is_active: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Account is inactive", ) except Exception as e: # Если произошла ошибка при проверке активности, считаем аккаунт активным print(f"Error checking user active status: {str(e)}") pass print("Creating access token...") access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( data={"sub": str(user.id), "email": user.email}, expires_delta=access_token_expires, ) print("Login successful") return {"access_token": access_token, "token_type": "bearer"} @app.get("/api/v1/profile", response_model=UserResponse) @app.get("/api/v1/users/me", response_model=UserResponse) async def get_profile(current_user: User = Depends(get_current_user)): """Get current user profile""" return UserResponse.model_validate(current_user) @app.put("/api/v1/profile", response_model=UserResponse) @app.put("/api/v1/users/me", response_model=UserResponse) @app.patch("/api/v1/users/me", response_model=UserResponse) async def update_profile( user_update: UserUpdate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Update user profile""" update_data = user_update.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(current_user, field, value) await db.commit() await db.refresh(current_user) return UserResponse.model_validate(current_user) # Маршруты для экстренных контактов @app.get("/api/v1/users/me/emergency-contacts", response_model=List[EmergencyContactResponse]) async def get_emergency_contacts( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Получение списка экстренных контактов пользователя""" result = await db.execute( select(EmergencyContact).filter(EmergencyContact.user_id == current_user.id) ) contacts = result.scalars().all() return contacts @app.post("/api/v1/users/me/emergency-contacts", response_model=EmergencyContactResponse) async def create_emergency_contact( contact_data: EmergencyContactCreate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Создание нового экстренного контакта""" # Преобразуем 'relationship' из схемы в 'relation_type' для модели contact_dict = contact_data.model_dump() relationship_value = contact_dict.pop('relationship', None) contact = EmergencyContact( **contact_dict, relation_type=relationship_value, user_id=current_user.id ) db.add(contact) await db.commit() await db.refresh(contact) return contact @app.get( "/api/v1/users/me/emergency-contacts/{contact_id}", response_model=EmergencyContactResponse, ) async def get_emergency_contact( contact_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Получение информации об экстренном контакте""" result = await db.execute( select(EmergencyContact).filter( EmergencyContact.id == contact_id, EmergencyContact.user_id == current_user.id ) ) contact = result.scalars().first() if contact is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Contact not found" ) return contact @app.patch( "/api/v1/users/me/emergency-contacts/{contact_id}", response_model=EmergencyContactResponse, ) async def update_emergency_contact( contact_id: int, contact_data: EmergencyContactUpdate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Обновление информации об экстренном контакте""" result = await db.execute( select(EmergencyContact).filter( EmergencyContact.id == contact_id, EmergencyContact.user_id == current_user.id ) ) contact = result.scalars().first() if contact is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Contact not found" ) update_data = contact_data.model_dump(exclude_unset=True) # Преобразуем 'relationship' из схемы в 'relation_type' для модели if 'relationship' in update_data: update_data['relation_type'] = update_data.pop('relationship') for field, value in update_data.items(): setattr(contact, field, value) await db.commit() await db.refresh(contact) return contact @app.delete( "/api/v1/users/me/emergency-contacts/{contact_id}", status_code=status.HTTP_204_NO_CONTENT, ) async def delete_emergency_contact( contact_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Удаление экстренного контакта""" result = await db.execute( select(EmergencyContact).filter( EmergencyContact.id == contact_id, EmergencyContact.user_id == current_user.id ) ) contact = result.scalars().first() if contact is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Contact not found" ) await db.delete(contact) await db.commit() return None @app.get("/api/v1/users/dashboard", tags=["Dashboard"]) async def get_user_dashboard( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Get user dashboard data""" try: # Get emergency contacts count emergency_contacts_result = await db.execute( select(EmergencyContact).filter( EmergencyContact.user_id == current_user.id, EmergencyContact.is_active == True ) ) emergency_contacts = emergency_contacts_result.scalars().all() dashboard_data = { "user": { "id": current_user.id, "uuid": str(current_user.uuid), "email": current_user.email, "username": current_user.username, "first_name": current_user.first_name, "last_name": current_user.last_name, "avatar_url": current_user.avatar_url }, "emergency_contacts_count": len(emergency_contacts), "settings": { "location_sharing_enabled": current_user.location_sharing_enabled, "emergency_notifications_enabled": current_user.emergency_notifications_enabled, "push_notifications_enabled": current_user.push_notifications_enabled }, "verification_status": { "email_verified": current_user.email_verified, "phone_verified": current_user.phone_verified }, "account_status": { "is_active": current_user.is_active, "created_at": str(current_user.created_at) if hasattr(current_user, 'created_at') else None } } return dashboard_data except Exception as e: print(f"Dashboard error: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to load dashboard data" ) @app.get("/api/v1/health") async def health_check_v1(): """Health check endpoint with API version""" return {"status": "healthy", "service": "user-service"} @app.get("/health") async def health_check(): """Health check endpoint""" return {"status": "healthy", "service": "user-service"} if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8001)