Files
chat/services/user_service/main.py
Andrew K. Choi cfc93cb99a
All checks were successful
continuous-integration/drone/push Build is passing
feat: Fix nutrition service and add location-based alerts
Changes:
- Fix nutrition service: add is_active column and Pydantic validation for UUID/datetime
- Add location-based alerts feature: users can now see alerts within 1km radius
- Fix CORS and response serialization in nutrition service
- Add getCurrentLocation() and loadAlertsNearby() functions
- Improve UI for nearby alerts display with distance and response count
2025-12-13 16:34:50 +09:00

446 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.get("/users")
async def get_all_users(db: AsyncSession = Depends(get_db)):
"""Get all users (public endpoint for testing)"""
result = await db.execute(select(User).limit(100))
users = result.scalars().all()
return [UserResponse.model_validate(user) for user in users] if users else []
@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)