Files
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

1294 lines
47 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

import asyncio
import json
import logging
from datetime import datetime, timedelta
from typing import List, Optional, Dict, Set
import math
import httpx
from fastapi import BackgroundTasks, Depends, FastAPI, HTTPException, Query, status, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy import func, select, update, desc, and_, or_
from sqlalchemy.ext.asyncio import AsyncSession
from services.emergency_service.models import EmergencyAlert, EmergencyResponse, EmergencyReport, SafetyCheck
from services.emergency_service.schemas import (
AlertStatus,
AlertType,
EmergencyAlertCreate,
EmergencyAlertResponse,
EmergencyAlertUpdate,
EmergencyResponseCreate,
EmergencyResponseResponse,
EmergencyStatistics,
EmergencyReportCreate,
EmergencyReportResponse,
NearbyAlertResponse,
SafetyCheckCreate,
SafetyCheckResponse,
EmergencyEventDetails,
UserInfo,
)
# Import User model from user_service
from services.user_service.models import User
from shared.auth import get_current_user_from_token
from shared.config import settings
from shared.database import AsyncSessionLocal
# Database dependency
async def get_db():
async with AsyncSessionLocal() as session:
try:
yield session
except Exception:
await session.rollback()
raise
finally:
await session.close()
app = FastAPI(
title="Emergency Service",
version="1.0.0",
description="""
Emergency Service API для системы безопасности женщин.
## Авторизация
Все эндпоинты требуют Bearer токен в заголовке Authorization.
Получить токен можно через User Service:
```
POST /api/v1/auth/login
```
Использование токена:
```
Authorization: Bearer <your_jwt_token>
```
""",
contact={
"name": "Women's Safety App Team",
"url": "https://example.com/support",
"email": "support@example.com",
},
)
# Configure logger
logger = logging.getLogger(__name__)
# Security scheme for OpenAPI documentation
security = HTTPBearer(
scheme_name="JWT Bearer Token",
description="JWT Bearer токен для авторизации. Получите токен через User Service /api/v1/auth/login"
)
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class WebSocketManager:
"""Manage WebSocket connections for emergency notifications"""
def __init__(self):
self.active_connections: Dict[int, WebSocket] = {}
self.connection_info: Dict[int, dict] = {} # Дополнительная информация о подключениях
async def connect(self, websocket: WebSocket, user_id: int):
"""Connect a WebSocket for a specific user"""
await websocket.accept()
self.active_connections[user_id] = websocket
# Сохраняем информацию о подключении
self.connection_info[user_id] = {
"connected_at": datetime.now(),
"client_host": websocket.client.host if websocket.client else "unknown",
"client_port": websocket.client.port if websocket.client else 0,
"last_ping": datetime.now(),
"message_count": 0,
"status": "connected"
}
print(f"WebSocket connected for user {user_id} from {websocket.client}")
# Отправляем приветственное сообщение
await self.send_personal_message(json.dumps({
"type": "connection_established",
"message": "WebSocket connection established successfully",
"user_id": user_id,
"timestamp": datetime.now().isoformat()
}), user_id)
def disconnect(self, user_id: int):
"""Disconnect a WebSocket for a specific user"""
if user_id in self.active_connections:
del self.active_connections[user_id]
if user_id in self.connection_info:
self.connection_info[user_id]["status"] = "disconnected"
self.connection_info[user_id]["disconnected_at"] = datetime.now()
print(f"WebSocket disconnected for user {user_id}")
async def send_personal_message(self, message: str, user_id: int):
"""Send a message to a specific user"""
if user_id in self.active_connections:
websocket = self.active_connections[user_id]
try:
await websocket.send_text(message)
# Обновляем статистику
if user_id in self.connection_info:
self.connection_info[user_id]["message_count"] += 1
self.connection_info[user_id]["last_ping"] = datetime.now()
except Exception as e:
print(f"Error sending message to user {user_id}: {e}")
self.disconnect(user_id)
async def broadcast_alert(self, alert_data: dict, user_ids: Optional[List[int]] = None):
"""Broadcast alert to specific users or all connected users"""
message = json.dumps({
"type": "emergency_alert",
"data": alert_data
})
target_users = user_ids if user_ids else list(self.active_connections.keys())
for user_id in target_users:
await self.send_personal_message(message, user_id)
async def send_alert_update(self, alert_id: int, alert_data: dict, user_ids: Optional[List[int]] = None):
"""Send alert update to specific users"""
message = json.dumps({
"type": "alert_update",
"alert_id": alert_id,
"data": alert_data
})
target_users = user_ids if user_ids else list(self.active_connections.keys())
for user_id in target_users:
await self.send_personal_message(message, user_id)
def get_connected_users_count(self) -> int:
"""Получить количество подключенных пользователей"""
return len(self.active_connections)
def get_connected_users_list(self) -> List[int]:
"""Получить список ID подключенных пользователей"""
return list(self.active_connections.keys())
def get_connection_info(self, user_id: Optional[int] = None) -> dict:
"""Получить информацию о подключениях"""
if user_id:
return self.connection_info.get(user_id, {})
# Возвращаем общую статистику
active_count = len(self.active_connections)
total_messages = sum(info.get("message_count", 0) for info in self.connection_info.values())
connection_details = {}
for user_id, info in self.connection_info.items():
if info.get("status") == "connected":
connected_at = info.get("connected_at")
last_ping = info.get("last_ping")
connection_details[user_id] = {
"connected_at": connected_at.isoformat() if connected_at else None,
"client_host": info.get("client_host"),
"client_port": info.get("client_port"),
"last_ping": last_ping.isoformat() if last_ping else None,
"message_count": info.get("message_count", 0),
"status": info.get("status"),
"duration_seconds": int((datetime.now() - connected_at).total_seconds())
if connected_at and info.get("status") == "connected" else None
}
return {
"active_connections": active_count,
"total_messages_sent": total_messages,
"connected_users": list(self.active_connections.keys()),
"connection_details": connection_details
}
async def ping_all_connections(self):
"""Проверить все WebSocket подключения"""
disconnected_users = []
for user_id, websocket in list(self.active_connections.items()):
try:
ping_message = json.dumps({
"type": "ping",
"timestamp": datetime.now().isoformat()
})
await websocket.send_text(ping_message)
# Обновляем время последнего пинга
if user_id in self.connection_info:
self.connection_info[user_id]["last_ping"] = datetime.now()
except Exception as e:
print(f"Connection lost for user {user_id}: {e}")
disconnected_users.append(user_id)
# Удаляем неактивные подключения
for user_id in disconnected_users:
self.disconnect(user_id)
return {
"active_connections": len(self.active_connections),
"disconnected_users": disconnected_users,
"ping_time": datetime.now().isoformat()
}
# Global WebSocket manager instance
ws_manager = WebSocketManager()
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
db: AsyncSession = Depends(get_db),
):
"""
Get current user from JWT Bearer token for OpenAPI documentation.
Требует Bearer токен в заголовке Authorization:
Authorization: Bearer <your_jwt_token>
Returns simplified User object to avoid SQLAlchemy issues.
"""
try:
# Получаем данные пользователя из токена напрямую
from shared.auth import verify_token
user_data = verify_token(credentials.credentials)
if user_data is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
# Возвращаем упрощенный объект пользователя
return type('User', (), {
'id': user_data["user_id"],
'email': user_data.get("email", "unknown@example.com"),
'username': user_data.get("username", f"user_{user_data['user_id']}")
})()
except HTTPException:
raise
except Exception as e:
logger.error(f"Authentication failed: {str(e)}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
async def get_current_user_websocket(token: str):
"""Get current user from WebSocket token - PRODUCTION READY"""
try:
from shared.auth import verify_token
import logging
# Логируем попытку аутентификации (без токена в логах!)
print(f"🔐 WebSocket auth: Attempting authentication for token length={len(token)}")
# ВАЖНО: Никаких заглушек! Только настоящие JWT токены
if token.startswith("temp_token") or token.startswith("test_token"):
print(f"❌ WebSocket auth: REJECTED - Temporary tokens not allowed in production!")
print(f"❌ Token prefix: {token[:20]}...")
return None
# Проверяем JWT токен
user_data = verify_token(token)
if not user_data:
print(f"❌ WebSocket auth: Invalid or expired JWT token")
return None
print(f"✅ WebSocket auth: JWT token valid for user_id={user_data['user_id']}, email={user_data.get('email', 'N/A')}")
# Создаем объект пользователя из токена
class AuthenticatedUser:
def __init__(self, user_id, email):
self.id = user_id
self.email = email
return AuthenticatedUser(user_data['user_id'], user_data.get('email', f'user_{user_data["user_id"]}'))
except Exception as e:
print(f"❌ WebSocket auth error: {e}")
return None
def calculate_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
"""Calculate distance between two points in kilometers using Haversine formula."""
# Convert latitude and longitude from degrees to radians
lat1, lon1, lat2, lon2 = map(math.radians, [lat1, lon1, lat2, lon2])
# Haversine formula
dlat = lat2 - lat1
dlon = lon2 - lon1
a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2
c = 2 * math.asin(math.sqrt(a))
# Radius of earth in kilometers
r = 6371
return c * r
@app.get("/health")
async def health_check():
"""Health check endpoint"""
return {"status": "healthy", "service": "emergency_service"}
@app.get("/alerts")
async def get_alerts_public(db: AsyncSession = Depends(get_db)):
"""Get all emergency alerts (public endpoint for testing)"""
result = await db.execute(
select(EmergencyAlert)
.order_by(EmergencyAlert.created_at.desc())
.limit(50)
)
alerts = result.scalars().all()
return [EmergencyAlertResponse.model_validate(alert) for alert in alerts]
@app.post("/alerts")
async def create_alert_public(
alert_data: dict,
db: AsyncSession = Depends(get_db)
):
"""Create emergency alert (public endpoint for testing)"""
try:
new_alert = EmergencyAlert(
user_id=alert_data.get("user_id", 1),
alert_type=alert_data.get("alert_type", "medical"),
latitude=alert_data.get("latitude", 0),
longitude=alert_data.get("longitude", 0),
title=alert_data.get("title", "Emergency Alert"),
description=alert_data.get("description", ""),
is_resolved=False
)
db.add(new_alert)
await db.commit()
await db.refresh(new_alert)
return {"status": "success", "alert_id": new_alert.id}
except Exception as e:
await db.rollback()
return {"status": "error", "detail": str(e)}
@app.websocket("/api/v1/emergency/ws/{user_id}")
async def websocket_endpoint(websocket: WebSocket, user_id: str):
"""WebSocket endpoint for emergency notifications"""
print(f"🔌 WebSocket connection attempt from {websocket.client}")
print(f"📝 user_id: {user_id}")
print(f"🔗 Query params: {dict(websocket.query_params)}")
# Get token from query parameter
token = websocket.query_params.get("token")
print(f"🎫 Token received: {token}")
if not token:
print("❌ No token provided, closing connection")
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
return
# Authenticate user
authenticated_user = await get_current_user_websocket(token)
if not authenticated_user:
print("❌ Authentication failed, closing connection")
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
return
# Get user ID as integer - authenticated_user is an instance with id attribute
auth_user_id = authenticated_user.id
print(f"✅ User authenticated: {authenticated_user.email} (ID: {auth_user_id})")
# Verify user_id matches authenticated user
try:
if int(user_id) != auth_user_id:
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
return
except ValueError:
if user_id != "current_user_id":
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
return
# Handle special case where client uses 'current_user_id' as placeholder
user_id = str(auth_user_id)
# Connect WebSocket
await ws_manager.connect(websocket, auth_user_id)
try:
# Send initial connection message
await ws_manager.send_personal_message(
json.dumps({
"type": "connection_established",
"message": "Connected to emergency notifications",
"user_id": auth_user_id
}),
auth_user_id
)
# Keep connection alive and listen for messages
while True:
try:
# Wait for messages (ping/pong, etc.)
data = await websocket.receive_text()
message = json.loads(data)
# Handle different message types
if message.get("type") == "ping":
await ws_manager.send_personal_message(
json.dumps({"type": "pong"}),
auth_user_id
)
except WebSocketDisconnect:
break
except Exception as e:
print(f"WebSocket error: {e}")
break
except WebSocketDisconnect:
pass
finally:
ws_manager.disconnect(auth_user_id)
async def get_nearby_users(
latitude: float, longitude: float, radius_km: float = 1.0
) -> List[dict]:
"""Get users within radius using Location Service"""
async with httpx.AsyncClient() as client:
try:
response = await client.get(
f"http://localhost:8003/api/v1/nearby-users",
params={
"latitude": latitude,
"longitude": longitude,
"radius_km": radius_km,
},
timeout=5.0,
)
if response.status_code == 200:
return response.json()
return []
except Exception:
return []
async def send_websocket_notifications_to_nearby_users(alert, nearby_users: List[dict]) -> int:
"""Send real-time WebSocket notifications to nearby users who are online"""
online_count = 0
# Create notification message
notification = {
"type": "emergency_alert",
"alert_id": alert.id,
"alert_type": alert.alert_type,
"latitude": alert.latitude,
"longitude": alert.longitude,
"address": alert.address,
"message": alert.message or "Экстренная ситуация рядом с вами!",
"created_at": alert.created_at.isoformat(),
"distance_km": None # Will be calculated per user
}
print(f"🔔 Sending WebSocket notifications to {len(nearby_users)} nearby users")
for user in nearby_users:
user_id = user.get("user_id")
distance = user.get("distance_km", 0)
# Update distance in notification
notification["distance_km"] = round(distance, 2)
# Check if user has active WebSocket connection
if user_id in ws_manager.active_connections:
try:
# Send notification via WebSocket
await ws_manager.send_personal_message(
json.dumps(notification, ensure_ascii=False, default=str),
user_id
)
online_count += 1
print(f"📡 Sent WebSocket notification to user {user_id} ({distance:.1f}km away)")
except Exception as e:
print(f"❌ Failed to send WebSocket to user {user_id}: {e}")
else:
print(f"💤 User {user_id} is offline - will receive push notification only")
print(f"✅ WebSocket notifications sent to {online_count}/{len(nearby_users)} online users")
return online_count
async def send_emergency_notifications(alert_id: int, nearby_users: List[dict]):
"""Send push notifications to nearby users"""
if not nearby_users:
return
print(f"📱 Sending push notifications to {len(nearby_users)} users via Notification Service")
async with httpx.AsyncClient() as client:
try:
response = await client.post(
"http://localhost:8005/api/v1/send-emergency-notifications",
json={
"alert_id": alert_id,
"user_ids": [user["user_id"] for user in nearby_users],
"message": "🚨 Экстренная ситуация рядом с вами! Проверьте приложение.",
"title": "Экстренное уведомление"
},
timeout=10.0,
)
if response.status_code == 200:
print(f"✅ Push notifications sent successfully")
else:
print(f"⚠️ Push notification service responded with {response.status_code}")
except Exception as e:
print(f"❌ Failed to send push notifications: {e}")
@app.post("/api/v1/alert", response_model=EmergencyAlertResponse)
async def create_emergency_alert(
alert_data: EmergencyAlertCreate,
background_tasks: BackgroundTasks,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Create new emergency alert"""
# Create alert
db_alert = EmergencyAlert(
user_id=current_user.id,
latitude=alert_data.latitude,
longitude=alert_data.longitude,
address=alert_data.address,
alert_type=alert_data.alert_type,
message=alert_data.message,
)
db.add(db_alert)
await db.commit()
await db.refresh(db_alert)
# Process alert in background
background_tasks.add_task(
process_emergency_alert_in_background,
int(db_alert.id), # Convert to int explicitly
alert_data.latitude,
alert_data.longitude
)
return EmergencyAlertResponse.model_validate(db_alert)
async def process_emergency_alert_in_background(alert_id: int, latitude: float, longitude: float):
"""Process emergency alert - notify nearby users via WebSocket and Push notifications"""
try:
print(f"🚨 Processing emergency alert {alert_id} at coordinates ({latitude}, {longitude})")
# Get nearby users within 5km radius
nearby_users = await get_nearby_users(latitude, longitude, radius_km=5.0)
print(f"📍 Found {len(nearby_users)} nearby users within 5km radius")
if nearby_users:
# Create new database session for background task
from shared.database import AsyncSessionLocal
async with AsyncSessionLocal() as db:
try:
# Get full alert details for notifications
result = await db.execute(select(EmergencyAlert).filter(EmergencyAlert.id == alert_id))
alert = result.scalars().first()
if not alert:
print(f"❌ Alert {alert_id} not found in database")
return
# Update alert with notification count
await db.execute(
update(EmergencyAlert)
.where(EmergencyAlert.id == alert_id)
.values(notified_users_count=len(nearby_users))
)
await db.commit()
print(f"✅ Updated alert {alert_id} with {len(nearby_users)} notified users")
# Send real-time WebSocket notifications to online users
online_notifications_sent = await send_websocket_notifications_to_nearby_users(alert, nearby_users)
# Send push notifications via notification service
await send_emergency_notifications(alert_id, nearby_users)
print(f"📱 Sent notifications: {online_notifications_sent} WebSocket + {len(nearby_users)} Push")
except Exception as e:
print(f"❌ Error processing emergency alert: {e}")
await db.rollback()
else:
print(f" No nearby users found for alert {alert_id}")
except Exception as e:
print(f"❌ Error in process_emergency_alert_in_background: {e}")
@app.post("/api/v1/alert/{alert_id}/respond", response_model=EmergencyResponseResponse)
async def respond_to_alert(
alert_id: int,
response_data: EmergencyResponseCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Respond to emergency alert"""
# Check if alert exists
result = await db.execute(select(EmergencyAlert).filter(EmergencyAlert.id == alert_id))
alert = result.scalars().first()
if not alert:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Alert not found"
)
# Check if already responded
existing_response = await db.execute(
select(EmergencyResponse).filter(
EmergencyResponse.alert_id == alert_id,
EmergencyResponse.responder_id == current_user.id
)
)
if existing_response.scalars().first():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="You have already responded to this alert"
)
# Create response
db_response = EmergencyResponse(
alert_id=alert_id,
responder_id=current_user.id,
response_type=response_data.response_type,
message=response_data.message,
eta_minutes=response_data.eta_minutes,
)
db.add(db_response)
# Update responded users count
await db.execute(
update(EmergencyAlert)
.where(EmergencyAlert.id == alert_id)
.values(responded_users_count=EmergencyAlert.responded_users_count + 1)
)
await db.commit()
await db.refresh(db_response)
# Create response with responder info
response_dict = db_response.__dict__.copy()
response_dict['responder_name'] = current_user.username
response_dict['responder_phone'] = getattr(current_user, 'phone_number', None)
return EmergencyResponseResponse.model_validate(response_dict)
@app.put("/api/v1/alert/{alert_id}/resolve")
async def resolve_alert(
alert_id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Resolve emergency alert"""
result = await db.execute(select(EmergencyAlert).filter(EmergencyAlert.id == alert_id))
alert = result.scalars().first()
if not alert:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Alert not found"
)
# Only alert creator can resolve
if alert.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only alert creator can resolve the alert"
)
# Update alert
await db.execute(
update(EmergencyAlert)
.where(EmergencyAlert.id == alert_id)
.values(
is_resolved=True,
resolved_at=datetime.utcnow(),
resolved_by=current_user.id
)
)
await db.commit()
return {"message": "Alert resolved successfully"}
@app.get("/api/v1/alerts/my", response_model=List[EmergencyAlertResponse])
async def get_my_alerts(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Get current user's emergency alerts"""
result = await db.execute(
select(EmergencyAlert)
.filter(EmergencyAlert.user_id == current_user.id)
.order_by(EmergencyAlert.created_at.desc())
)
alerts = result.scalars().all()
return [EmergencyAlertResponse.model_validate(alert) for alert in alerts]
@app.get("/api/v1/alerts/active", response_model=List[EmergencyAlertResponse])
async def get_active_alerts(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Get active emergency alerts"""
result = await db.execute(
select(EmergencyAlert)
.filter(EmergencyAlert.is_resolved == False)
.order_by(EmergencyAlert.created_at.desc())
.limit(50)
)
alerts = result.scalars().all()
return [EmergencyAlertResponse.model_validate(alert) for alert in alerts]
@app.get("/api/v1/emergency/reports", response_model=List[EmergencyAlertResponse])
async def get_emergency_reports(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Get emergency reports/alerts for admin users or general statistics"""
# For now, return all active alerts (in production, add admin check)
result = await db.execute(
select(EmergencyAlert)
.filter(EmergencyAlert.is_resolved == False)
.order_by(EmergencyAlert.created_at.desc())
.limit(50)
)
alerts = result.scalars().all()
return [EmergencyAlertResponse.model_validate(alert) for alert in alerts]
@app.get("/api/v1/stats", response_model=EmergencyStatistics)
async def get_emergency_stats(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Get emergency statistics"""
# Get total alerts count
total_alerts_result = await db.execute(select(func.count(EmergencyAlert.id)))
total_alerts = total_alerts_result.scalar() or 0
# Get active alerts count
active_alerts_result = await db.execute(
select(func.count(EmergencyAlert.id)).filter(EmergencyAlert.is_resolved == False)
)
active_alerts = active_alerts_result.scalar() or 0
# Calculate resolved alerts
resolved_alerts = total_alerts - active_alerts
# Get total responders count
total_responders_result = await db.execute(select(func.count(EmergencyResponse.id)))
total_responders = total_responders_result.scalar() or 0
return EmergencyStatistics(
total_alerts=total_alerts,
active_alerts=active_alerts,
resolved_alerts=resolved_alerts,
avg_response_time_minutes=0, # TODO: Calculate this
total_responders=total_responders,
)
@app.get("/api/v1/alerts/nearby", response_model=List[NearbyAlertResponse])
async def get_nearby_alerts(
latitude: float = Query(..., ge=-90, le=90),
longitude: float = Query(..., ge=-180, le=180),
radius_km: float = Query(default=10.0, ge=0.1, le=100),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Get nearby emergency alerts within specified radius"""
# For now, return all active alerts (in production, add distance filtering)
result = await db.execute(
select(EmergencyAlert)
.filter(EmergencyAlert.is_resolved == False)
.order_by(EmergencyAlert.created_at.desc())
.limit(20)
)
alerts = result.scalars().all()
nearby_alerts = []
for alert in alerts:
distance = calculate_distance(latitude, longitude, alert.latitude, alert.longitude)
if distance <= radius_km:
nearby_alerts.append(NearbyAlertResponse(
id=alert.id,
alert_type=alert.alert_type,
latitude=alert.latitude,
longitude=alert.longitude,
address=alert.address,
distance_km=round(distance, 2),
created_at=alert.created_at,
responded_users_count=alert.responded_users_count or 0
))
return sorted(nearby_alerts, key=lambda x: x.distance_km)
@app.post("/api/v1/report", response_model=EmergencyReportResponse)
async def create_emergency_report(
report_data: EmergencyReportCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Create emergency report"""
db_report = EmergencyReport(
user_id=current_user.id if not report_data.is_anonymous else None,
latitude=report_data.latitude,
longitude=report_data.longitude,
address=report_data.address,
report_type=report_data.report_type,
description=report_data.description,
is_anonymous=report_data.is_anonymous,
severity=report_data.severity
)
db.add(db_report)
await db.commit()
await db.refresh(db_report)
return EmergencyReportResponse.model_validate(db_report)
@app.get("/api/v1/reports", response_model=List[EmergencyReportResponse])
async def get_emergency_reports(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Get emergency reports"""
result = await db.execute(
select(EmergencyReport)
.order_by(EmergencyReport.created_at.desc())
.limit(50)
)
reports = result.scalars().all()
return [EmergencyReportResponse.model_validate(report) for report in reports]
@app.post("/api/v1/safety-check", response_model=SafetyCheckResponse)
async def create_safety_check(
check_data: SafetyCheckCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Create safety check-in"""
db_check = SafetyCheck(
user_id=current_user.id,
message=check_data.message,
location_latitude=check_data.location_latitude,
location_longitude=check_data.location_longitude
)
db.add(db_check)
await db.commit()
await db.refresh(db_check)
return SafetyCheckResponse.model_validate(db_check)
@app.get("/api/v1/safety-checks", response_model=List[SafetyCheckResponse])
async def get_safety_checks(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Get user's safety check-ins"""
result = await db.execute(
select(SafetyCheck)
.filter(SafetyCheck.user_id == current_user.id)
.order_by(SafetyCheck.created_at.desc())
.limit(50)
)
checks = result.scalars().all()
return [SafetyCheckResponse.model_validate(check) for check in checks]
@app.put("/api/v1/alert/{alert_id}", response_model=EmergencyAlertResponse)
async def update_emergency_alert(
alert_id: int,
update_data: EmergencyAlertUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Update emergency alert"""
result = await db.execute(select(EmergencyAlert).filter(EmergencyAlert.id == alert_id))
alert = result.scalars().first()
if not alert:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Alert not found"
)
# Only alert creator can update
if alert.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only alert creator can update the alert"
)
# Update fields
update_dict = {}
if update_data.is_resolved is not None:
update_dict['is_resolved'] = update_data.is_resolved
if update_data.is_resolved:
update_dict['resolved_at'] = datetime.utcnow()
update_dict['resolved_by'] = current_user.id
if update_data.message is not None:
update_dict['message'] = update_data.message
if update_dict:
await db.execute(
update(EmergencyAlert)
.where(EmergencyAlert.id == alert_id)
.values(**update_dict)
)
await db.commit()
await db.refresh(alert)
return EmergencyAlertResponse.model_validate(alert)
@app.get("/api/v1/alert/{alert_id}/responses", response_model=List[EmergencyResponseResponse])
async def get_alert_responses(
alert_id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Get responses for specific alert"""
# Check if alert exists
result = await db.execute(select(EmergencyAlert).filter(EmergencyAlert.id == alert_id))
alert = result.scalars().first()
if not alert:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Alert not found"
)
# Get responses
responses_result = await db.execute(
select(EmergencyResponse)
.filter(EmergencyResponse.alert_id == alert_id)
.order_by(EmergencyResponse.created_at.desc())
)
responses = responses_result.scalars().all()
return [EmergencyResponseResponse.model_validate(response) for response in responses]
@app.get("/api/v1/websocket/connections")
async def get_websocket_connections(
current_user: User = Depends(get_current_user)
):
"""Получить информацию о WebSocket подключениях"""
return ws_manager.get_connection_info()
@app.get("/api/v1/websocket/connections/{user_id}")
async def get_user_websocket_info(
user_id: int,
current_user: User = Depends(get_current_user)
):
"""Получить информацию о подключении конкретного пользователя"""
connection_info = ws_manager.get_connection_info(user_id)
if not connection_info:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User connection not found"
)
return connection_info
@app.post("/api/v1/websocket/ping")
async def ping_websocket_connections(
current_user: User = Depends(get_current_user)
):
"""Проверить все WebSocket подключения (пинг)"""
result = await ws_manager.ping_all_connections()
return result
@app.get("/api/v1/websocket/stats")
async def get_websocket_stats(
current_user: User = Depends(get_current_user)
):
"""Получить общую статистику WebSocket подключений"""
info = ws_manager.get_connection_info()
return {
"total_connections": info["active_connections"],
"connected_users": info["connected_users"],
"total_messages_sent": info["total_messages_sent"],
"connection_count": len(info["connected_users"]),
"timestamp": datetime.now().isoformat()
}
@app.post("/api/v1/websocket/broadcast")
async def broadcast_test_message(
message: str,
current_user: User = Depends(get_current_user)
):
"""Отправить тестовое сообщение всем подключенным пользователям"""
test_data = {
"type": "test_broadcast",
"message": message,
"from_user": current_user.id,
"timestamp": datetime.now().isoformat()
}
await ws_manager.broadcast_alert(test_data)
return {
"message": "Test broadcast sent",
"recipients": ws_manager.get_connected_users_list(),
"data": test_data
}
# MOBILE APP COMPATIBILITY ENDPOINTS
# Мобильное приложение ожидает endpoints с /api/v1/emergency/events
@app.post("/api/v1/emergency/events", response_model=EmergencyAlertResponse)
async def create_emergency_event(
alert_data: EmergencyAlertCreate,
background_tasks: BackgroundTasks,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Create emergency event (alias for create_alert for mobile compatibility)"""
# Используем существующую логику создания alert
return await create_emergency_alert(alert_data, background_tasks, current_user, db)
@app.get("/api/v1/emergency/events/nearby", response_model=List[NearbyAlertResponse])
async def get_nearby_emergency_events(
latitude: float = Query(..., description="User latitude"),
longitude: float = Query(..., description="User longitude"),
radius: float = Query(5.0, description="Search radius in km"),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Get nearby emergency events (alias for nearby alerts for mobile compatibility)"""
# Используем существующую логику поиска nearby alerts
return await get_nearby_alerts(latitude, longitude, radius, current_user, db)
@app.get("/api/v1/emergency/events", response_model=List[EmergencyAlertResponse])
async def get_emergency_events(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Get all emergency events (alias for active alerts for mobile compatibility)"""
# Используем существующую логику получения активных alerts
return await get_active_alerts(current_user, db)
@app.get("/api/v1/emergency/events/my", response_model=List[EmergencyAlertResponse])
async def get_my_emergency_events(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Get my emergency events (alias for my alerts for mobile compatibility)"""
# Используем существующую логику получения моих alerts
return await get_my_alerts(current_user, db)
@app.get("/api/v1/emergency/events/{event_id}", response_model=EmergencyEventDetails)
async def get_emergency_event(
event_id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Get full detailed information about emergency event by ID"""
try:
# Получаем alert с информацией о пользователе
alert_result = await db.execute(
select(EmergencyAlert, User)
.join(User, EmergencyAlert.user_id == User.id)
.filter(EmergencyAlert.id == event_id)
)
alert_data = alert_result.first()
if not alert_data:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Emergency event not found"
)
alert, user = alert_data
# Получаем все ответы на это событие с информацией о респондентах
responses_result = await db.execute(
select(EmergencyResponse, User)
.join(User, EmergencyResponse.responder_id == User.id)
.filter(EmergencyResponse.alert_id == event_id)
.order_by(EmergencyResponse.created_at.desc())
)
# Формируем список ответов
responses = []
for response_data in responses_result:
emergency_response, responder = response_data
# Формируем полное имя респондента
responder_name = responder.username
if responder.first_name and responder.last_name:
responder_name = f"{responder.first_name} {responder.last_name}"
elif responder.first_name:
responder_name = responder.first_name
elif responder.last_name:
responder_name = responder.last_name
response_dict = {
"id": emergency_response.id,
"alert_id": emergency_response.alert_id,
"responder_id": emergency_response.responder_id,
"response_type": emergency_response.response_type,
"message": emergency_response.message,
"eta_minutes": emergency_response.eta_minutes,
"created_at": emergency_response.created_at,
"responder_name": responder_name,
"responder_phone": responder.phone
}
responses.append(EmergencyResponseResponse(**response_dict))
# Создаем объект с информацией о пользователе
full_name = None
if user.first_name and user.last_name:
full_name = f"{user.first_name} {user.last_name}"
elif user.first_name:
full_name = user.first_name
elif user.last_name:
full_name = user.last_name
user_info = UserInfo(
id=user.id,
username=user.username,
full_name=full_name,
phone=user.phone
)
# Определяем статус события на основе is_resolved
from services.emergency_service.schemas import AlertStatus
event_status = AlertStatus.RESOLVED if alert.is_resolved else AlertStatus.ACTIVE
# Формируем полный ответ
event_details = EmergencyEventDetails(
id=alert.id,
uuid=alert.uuid,
user_id=alert.user_id,
latitude=alert.latitude,
longitude=alert.longitude,
address=alert.address,
alert_type=alert.alert_type,
message=alert.message,
status=event_status,
created_at=alert.created_at,
updated_at=alert.updated_at,
resolved_at=alert.resolved_at,
user=user_info,
responses=responses,
notifications_sent=len(responses), # Примерная статистика
websocket_notifications_sent=alert.notified_users_count or 0,
push_notifications_sent=alert.responded_users_count or 0,
contact_emergency_services=True, # Значение по умолчанию
notify_emergency_contacts=True # Значение по умолчанию
)
logger.info(f"Retrieved detailed event info for event_id={event_id}, responses_count={len(responses)}")
return event_details
except HTTPException:
raise
except Exception as e:
logger.error(f"Error retrieving emergency event {event_id}: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve emergency event details"
)
@app.get("/api/v1/emergency/events/{event_id}/brief", response_model=EmergencyAlertResponse)
async def get_emergency_event_brief(
event_id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Get brief information about emergency event by ID (for mobile apps)"""
# Получаем конкретный alert
result = await db.execute(select(EmergencyAlert).filter(EmergencyAlert.id == event_id))
alert = result.scalars().first()
if not alert:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Emergency event not found"
)
logger.info(f"Retrieved brief event info for event_id={event_id}")
return EmergencyAlertResponse.model_validate(alert)
@app.put("/api/v1/emergency/events/{event_id}/resolve")
async def resolve_emergency_event(
event_id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Resolve emergency event (alias for resolve alert)"""
# Используем существующую логику resolve alert
return await resolve_alert(event_id, current_user, db)
@app.post("/api/v1/emergency/events/{event_id}/respond", response_model=EmergencyResponseResponse)
async def respond_to_emergency_event(
event_id: int,
response: EmergencyResponseCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Respond to emergency event (alias for respond to alert)"""
# Используем существующую логику respond to alert
return await respond_to_alert(event_id, response, current_user, db)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8002)