Initial commit: AutonetSellCar platform with deployment system
- 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>
This commit is contained in:
276
backend/app/api/push.py
Normal file
276
backend/app/api/push.py
Normal file
@@ -0,0 +1,276 @@
|
||||
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
|
||||
Reference in New Issue
Block a user