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:
AutonetSellCar Deploy
2025-12-30 13:24:39 +09:00
commit 1f0dcb1ddb
224 changed files with 55119 additions and 0 deletions

276
backend/app/api/push.py Normal file
View 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