fixes
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-09-25 15:32:19 +09:00
parent bd7a481803
commit dd7349bb4c
9 changed files with 646 additions and 80 deletions

View File

@@ -0,0 +1,21 @@
import uuid
from sqlalchemy import Column, ForeignKey, Integer, String, Text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship as orm_relationship
from shared.database import BaseModel
class EmergencyContact(BaseModel):
__tablename__ = "emergency_contacts"
uuid = Column(UUID(as_uuid=True), default=uuid.uuid4, unique=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), index=True, nullable=False)
name = Column(String(100), nullable=False)
phone_number = Column(String(20), nullable=False)
relation_type = Column(String(50)) # Переименовано из relationship в relation_type
notes = Column(Text)
# Отношение к пользователю
user = orm_relationship("User", back_populates="emergency_contacts")

View File

@@ -0,0 +1,46 @@
from typing import Optional
from uuid import UUID
from pydantic import BaseModel, Field
class EmergencyContactBase(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
phone_number: str = Field(..., min_length=5, max_length=20)
relationship: Optional[str] = Field(None, max_length=50)
notes: Optional[str] = Field(None, max_length=500)
class EmergencyContactCreate(EmergencyContactBase):
pass
class EmergencyContactUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=100)
phone_number: Optional[str] = Field(None, min_length=5, max_length=20)
relationship: Optional[str] = Field(None, max_length=50)
notes: Optional[str] = Field(None, max_length=500)
class EmergencyContactResponse(EmergencyContactBase):
id: int
uuid: UUID
user_id: int
model_config = {
"from_attributes": True,
"populate_by_name": True,
"json_schema_extra": {
"examples": [
{
"name": "John Doe",
"phone_number": "+1234567890",
"relationship": "Father",
"notes": "Call in case of emergency",
"id": 1,
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"user_id": 1,
}
]
}
}

View File

@@ -1,10 +1,17 @@
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,
@@ -55,24 +62,53 @@ async def health_check():
return {"status": "healthy", "service": "user_service"}
@app.post("/api/v1/register", response_model=UserResponse)
@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
# 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=user_data.phone,
phone=phone,
password_hash=hashed_password,
first_name=user_data.first_name,
last_name=user_data.last_name,
first_name=first_name or "", # Устанавливаем пустую строку, если None
last_name=last_name or "", # Устанавливаем пустую строку, если None
date_of_birth=user_data.date_of_birth,
bio=user_data.bio,
)
@@ -84,23 +120,55 @@ async def register_user(user_data: UserCreate, db: AsyncSession = Depends(get_db
return UserResponse.model_validate(db_user)
@app.post("/api/v1/login", response_model=Token)
@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"""
result = await db.execute(select(User).filter(User.email == user_credentials.email))
user = result.scalars().first()
if not user or not verify_password(user_credentials.password, user.password_hash):
# Определяем, по какому полю ищем пользователя
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",
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Account is deactivated",
)
# Проверка активности аккаунта
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(
@@ -112,12 +180,15 @@ async def login(user_credentials: UserLogin, db: AsyncSession = Depends(get_db))
@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),
@@ -135,7 +206,135 @@ async def update_profile(
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"}

View File

@@ -2,6 +2,7 @@ import uuid
from sqlalchemy import Boolean, Column, Date, Integer, String, Text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from shared.database import BaseModel
@@ -10,6 +11,7 @@ class User(BaseModel):
__tablename__ = "users"
uuid = Column(UUID(as_uuid=True), default=uuid.uuid4, unique=True, index=True)
username = Column(String(50), unique=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
phone = Column(String, unique=True, index=True)
password_hash = Column(String, nullable=False)
@@ -20,6 +22,9 @@ class User(BaseModel):
date_of_birth = Column(Date)
avatar_url = Column(String)
bio = Column(Text)
# Отношения
emergency_contacts = relationship("EmergencyContact", back_populates="user", cascade="all, delete-orphan")
# Emergency contacts
emergency_contact_1_name = Column(String(100))

View File

@@ -7,11 +7,37 @@ from pydantic import BaseModel, EmailStr, Field, field_validator
class UserBase(BaseModel):
email: EmailStr
username: Optional[str] = None
phone: Optional[str] = None
first_name: str = Field(..., min_length=1, max_length=50)
last_name: str = Field(..., min_length=1, max_length=50)
phone_number: Optional[str] = None # Альтернативное поле для phone
first_name: Optional[str] = ""
last_name: Optional[str] = ""
full_name: Optional[str] = None # Будет разделено на first_name и last_name
date_of_birth: Optional[date] = None
bio: Optional[str] = Field(None, max_length=500)
@field_validator("full_name")
@classmethod
def split_full_name(cls, v, info):
"""Разделяет полное имя на first_name и last_name."""
if v:
values = info.data
parts = v.split(" ", 1)
if "first_name" in values and not values["first_name"]:
info.data["first_name"] = parts[0]
if "last_name" in values and not values["last_name"]:
info.data["last_name"] = parts[1] if len(parts) > 1 else ""
return v
@field_validator("phone_number")
@classmethod
def map_phone_number(cls, v, info):
"""Копирует phone_number в phone если phone не указан."""
if v:
values = info.data
if "phone" in values and not values["phone"]:
info.data["phone"] = v
return v
class UserCreate(UserBase):
@@ -65,7 +91,8 @@ class UserResponse(UserBase):
class UserLogin(BaseModel):
email: EmailStr
email: Optional[EmailStr] = None
username: Optional[str] = None
password: str