Files
xenia-pos-cloud/cloud_backend/routers/heartbeat.py
bonamin 06b01533d4 feat: waiter domain + local IP tracking
- Site model: add waiter_domain and last_seen_local_ip columns
- HeartbeatRequest: accept optional local_ip field from local backend
- HeartbeatResponse: return waiter_domain to local backend
- heartbeat router: persist local_ip on each check-in
- SiteDetailPage: show Public IP / Local IP separately, add Waiter Domain
  card with inline edit modal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 14:06:36 +03:00

43 lines
1.5 KiB
Python

from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, Header, Request, status
from passlib.context import CryptContext
from sqlalchemy.orm import Session
from config import settings
from database import get_db
from models.site import Site
from schemas.site import HeartbeatRequest, HeartbeatResponse
router = APIRouter()
_pwd = CryptContext(schemes=["bcrypt"], deprecated="auto")
@router.post("/", response_model=HeartbeatResponse)
def heartbeat(
body: HeartbeatRequest,
request: Request,
x_site_id: str = Header(..., alias="X-Site-ID"),
x_site_key: str = Header(..., alias="X-Site-Key"),
db: Session = Depends(get_db),
):
site = db.query(Site).filter(Site.site_id == x_site_id).first()
if not site or not _pwd.verify(x_site_key, site.secret_key_hash):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid site credentials")
now = datetime.now(timezone.utc)
site.last_seen_at = now
site.last_seen_ip = request.client.host if request.client else None
if body.local_ip:
site.last_seen_local_ip = body.local_ip
db.commit()
licensed = site.is_active and (site.license_expires_at.replace(tzinfo=timezone.utc) > now)
return HeartbeatResponse(
licensed=licensed,
locked=site.is_locked,
lock_reason=site.lock_reason,
expires_at=site.license_expires_at,
latest_version=settings.LATEST_VERSION,
waiter_domain=site.waiter_domain,
)