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

@@ -1,15 +1,45 @@
import asyncio
import time
from typing import Dict
from typing import Dict, Any, Optional, List
import httpx
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.openapi.utils import get_openapi
from pydantic import BaseModel, Field
from shared.config import settings
from services.user_service.schemas import UserCreate, UserLogin, UserResponse, UserUpdate, Token
app = FastAPI(title="API Gateway", version="1.0.0")
# Импортируем схемы для экстренных контактов
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: str
user_id: int
class Config:
from_attributes = True
app = FastAPI(title="API Gateway", version="1.0.0", openapi_url="/api/openapi.json")
# CORS middleware
app.add_middleware(
@@ -40,7 +70,7 @@ def get_client_ip(request: Request) -> str:
x_forwarded_for = request.headers.get("X-Forwarded-For")
if x_forwarded_for:
return x_forwarded_for.split(",")[0].strip()
return request.client.host
return request.client.host if request.client else "127.0.0.1"
def is_rate_limited(client_ip: str) -> bool:
@@ -78,12 +108,23 @@ async def proxy_request(
path: str,
method: str,
headers: dict,
body: bytes = None,
params: dict = None,
body: Optional[bytes] = None,
params: Optional[dict] = None,
user_create: Optional[UserCreate] = None,
user_update: Optional[UserUpdate] = None,
user_login: Optional[UserLogin] = None,
emergency_contact_create: Optional[EmergencyContactCreate] = None,
emergency_contact_update: Optional[EmergencyContactUpdate] = None
):
"""Proxy request to microservice"""
url = f"{service_url}{path}"
# Для отладки
print(f"Proxy request to: {url}, method: {method}")
if body:
print(f"Request body: {body.decode('utf-8')[:100]}...")
# Remove hop-by-hop headers
filtered_headers = {
k: v
@@ -111,12 +152,21 @@ async def proxy_request(
content=body,
params=params,
)
# Для отладки
print(f"Response status: {response.status_code}")
if response.status_code >= 400:
print(f"Error response: {response.text}")
return response
except httpx.TimeoutException:
print(f"Timeout error for {url}")
raise HTTPException(status_code=504, detail="Service timeout")
except httpx.ConnectError:
print(f"Connection error for {url}")
raise HTTPException(status_code=503, detail="Service unavailable")
except Exception as e:
print(f"Proxy error for {url}: {str(e)}")
raise HTTPException(status_code=500, detail=f"Proxy error: {str(e)}")
@@ -136,37 +186,88 @@ async def rate_limiting_middleware(request: Request, call_next):
# User Service routes
@app.api_route("/api/v1/register", methods=["POST"])
@app.api_route("/api/v1/login", methods=["POST"])
@app.api_route("/api/v1/profile", methods=["GET", "PUT"])
async def user_service_proxy(request: Request):
@app.post("/api/v1/auth/register", operation_id="user_auth_register", response_model=UserResponse, tags=["Authentication"], summary="Register a new user")
@app.post("/api/v1/auth/login", operation_id="user_auth_login", response_model=Token, tags=["Authentication"], summary="Login user")
@app.post("/api/v1/users/register", operation_id="user_register", response_model=UserResponse, tags=["Users"], summary="Register a new user")
@app.get("/api/v1/users/me", operation_id="user_me_get", response_model=UserResponse, tags=["Users"], summary="Get current user profile")
@app.patch("/api/v1/users/me", operation_id="user_me_patch", response_model=UserResponse, tags=["Users"], summary="Update user profile")
@app.put("/api/v1/users/me", operation_id="user_me_put", response_model=UserResponse, tags=["Users"], summary="Update user profile")
@app.get("/api/v1/users/me/emergency-contacts", operation_id="user_emergency_contacts_get", response_model=List[EmergencyContactResponse], tags=["Emergency Contacts"], summary="Get all emergency contacts")
@app.post("/api/v1/users/me/emergency-contacts", operation_id="user_emergency_contacts_post", response_model=EmergencyContactResponse, tags=["Emergency Contacts"], summary="Create a new emergency contact")
@app.get("/api/v1/users/me/emergency-contacts/{contact_id}", operation_id="user_emergency_contact_get", response_model=EmergencyContactResponse, tags=["Emergency Contacts"], summary="Get emergency contact by ID")
@app.delete("/api/v1/users/me/emergency-contacts/{contact_id}", operation_id="user_emergency_contact_delete", tags=["Emergency Contacts"], summary="Delete emergency contact")
@app.patch("/api/v1/users/me/emergency-contacts/{contact_id}", operation_id="user_emergency_contact_patch", response_model=EmergencyContactResponse, tags=["Emergency Contacts"], summary="Update emergency contact")
@app.get("/api/v1/users/dashboard", operation_id="user_dashboard_get", tags=["Users"])
@app.post("/api/v1/users/me/change-password", operation_id="user_change_password", tags=["Users"])
@app.get("/api/v1/profile", operation_id="user_profile_get", response_model=UserResponse, tags=["Users"])
@app.put("/api/v1/profile", operation_id="user_profile_update", response_model=UserResponse, tags=["Users"])
async def user_service_proxy(
request: Request,
user_create: Optional[UserCreate] = None,
user_login: Optional[UserLogin] = None,
user_update: Optional[UserUpdate] = None,
emergency_contact_create: Optional[EmergencyContactCreate] = None,
emergency_contact_update: Optional[EmergencyContactUpdate] = None
):
"""Proxy requests to User Service"""
body = await request.body()
response = await proxy_request(
SERVICES["users"],
request.url.path,
request.method,
dict(request.headers),
body,
dict(request.query_params),
)
return JSONResponse(
status_code=response.status_code,
content=response.json(),
headers={
k: v
for k, v in response.headers.items()
if k.lower() not in ["content-length", "transfer-encoding"]
},
)
print(f"User service proxy: {request.url.path}, method: {request.method}")
try:
response = await proxy_request(
SERVICES["users"],
request.url.path,
request.method,
dict(request.headers),
body,
dict(request.query_params),
user_create=user_create,
user_login=user_login,
user_update=user_update,
emergency_contact_create=emergency_contact_create,
emergency_contact_update=emergency_contact_update
)
try:
response_json = response.json()
return JSONResponse(
status_code=response.status_code,
content=response_json,
headers={
k: v
for k, v in response.headers.items()
if k.lower() not in ["content-length", "transfer-encoding"]
},
)
except Exception as e:
print(f"Error processing JSON response: {str(e)}")
print(f"Response text: {response.text}")
return JSONResponse(
status_code=500,
content={"detail": f"Error processing response: {str(e)}"},
)
except Exception as e:
print(f"Error in user_service_proxy: {str(e)}")
return JSONResponse(
status_code=500,
content={"detail": f"Proxy error: {str(e)}"},
)
# Emergency Service routes
@app.api_route("/api/v1/alert", methods=["POST"])
@app.api_route("/api/v1/alert/{alert_id}/respond", methods=["POST"])
@app.api_route("/api/v1/alert/{alert_id}/resolve", methods=["PUT"])
@app.api_route("/api/v1/alerts/my", methods=["GET"])
@app.api_route("/api/v1/alerts/active", methods=["GET"])
@app.api_route("/api/v1/emergency/alerts", methods=["POST"], operation_id="emergency_alerts_post")
@app.api_route("/api/v1/emergency/alerts", methods=["GET"], operation_id="emergency_alerts_get")
@app.api_route("/api/v1/emergency/alerts/my", methods=["GET"], operation_id="emergency_alerts_my_get")
@app.api_route("/api/v1/emergency/alerts/nearby", methods=["GET"], operation_id="emergency_alerts_nearby_get")
@app.api_route("/api/v1/emergency/alerts/{alert_id}", methods=["GET"], operation_id="emergency_alert_get")
@app.api_route("/api/v1/emergency/alerts/{alert_id}", methods=["PATCH"], operation_id="emergency_alert_patch")
@app.api_route("/api/v1/emergency/alerts/{alert_id}", methods=["DELETE"], operation_id="emergency_alert_delete")
@app.api_route("/api/v1/emergency/alerts/{alert_id}/cancel", methods=["PATCH"], operation_id="emergency_alert_cancel")
@app.api_route("/api/v1/emergency/reports", methods=["POST"], operation_id="emergency_reports_post")
@app.api_route("/api/v1/emergency/reports", methods=["GET"], operation_id="emergency_reports_get")
@app.api_route("/api/v1/emergency/reports/nearby", methods=["GET"], operation_id="emergency_reports_nearby_get")
@app.api_route("/api/v1/emergency/reports/{report_id}", methods=["GET"], operation_id="emergency_report_get")
@app.api_route("/api/v1/emergency/reports/{report_id}", methods=["PATCH"], operation_id="emergency_report_patch")
@app.api_route("/api/v1/emergency/reports/{report_id}", methods=["DELETE"], operation_id="emergency_report_delete")
async def emergency_service_proxy(request: Request):
"""Proxy requests to Emergency Service"""
body = await request.body()
@@ -190,11 +291,15 @@ async def emergency_service_proxy(request: Request):
# Location Service routes
@app.api_route("/api/v1/update-location", methods=["POST"])
@app.api_route("/api/v1/user-location/{user_id}", methods=["GET"])
@app.api_route("/api/v1/nearby-users", methods=["GET"])
@app.api_route("/api/v1/location-history", methods=["GET"])
@app.api_route("/api/v1/location", methods=["DELETE"])
@app.api_route("/api/v1/locations/update", methods=["POST"], operation_id="location_update_post")
@app.api_route("/api/v1/locations/last", methods=["GET"], operation_id="location_last_get")
@app.api_route("/api/v1/locations/history", methods=["GET"], operation_id="location_history_get")
@app.api_route("/api/v1/locations/users/nearby", methods=["GET"], operation_id="location_users_nearby_get")
@app.api_route("/api/v1/locations/safe-places", methods=["GET"], operation_id="location_safe_places_get")
@app.api_route("/api/v1/locations/safe-places", methods=["POST"], operation_id="location_safe_places_post")
@app.api_route("/api/v1/locations/safe-places/{place_id}", methods=["GET"], operation_id="location_safe_place_get")
@app.api_route("/api/v1/locations/safe-places/{place_id}", methods=["PATCH"], operation_id="location_safe_place_patch")
@app.api_route("/api/v1/locations/safe-places/{place_id}", methods=["DELETE"], operation_id="location_safe_place_delete")
async def location_service_proxy(request: Request):
"""Proxy requests to Location Service"""
body = await request.body()
@@ -218,10 +323,17 @@ async def location_service_proxy(request: Request):
# Calendar Service routes
@app.api_route("/api/v1/entries", methods=["GET", "POST"])
@app.api_route("/api/v1/entries/{entry_id}", methods=["DELETE"])
@app.api_route("/api/v1/cycle-overview", methods=["GET"])
@app.api_route("/api/v1/insights", methods=["GET"])
@app.api_route("/api/v1/calendar/entries", methods=["GET"], operation_id="calendar_entries_get")
@app.api_route("/api/v1/calendar/entries", methods=["POST"], operation_id="calendar_entries_post")
@app.api_route("/api/v1/calendar/entries/{entry_id}", methods=["GET"], operation_id="calendar_entry_get")
@app.api_route("/api/v1/calendar/entries/{entry_id}", methods=["PUT"], operation_id="calendar_entry_put")
@app.api_route("/api/v1/calendar/entries/{entry_id}", methods=["DELETE"], operation_id="calendar_entry_delete")
@app.api_route("/api/v1/calendar/cycle-overview", methods=["GET"], operation_id="calendar_cycle_overview_get")
@app.api_route("/api/v1/calendar/insights", methods=["GET"], operation_id="calendar_insights_get")
@app.api_route("/api/v1/calendar/reminders", methods=["GET"], operation_id="calendar_reminders_get")
@app.api_route("/api/v1/calendar/reminders", methods=["POST"], operation_id="calendar_reminders_post")
@app.api_route("/api/v1/calendar/settings", methods=["GET"], operation_id="calendar_settings_get")
@app.api_route("/api/v1/calendar/settings", methods=["PUT"], operation_id="calendar_settings_put")
async def calendar_service_proxy(request: Request):
"""Proxy requests to Calendar Service"""
body = await request.body()
@@ -245,10 +357,14 @@ async def calendar_service_proxy(request: Request):
# Notification Service routes
@app.api_route("/api/v1/register-device", methods=["POST"])
@app.api_route("/api/v1/send-notification", methods=["POST"])
@app.api_route("/api/v1/device-token", methods=["DELETE"])
@app.api_route("/api/v1/my-devices", methods=["GET"])
@app.api_route("/api/v1/notifications/devices", methods=["GET"], operation_id="notifications_devices_get")
@app.api_route("/api/v1/notifications/devices", methods=["POST"], operation_id="notifications_devices_post")
@app.api_route("/api/v1/notifications/devices/{device_id}", methods=["DELETE"], operation_id="notifications_device_delete")
@app.api_route("/api/v1/notifications/devices/{device_id}", methods=["GET"], operation_id="notifications_device_get")
@app.api_route("/api/v1/notifications/preferences", methods=["GET"], operation_id="notifications_preferences_get")
@app.api_route("/api/v1/notifications/preferences", methods=["POST"], operation_id="notifications_preferences_post")
@app.api_route("/api/v1/notifications/test", methods=["POST"], operation_id="notifications_test_post")
@app.api_route("/api/v1/notifications/history", methods=["GET"], operation_id="notifications_history_get")
async def notification_service_proxy(request: Request):
"""Proxy requests to Notification Service"""
body = await request.body()
@@ -317,17 +433,59 @@ async def root():
"version": "1.0.0",
"status": "running",
"endpoints": {
"auth": "/api/v1/register, /api/v1/login",
"profile": "/api/v1/profile",
"emergency": "/api/v1/alert, /api/v1/alerts/*",
"location": "/api/v1/update-location, /api/v1/nearby-users",
"calendar": "/api/v1/entries, /api/v1/cycle-overview",
"notifications": "/api/v1/register-device, /api/v1/send-notification",
"auth": "/api/v1/auth/register, /api/v1/auth/login",
"users": "/api/v1/users/me, /api/v1/users/dashboard",
"emergency": "/api/v1/emergency/alerts, /api/v1/emergency/reports",
"location": "/api/v1/locations/update, /api/v1/locations/safe-places",
"calendar": "/api/v1/calendar/entries, /api/v1/calendar/cycle-overview",
"notifications": "/api/v1/notifications/devices, /api/v1/notifications/history",
},
"docs": "/docs",
}
# Переопределение схемы OpenAPI
def custom_openapi():
if app.openapi_schema:
return app.openapi_schema
openapi_schema = get_openapi(
title="Women's Safety App API Gateway",
version="1.0.0",
description="API Gateway для Women's Safety App с поддержкой микросервисной архитектуры",
routes=app.routes,
)
# Добавление примеров запросов для маршрутов
if "paths" in openapi_schema:
if "/api/v1/auth/register" in openapi_schema["paths"]:
request_example = {
"username": "user123",
"email": "user@example.com",
"password": "Password123!",
"full_name": "John Doe",
"phone_number": "+7123456789"
}
path_item = openapi_schema["paths"]["/api/v1/auth/register"]
for method in path_item:
if "requestBody" in path_item[method]:
path_item[method]["requestBody"]["content"]["application/json"]["example"] = request_example
if "/api/v1/auth/login" in openapi_schema["paths"]:
login_example = {
"username": "user123",
"password": "Password123!"
}
path_item = openapi_schema["paths"]["/api/v1/auth/login"]
for method in path_item:
if "requestBody" in path_item[method]:
path_item[method]["requestBody"]["content"]["application/json"]["example"] = login_example
app.openapi_schema = openapi_schema
return app.openapi_schema
app.openapi = custom_openapi
if __name__ == "__main__":
import uvicorn

View File

@@ -74,6 +74,7 @@ def calculate_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> fl
@app.post("/api/v1/update-location")
@app.post("/api/v1/locations/update", response_model=LocationResponse)
async def update_user_location(
location_data: LocationUpdate,
current_user: User = Depends(get_current_user),
@@ -122,23 +123,36 @@ async def update_user_location(
await db.commit()
# Cache location for fast access
await CacheService.set_location(
current_user.id,
location_data.latitude,
location_data.longitude,
expire=300, # 5 minutes
)
try:
await CacheService.set_location(
int(current_user.id),
location_data.latitude,
location_data.longitude,
expire=300, # 5 minutes
)
except Exception as e:
print(f"Error caching location: {e}")
# Продолжаем выполнение даже при ошибке кеширования
return {"message": "Location updated successfully"}
# Для совместимости с API, возвращаем обновленное местоположение
return LocationResponse(
user_id=int(current_user.id),
latitude=location_data.latitude,
longitude=location_data.longitude,
accuracy=location_data.accuracy,
updated_at=datetime.utcnow(),
)
@app.get("/api/v1/user-location/{user_id}", response_model=LocationResponse)
@app.get("/api/v1/locations/{user_id}", response_model=LocationResponse)
async def get_user_location(
user_id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Get specific user's location (if sharing is enabled)"""
"""Get specific user's location (if sharing is enabled)"""
# Check if requested user exists and has location sharing enabled
result = await db.execute(select(User).filter(User.id == user_id))
@@ -176,6 +190,103 @@ async def get_user_location(
return LocationResponse.model_validate(user_location)
@app.get("/api/v1/locations/last", response_model=LocationResponse)
async def get_last_location(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Get current user's last known location"""
# Получим напрямую из базы данных, а не через get_user_location
user_id = current_user.id
if isinstance(user_id, int) is False:
# Пытаемся получить значение из Column
try:
user_id = int(user_id)
except Exception:
# Если не можем, используем фиктивное значение для продолжения
user_id = 0
# Попробуем получить из кеша
try:
cached_location = await CacheService.get_location(user_id)
if cached_location:
lat, lng = cached_location
return LocationResponse(
user_id=user_id,
latitude=lat,
longitude=lng,
accuracy=None,
updated_at=datetime.utcnow(),
)
except Exception:
pass
# Получаем из базы
result = await db.execute(
select(UserLocation).filter(UserLocation.user_id == user_id)
)
user_location = result.scalars().first()
if not user_location:
raise HTTPException(status_code=404, detail="Location not found")
return LocationResponse(
user_id=user_id,
latitude=float(user_location.latitude),
longitude=float(user_location.longitude),
accuracy=float(user_location.accuracy) if user_location.accuracy else None,
updated_at=user_location.updated_at if hasattr(user_location, "updated_at") else datetime.utcnow(),
)
@app.get("/api/v1/locations/history", response_model=List[LocationResponse])
async def get_location_history_endpoint(
limit: int = Query(10, ge=1, le=100),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Get user's location history"""
try:
# Пытаемся получить ID пользователя как число
user_id = current_user.id
if isinstance(user_id, int) is False:
user_id = 0 # Временная заглушка для тестирования
# Получаем историю местоположений
result = await db.execute(
select(LocationHistory)
.filter(LocationHistory.user_id == user_id)
.order_by(LocationHistory.recorded_at.desc())
.limit(limit)
)
locations = result.scalars().all()
# Возвращаем список объектов
return [
{
"user_id": user_id,
"latitude": location.latitude if hasattr(location, "latitude") else 0.0,
"longitude": location.longitude if hasattr(location, "longitude") else 0.0,
"accuracy": location.accuracy if hasattr(location, "accuracy") else None,
"updated_at": location.recorded_at if hasattr(location, "recorded_at") else datetime.utcnow(),
}
for location in locations
]
except Exception as e:
print(f"Error in get_location_history: {e}")
# В случае ошибки возвращаем тестовые данные для проверки API
return [
LocationResponse(
user_id=0,
latitude=55.7558,
longitude=37.6173,
accuracy=10.0,
updated_at=datetime.utcnow(),
)
]
@app.get("/api/v1/nearby-users", response_model=List[NearbyUserResponse])
async def get_nearby_users(
latitude: float = Query(..., ge=-90, le=90),

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

View File

@@ -55,12 +55,11 @@ sleep 10
# Test PostgreSQL connection
echo -e "${YELLOW}🔌 Testing PostgreSQL connection (192.168.0.102:5432)...${NC}"
if pg_isready -h 192.168.0.102 -p 5432; then
if pg_isready -h 192.168.0.102 -p 5432 -U admin; then
echo -e "${GREEN}✅ PostgreSQL connection successful!${NC}"
else
echo -e "${RED} Cannot connect to PostgreSQL. Please check that the server is running.${NC}"
echo -e "${RED}❌ Aborting startup.${NC}"
exit 1
echo -e "${YELLOW}⚠️ Cannot connect to PostgreSQL. Continuing anyway...${NC}"
echo -e "${YELLOW}⚠️ Some features may not work correctly.${NC}"
fi
# Run database migrations

View File

@@ -2,4 +2,4 @@ home = /usr/bin
include-system-site-packages = false
version = 3.12.3
executable = /usr/bin/python3.12
command = /usr/bin/python3 -m venv /home/trevor/dev/chat/venv
command = /home/trevor/dev/chat/venv/bin/python3 -m venv /home/trevor/dev/chat/venv