Files
chat/services/user_service/main.py
Andrew K. Choi dd7349bb4c
All checks were successful
continuous-integration/drone/push Build is passing
fixes
2025-09-25 15:32:19 +09:00

347 lines
12 KiB
Python
Raw 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.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
hashed_password = get_password_hash(user_data.password)
# Используем 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"""
# Определяем, по какому полю ищем пользователя
user = None
if user_credentials.email:
result = await db.execute(select(User).filter(User.email == user_credentials.email))
user = result.scalars().first()
elif user_credentials.username:
result = await db.execute(select(User).filter(User.username == user_credentials.username))
user = result.scalars().first()
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Either email or username must be provided",
)
# Проверяем наличие пользователя и правильность пароля
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
)
# Проверка пароля
try:
if not verify_password(user_credentials.password, str(user.password_hash)):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
)
except Exception:
# Если произошла ошибка при проверке пароля, то считаем, что пароль неверный
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
)
# Проверка активности аккаунта
try:
is_active = bool(user.is_active)
if not is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Account is inactive",
)
except Exception:
# Если произошла ошибка при проверке активности, считаем аккаунт активным
pass
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,
)
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/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)