- Frontend: Next.js 14 with TypeScript - Backend: FastAPI with SQLAlchemy - Agent: Carmodoo sync agent - Deployment: Docker Compose based staging/production setup - Scripts: Automated deployment with rollback support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
277 lines
8.8 KiB
Python
277 lines
8.8 KiB
Python
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.orm import Session
|
|
from typing import Optional
|
|
from pydantic import BaseModel
|
|
from datetime import datetime
|
|
from ..database import get_db
|
|
from ..models import User, PushSubscription, UserNotificationPreference
|
|
from .auth import get_current_user, get_current_admin_user
|
|
|
|
router = APIRouter(prefix="/push", tags=["Push Notifications"])
|
|
|
|
|
|
# VAPID keys for web push (in production, store these securely)
|
|
# Generate these using: npx web-push generate-vapid-keys
|
|
VAPID_PUBLIC_KEY = "BMjR7pDj6PUjFo8VkA4f1BYhOAzGhJPcVnT7mJ6Bq8jG9yYKvN8dZ5jT3pQ2sL9wR0xF4bM1nK3vH5uC7yX2aE0"
|
|
|
|
|
|
class PushSubscriptionCreate(BaseModel):
|
|
endpoint: str
|
|
p256dh_key: str
|
|
auth_key: str
|
|
device_info: Optional[str] = None
|
|
|
|
|
|
class NotificationPreferenceUpdate(BaseModel):
|
|
vehicle_recommended: Optional[bool] = None
|
|
shipping_update: Optional[bool] = None
|
|
payment_confirmed: Optional[bool] = None
|
|
withdrawal_processed: Optional[bool] = None
|
|
dealer_status: Optional[bool] = None
|
|
share_purchased: Optional[bool] = None
|
|
referral_reward: Optional[bool] = None
|
|
inquiry_reply: Optional[bool] = None
|
|
system_announcements: Optional[bool] = None
|
|
push_enabled: Optional[bool] = None
|
|
email_enabled: Optional[bool] = None
|
|
|
|
|
|
@router.get("/vapid-key")
|
|
def get_vapid_public_key():
|
|
"""Get VAPID public key for push subscription"""
|
|
return {"public_key": VAPID_PUBLIC_KEY}
|
|
|
|
|
|
@router.post("/subscribe")
|
|
def subscribe_push(
|
|
subscription: PushSubscriptionCreate,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Subscribe to push notifications"""
|
|
# Check if subscription already exists
|
|
existing = db.query(PushSubscription).filter(
|
|
PushSubscription.user_id == current_user.id,
|
|
PushSubscription.endpoint == subscription.endpoint
|
|
).first()
|
|
|
|
if existing:
|
|
# Update existing subscription
|
|
existing.p256dh_key = subscription.p256dh_key
|
|
existing.auth_key = subscription.auth_key
|
|
existing.device_info = subscription.device_info
|
|
existing.is_active = True
|
|
existing.last_used_at = datetime.utcnow()
|
|
else:
|
|
# Create new subscription
|
|
new_sub = PushSubscription(
|
|
user_id=current_user.id,
|
|
endpoint=subscription.endpoint,
|
|
p256dh_key=subscription.p256dh_key,
|
|
auth_key=subscription.auth_key,
|
|
device_info=subscription.device_info,
|
|
is_active=True
|
|
)
|
|
db.add(new_sub)
|
|
|
|
db.commit()
|
|
return {"message": "Push subscription saved successfully"}
|
|
|
|
|
|
@router.delete("/unsubscribe")
|
|
def unsubscribe_push(
|
|
endpoint: str,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Unsubscribe from push notifications"""
|
|
subscription = db.query(PushSubscription).filter(
|
|
PushSubscription.user_id == current_user.id,
|
|
PushSubscription.endpoint == endpoint
|
|
).first()
|
|
|
|
if subscription:
|
|
subscription.is_active = False
|
|
db.commit()
|
|
|
|
return {"message": "Push subscription removed"}
|
|
|
|
|
|
@router.get("/subscriptions")
|
|
def get_my_subscriptions(
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Get user's active push subscriptions"""
|
|
subscriptions = db.query(PushSubscription).filter(
|
|
PushSubscription.user_id == current_user.id,
|
|
PushSubscription.is_active == True
|
|
).all()
|
|
|
|
return [
|
|
{
|
|
"id": sub.id,
|
|
"endpoint": sub.endpoint[:50] + "..." if len(sub.endpoint) > 50 else sub.endpoint,
|
|
"device_info": sub.device_info,
|
|
"created_at": sub.created_at.isoformat() if sub.created_at else None,
|
|
"last_used_at": sub.last_used_at.isoformat() if sub.last_used_at else None
|
|
}
|
|
for sub in subscriptions
|
|
]
|
|
|
|
|
|
@router.get("/preferences")
|
|
def get_notification_preferences(
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Get user's notification preferences"""
|
|
prefs = db.query(UserNotificationPreference).filter(
|
|
UserNotificationPreference.user_id == current_user.id
|
|
).first()
|
|
|
|
if not prefs:
|
|
# Create default preferences
|
|
prefs = UserNotificationPreference(user_id=current_user.id)
|
|
db.add(prefs)
|
|
db.commit()
|
|
db.refresh(prefs)
|
|
|
|
return {
|
|
"vehicle_recommended": prefs.vehicle_recommended,
|
|
"shipping_update": prefs.shipping_update,
|
|
"payment_confirmed": prefs.payment_confirmed,
|
|
"withdrawal_processed": prefs.withdrawal_processed,
|
|
"dealer_status": prefs.dealer_status,
|
|
"share_purchased": prefs.share_purchased,
|
|
"referral_reward": prefs.referral_reward,
|
|
"inquiry_reply": prefs.inquiry_reply,
|
|
"system_announcements": prefs.system_announcements,
|
|
"push_enabled": prefs.push_enabled,
|
|
"email_enabled": prefs.email_enabled,
|
|
}
|
|
|
|
|
|
@router.put("/preferences")
|
|
def update_notification_preferences(
|
|
preferences: NotificationPreferenceUpdate,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Update user's notification preferences"""
|
|
prefs = db.query(UserNotificationPreference).filter(
|
|
UserNotificationPreference.user_id == current_user.id
|
|
).first()
|
|
|
|
if not prefs:
|
|
prefs = UserNotificationPreference(user_id=current_user.id)
|
|
db.add(prefs)
|
|
|
|
# Update preferences
|
|
for field, value in preferences.dict(exclude_none=True).items():
|
|
setattr(prefs, field, value)
|
|
|
|
db.commit()
|
|
db.refresh(prefs)
|
|
|
|
return {"message": "Preferences updated successfully"}
|
|
|
|
|
|
# Admin endpoints
|
|
@router.get("/admin/stats")
|
|
def admin_get_push_stats(
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_admin_user)
|
|
):
|
|
"""Get push notification statistics (Admin only)"""
|
|
total_subscriptions = db.query(PushSubscription).filter(
|
|
PushSubscription.is_active == True
|
|
).count()
|
|
|
|
users_with_push = db.query(PushSubscription.user_id).filter(
|
|
PushSubscription.is_active == True
|
|
).distinct().count()
|
|
|
|
return {
|
|
"total_subscriptions": total_subscriptions,
|
|
"users_with_push": users_with_push
|
|
}
|
|
|
|
|
|
# Helper function to send push notification (called from other modules)
|
|
def send_push_notification(
|
|
db: Session,
|
|
user_id: int,
|
|
title: str,
|
|
body: str,
|
|
url: str = None,
|
|
notification_type: str = "system"
|
|
):
|
|
"""
|
|
Send push notification to a user.
|
|
In production, this would use pywebpush to actually send the notification.
|
|
"""
|
|
# Check user preferences
|
|
prefs = db.query(UserNotificationPreference).filter(
|
|
UserNotificationPreference.user_id == user_id
|
|
).first()
|
|
|
|
if prefs and not prefs.push_enabled:
|
|
return False
|
|
|
|
# Check specific notification type preference
|
|
if prefs:
|
|
type_pref_map = {
|
|
"vehicle_recommended": prefs.vehicle_recommended,
|
|
"shipping_update": prefs.shipping_update,
|
|
"payment_confirmed": prefs.payment_confirmed,
|
|
"withdrawal_processed": prefs.withdrawal_processed,
|
|
"dealer_approved": prefs.dealer_status,
|
|
"dealer_rejected": prefs.dealer_status,
|
|
"share_purchased": prefs.share_purchased,
|
|
"referral_reward": prefs.referral_reward,
|
|
"inquiry_reply": prefs.inquiry_reply,
|
|
"system": prefs.system_announcements,
|
|
}
|
|
if notification_type in type_pref_map and not type_pref_map[notification_type]:
|
|
return False
|
|
|
|
# Get user's active subscriptions
|
|
subscriptions = db.query(PushSubscription).filter(
|
|
PushSubscription.user_id == user_id,
|
|
PushSubscription.is_active == True
|
|
).all()
|
|
|
|
if not subscriptions:
|
|
return False
|
|
|
|
# In production, use pywebpush to send notifications
|
|
# For now, we just log and return success
|
|
# Example with pywebpush:
|
|
# from pywebpush import webpush, WebPushException
|
|
# for sub in subscriptions:
|
|
# try:
|
|
# webpush(
|
|
# subscription_info={
|
|
# "endpoint": sub.endpoint,
|
|
# "keys": {
|
|
# "p256dh": sub.p256dh_key,
|
|
# "auth": sub.auth_key
|
|
# }
|
|
# },
|
|
# data=json.dumps({
|
|
# "title": title,
|
|
# "body": body,
|
|
# "url": url
|
|
# }),
|
|
# vapid_private_key=VAPID_PRIVATE_KEY,
|
|
# vapid_claims={"sub": "mailto:admin@autosellcar.com"}
|
|
# )
|
|
# sub.last_used_at = datetime.utcnow()
|
|
# except WebPushException as ex:
|
|
# if ex.response and ex.response.status_code == 410:
|
|
# sub.is_active = False
|
|
# db.commit()
|
|
|
|
return True
|