init commit
This commit is contained in:
319
services/emergency_service/main.py
Normal file
319
services/emergency_service/main.py
Normal file
@@ -0,0 +1,319 @@
|
||||
from fastapi import FastAPI, HTTPException, Depends, status, BackgroundTasks
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
from shared.config import settings
|
||||
from shared.database import get_db, AsyncSessionLocal
|
||||
from shared.auth import get_current_user_from_token
|
||||
from services.emergency_service.models import EmergencyAlert, EmergencyResponse
|
||||
from services.emergency_service.schemas import (
|
||||
EmergencyAlertCreate, EmergencyAlertResponse,
|
||||
EmergencyResponseCreate, EmergencyResponseResponse,
|
||||
EmergencyStats
|
||||
)
|
||||
from services.user_service.models import User
|
||||
import httpx
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
app = FastAPI(title="Emergency Service", version="1.0.0")
|
||||
|
||||
# CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.CORS_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": "emergency_service"}
|
||||
|
||||
|
||||
async def get_nearby_users(latitude: float, longitude: float, radius_km: float = 1.0) -> list:
|
||||
"""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_emergency_notifications(alert_id: int, nearby_users: list):
|
||||
"""Send push notifications to nearby users"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
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]
|
||||
},
|
||||
timeout=10.0
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Failed to send 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 and notify nearby users"""
|
||||
|
||||
# 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.value,
|
||||
message=alert_data.message,
|
||||
)
|
||||
|
||||
db.add(db_alert)
|
||||
await db.commit()
|
||||
await db.refresh(db_alert)
|
||||
|
||||
# Get nearby users and send notifications in background
|
||||
background_tasks.add_task(
|
||||
process_emergency_alert,
|
||||
db_alert.id,
|
||||
alert_data.latitude,
|
||||
alert_data.longitude
|
||||
)
|
||||
|
||||
return EmergencyAlertResponse.model_validate(db_alert)
|
||||
|
||||
|
||||
async def process_emergency_alert(alert_id: int, latitude: float, longitude: float):
|
||||
"""Process emergency alert - get nearby users and send notifications"""
|
||||
# Get nearby users
|
||||
nearby_users = await get_nearby_users(latitude, longitude, settings.MAX_EMERGENCY_RADIUS_KM)
|
||||
|
||||
# Update alert with notified users count
|
||||
async with AsyncSessionLocal() as db:
|
||||
result = await db.execute(select(EmergencyAlert).filter(EmergencyAlert.id == alert_id))
|
||||
alert = result.scalars().first()
|
||||
if alert:
|
||||
alert.notified_users_count = len(nearby_users)
|
||||
await db.commit()
|
||||
|
||||
# Send notifications
|
||||
if nearby_users:
|
||||
await send_emergency_notifications(alert_id, nearby_users)
|
||||
|
||||
|
||||
@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=404, detail="Alert not found")
|
||||
|
||||
if alert.is_resolved:
|
||||
raise HTTPException(status_code=400, detail="Alert already resolved")
|
||||
|
||||
# Check if user 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=400, detail="You 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.value,
|
||||
message=response_data.message,
|
||||
eta_minutes=response_data.eta_minutes,
|
||||
)
|
||||
|
||||
db.add(db_response)
|
||||
|
||||
# Update responded users count
|
||||
alert.responded_users_count += 1
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(db_response)
|
||||
|
||||
return EmergencyResponseResponse.model_validate(db_response)
|
||||
|
||||
|
||||
@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)
|
||||
):
|
||||
"""Mark alert as resolved (only by alert creator)"""
|
||||
|
||||
result = await db.execute(select(EmergencyAlert).filter(EmergencyAlert.id == alert_id))
|
||||
alert = result.scalars().first()
|
||||
|
||||
if not alert:
|
||||
raise HTTPException(status_code=404, detail="Alert not found")
|
||||
|
||||
if alert.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Only alert creator can resolve it")
|
||||
|
||||
if alert.is_resolved:
|
||||
raise HTTPException(status_code=400, detail="Alert already resolved")
|
||||
|
||||
alert.is_resolved = True
|
||||
alert.resolved_at = datetime.utcnow()
|
||||
alert.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),
|
||||
limit: int = 50
|
||||
):
|
||||
"""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())
|
||||
.limit(limit)
|
||||
)
|
||||
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),
|
||||
limit: int = 20
|
||||
):
|
||||
"""Get active alerts in user's area (last 2 hours)"""
|
||||
|
||||
# Get user's current location first
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.get(
|
||||
f"http://localhost:8003/api/v1/user-location/{current_user.id}",
|
||||
timeout=5.0
|
||||
)
|
||||
if response.status_code != 200:
|
||||
raise HTTPException(status_code=400, detail="User location not available")
|
||||
location_data = response.json()
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Location service unavailable")
|
||||
|
||||
# Get alerts from last 2 hours
|
||||
two_hours_ago = datetime.utcnow() - timedelta(hours=2)
|
||||
|
||||
result = await db.execute(
|
||||
select(EmergencyAlert)
|
||||
.filter(
|
||||
EmergencyAlert.is_resolved == False,
|
||||
EmergencyAlert.created_at >= two_hours_ago
|
||||
)
|
||||
.order_by(EmergencyAlert.created_at.desc())
|
||||
.limit(limit)
|
||||
)
|
||||
alerts = result.scalars().all()
|
||||
|
||||
return [EmergencyAlertResponse.model_validate(alert) for alert in alerts]
|
||||
|
||||
|
||||
@app.get("/api/v1/stats", response_model=EmergencyStats)
|
||||
async def get_emergency_stats(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get emergency service statistics"""
|
||||
|
||||
# Get total alerts
|
||||
total_result = await db.execute(select(func.count(EmergencyAlert.id)))
|
||||
total_alerts = total_result.scalar()
|
||||
|
||||
# Get active alerts
|
||||
active_result = await db.execute(
|
||||
select(func.count(EmergencyAlert.id))
|
||||
.filter(EmergencyAlert.is_resolved == False)
|
||||
)
|
||||
active_alerts = active_result.scalar()
|
||||
|
||||
# Get resolved alerts
|
||||
resolved_alerts = total_alerts - active_alerts
|
||||
|
||||
# Get total responders
|
||||
responders_result = await db.execute(
|
||||
select(func.count(func.distinct(EmergencyResponse.responder_id)))
|
||||
)
|
||||
total_responders = responders_result.scalar()
|
||||
|
||||
return EmergencyStats(
|
||||
total_alerts=total_alerts,
|
||||
active_alerts=active_alerts,
|
||||
resolved_alerts=resolved_alerts,
|
||||
avg_response_time_minutes=None, # TODO: Calculate this
|
||||
total_responders=total_responders
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/v1/health")
|
||||
async def health_check():
|
||||
"""Health check endpoint"""
|
||||
return {"status": "healthy", "service": "emergency-service"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8002)
|
||||
44
services/emergency_service/models.py
Normal file
44
services/emergency_service/models.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from sqlalchemy import Column, String, Integer, Float, DateTime, Text, ForeignKey, Boolean
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from shared.database import BaseModel
|
||||
import uuid
|
||||
|
||||
|
||||
class EmergencyAlert(BaseModel):
|
||||
__tablename__ = "emergency_alerts"
|
||||
|
||||
uuid = Column(UUID(as_uuid=True), default=uuid.uuid4, unique=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
|
||||
# Location at time of alert
|
||||
latitude = Column(Float, nullable=False)
|
||||
longitude = Column(Float, nullable=False)
|
||||
address = Column(String(500))
|
||||
|
||||
# Alert details
|
||||
alert_type = Column(String(50), default="general") # general, medical, violence, etc.
|
||||
message = Column(Text)
|
||||
is_resolved = Column(Boolean, default=False)
|
||||
resolved_at = Column(DateTime(timezone=True))
|
||||
resolved_by = Column(Integer, ForeignKey("users.id"))
|
||||
|
||||
# Response tracking
|
||||
notified_users_count = Column(Integer, default=0)
|
||||
responded_users_count = Column(Integer, default=0)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<EmergencyAlert {self.uuid}>"
|
||||
|
||||
|
||||
class EmergencyResponse(BaseModel):
|
||||
__tablename__ = "emergency_responses"
|
||||
|
||||
alert_id = Column(Integer, ForeignKey("emergency_alerts.id"), nullable=False, index=True)
|
||||
responder_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
|
||||
response_type = Column(String(50)) # help_on_way, contacted_authorities, etc.
|
||||
message = Column(Text)
|
||||
eta_minutes = Column(Integer) # Estimated time of arrival
|
||||
|
||||
def __repr__(self):
|
||||
return f"<EmergencyResponse {self.id}>"
|
||||
80
services/emergency_service/schemas.py
Normal file
80
services/emergency_service/schemas.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class AlertType(str, Enum):
|
||||
GENERAL = "general"
|
||||
MEDICAL = "medical"
|
||||
VIOLENCE = "violence"
|
||||
HARASSMENT = "harassment"
|
||||
UNSAFE_AREA = "unsafe_area"
|
||||
|
||||
|
||||
class ResponseType(str, Enum):
|
||||
HELP_ON_WAY = "help_on_way"
|
||||
CONTACTED_AUTHORITIES = "contacted_authorities"
|
||||
SAFE_NOW = "safe_now"
|
||||
FALSE_ALARM = "false_alarm"
|
||||
|
||||
|
||||
class EmergencyAlertCreate(BaseModel):
|
||||
latitude: float = Field(..., ge=-90, le=90)
|
||||
longitude: float = Field(..., ge=-180, le=180)
|
||||
alert_type: AlertType = AlertType.GENERAL
|
||||
message: Optional[str] = Field(None, max_length=500)
|
||||
address: Optional[str] = Field(None, max_length=500)
|
||||
|
||||
|
||||
class EmergencyAlertResponse(BaseModel):
|
||||
id: int
|
||||
uuid: str
|
||||
user_id: int
|
||||
latitude: float
|
||||
longitude: float
|
||||
address: Optional[str]
|
||||
alert_type: str
|
||||
message: Optional[str]
|
||||
is_resolved: bool
|
||||
resolved_at: Optional[datetime]
|
||||
notified_users_count: int
|
||||
responded_users_count: int
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class EmergencyResponseCreate(BaseModel):
|
||||
response_type: ResponseType
|
||||
message: Optional[str] = Field(None, max_length=500)
|
||||
eta_minutes: Optional[int] = Field(None, ge=0, le=240) # Max 4 hours
|
||||
|
||||
|
||||
class EmergencyResponseResponse(BaseModel):
|
||||
id: int
|
||||
alert_id: int
|
||||
responder_id: int
|
||||
response_type: str
|
||||
message: Optional[str]
|
||||
eta_minutes: Optional[int]
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class NearbyUsersResponse(BaseModel):
|
||||
user_id: int
|
||||
distance_meters: float
|
||||
latitude: float
|
||||
longitude: float
|
||||
|
||||
|
||||
class EmergencyStats(BaseModel):
|
||||
total_alerts: int
|
||||
active_alerts: int
|
||||
resolved_alerts: int
|
||||
avg_response_time_minutes: Optional[float]
|
||||
total_responders: int
|
||||
Reference in New Issue
Block a user