swiftops-backend / src /app /api /v1 /public_tracking.py
kamau1's picture
Fix dependency imports: correct get_db and get_current_user imports to use app.api.deps
d4444b8
"""
Public Tracking API Endpoints
Customer-facing API for installation tracking.
NO AUTHENTICATION REQUIRED - Uses OTP verification instead.
Flow:
1. POST /request-otp - Customer requests OTP via phone/email
2. POST /verify-otp - Customer verifies OTP, gets JWT token + order list
3. GET /{order_id} - Customer views tracking details using JWT token
"""
from fastapi import APIRouter, Depends, HTTPException, status, Header
from sqlalchemy.orm import Session
from typing import Optional
from uuid import UUID
import jwt
from datetime import datetime, timedelta
import logging
from app.api.deps import get_db
from app.config import settings
from app.services.tracking_service import TrackingService
from app.schemas.tracking import (
TrackingOTPRequest,
TrackingOTPResponse,
TrackingOTPVerify,
TrackingVerifyResponse,
TrackingDetailsResponse
)
router = APIRouter()
logger = logging.getLogger(__name__)
# JWT settings for tracking tokens
TRACKING_JWT_SECRET = settings.SECRET_KEY
TRACKING_JWT_ALGORITHM = "HS256"
TRACKING_JWT_EXPIRY_HOURS = 24
# ============================================
# HELPER FUNCTIONS
# ============================================
def generate_tracking_jwt(customer_identifier: str) -> str:
"""
Generate JWT token for tracking session.
Args:
customer_identifier: Phone or email verified via OTP
Returns:
JWT token string (24h expiry)
"""
payload = {
"sub": customer_identifier,
"purpose": "tracking",
"exp": datetime.utcnow() + timedelta(hours=TRACKING_JWT_EXPIRY_HOURS),
"iat": datetime.utcnow()
}
token = jwt.encode(payload, TRACKING_JWT_SECRET, algorithm=TRACKING_JWT_ALGORITHM)
return token
def validate_tracking_jwt(authorization: Optional[str] = Header(None)) -> str:
"""
Validate tracking JWT token from Authorization header.
Args:
authorization: Authorization header (Bearer token)
Returns:
Customer identifier from token
Raises:
HTTPException: If token invalid or missing
"""
if not authorization:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authorization header required"
)
# Extract token from "Bearer <token>"
parts = authorization.split()
if len(parts) != 2 or parts[0].lower() != "bearer":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authorization header format. Expected: Bearer <token>"
)
token = parts[1]
# Decode and validate token
try:
payload = jwt.decode(
token,
TRACKING_JWT_SECRET,
algorithms=[TRACKING_JWT_ALGORITHM]
)
# Verify token purpose
if payload.get("purpose") != "tracking":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token purpose"
)
customer_identifier = payload.get("sub")
if not customer_identifier:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token payload"
)
return customer_identifier
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired. Please request a new OTP."
)
except jwt.InvalidTokenError as e:
logger.warning(f"Invalid tracking JWT: {str(e)}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
# ============================================
# PUBLIC ENDPOINTS
# ============================================
@router.post("/request-otp", response_model=TrackingOTPResponse)
def request_tracking_otp(
request: TrackingOTPRequest
) -> TrackingOTPResponse:
"""
Request OTP for tracking access (Public endpoint).
Customer provides phone OR email. OTP sent via WhatsApp or email.
No authentication required.
**Flow:**
1. Customer enters phone/email on tracking page
2. System sends OTP via chosen delivery method
3. Customer receives OTP code
**Rate Limiting:**
- OTP service enforces rate limits
- Typically 1 OTP per 60 seconds per identifier
"""
try:
identifier, masked = TrackingService.request_tracking_otp(
phone=request.phone,
email=request.email,
delivery_method=request.delivery_method
)
return TrackingOTPResponse(
success=True,
message=f"OTP sent successfully via {request.delivery_method}",
masked_identifier=masked
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to send tracking OTP: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to send OTP. Please try again."
)
@router.post("/verify-otp", response_model=TrackingVerifyResponse)
def verify_tracking_otp(
request: TrackingOTPVerify,
db: Session = Depends(get_db)
) -> TrackingVerifyResponse:
"""
Verify OTP and get tracking access token (Public endpoint).
Customer provides OTP code received via WhatsApp/email.
Returns JWT token for tracking session + list of customer's orders.
**Flow:**
1. Customer enters OTP code
2. System verifies OTP (uses Redis)
3. System looks up customer's orders by phone/email
4. Returns JWT token (24h validity) + order list
**JWT Token:**
- Store token in browser localStorage
- Include in Authorization header for tracking details
- Expires after 24 hours
- No server-side storage (client-side only)
**Security:**
- OTP verification required
- Token tied to specific customer identifier
- Order access validated on each request
"""
try:
# Verify OTP and get customer's orders
orders = TrackingService.verify_tracking_otp(
db=db,
phone=request.phone,
email=request.email,
otp_code=request.otp_code
)
# Generate tracking JWT token
identifier = request.phone if request.phone else request.email
token = generate_tracking_jwt(identifier)
return TrackingVerifyResponse(
access_token=token,
token_type="bearer",
expires_in=TRACKING_JWT_EXPIRY_HOURS * 3600, # seconds
orders=orders
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to verify tracking OTP: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Verification failed. Please try again."
)
@router.get("/{order_id}", response_model=TrackingDetailsResponse)
def get_tracking_details(
order_id: UUID,
customer_identifier: str = Depends(validate_tracking_jwt),
db: Session = Depends(get_db)
) -> TrackingDetailsResponse:
"""
Get detailed tracking information for an order (Authenticated).
Requires valid tracking JWT token in Authorization header.
Returns complete order, ticket, agent, and journey details.
**Authentication:**
- Authorization: Bearer <tracking_jwt_token>
- Token obtained from /verify-otp endpoint
- Token expires after 24 hours
**Security:**
- Validates order belongs to customer from token
- Prevents unauthorized access to other customers' orders
**Tracking Information:**
- Order details (status, package, price)
- Ticket details (scheduled date/time, progress)
- Agent details (name, phone) if assigned
- Journey tracking (current location, route) if started
**Real-time Updates:**
- Frontend should poll this endpoint every 30-60 seconds
- Location updates when agent is en route
- Status updates when ticket progresses
"""
try:
# Get tracking details (includes security validation)
details = TrackingService.get_tracking_details(
db=db,
order_id=order_id,
customer_identifier=customer_identifier
)
return TrackingDetailsResponse(**details)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to get tracking details: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve tracking details"
)
# ============================================
# HEALTH CHECK
# ============================================
@router.get("/health")
def tracking_health_check():
"""
Health check endpoint for tracking service.
Public endpoint, no authentication required.
Useful for monitoring and frontend connectivity checks.
"""
return {
"status": "healthy",
"service": "customer_tracking",
"timestamp": datetime.utcnow().isoformat()
}