All checks were successful
continuous-integration/drone/push Build is passing
347 lines
12 KiB
Python
347 lines
12 KiB
Python
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)
|