Security: deep /api/health (DB+MinIO probes, 503 on failure) + /api/live; rate-limited public forms with honeypots; real client IP behind Traefik (--proxy-headers); TipTap URL sanitization on write (nh3); non-root containers + slim .dockerignore; prod MinIO console bound to 127.0.0.1; HSTS/nosniff on TLS routers; JWT 8h; no default credentials in code. Reliability: Alembic migrations (create_all baseline); graceful shutdown (exec uvicorn); hardened backups (checksums, sanity check, freshness gauge, optional rclone off-site) + restore.sh + hourly db-backup sidecar (ops profile); stack.sh ops tooling (deploy/logs/health/backup/restore/monitoring); .gitattributes LF for shell scripts. Orchestration: json-file log rotation, pinned images, per-service memory limits, edge/internal networks, Traefik ping/accesslog/metrics + flood limiter, conditional minio-setup, pgvector db image, one-shot db-init (umami + aramesh_chatbot databases). Chatbot: unified from site-chatbot branch (RAG + pgvector, SSE, ChatWidget). Monitoring: Prometheus/Alertmanager/Loki/Promtail/Grafana + cAdvisor, node/postgres/blackbox exporters, provisioned dashboards and 10 alert rules; alerts delivered to Bale (tapi.bale.ai — Telegram-compatible). Analytics: Umami on shared Postgres, build-time tracker injection, custom events (contact/newsletter/phone/chat/lang) via frontend/src/analytics.ts. Hygiene: CSV export for contact messages and subscribers; upload image optimization (Pillow); CI script + GitHub-compatible workflow; audit.sh; gen-env.sh for strong credentials.
78 lines
2.4 KiB
Python
78 lines
2.4 KiB
Python
"""انتهای فرمها — ثبت پیام تماس و عضویت در خبرنامه.
|
|
|
|
هر فرم یک دامنهٔ پنهان (honeypot) دارد: رباتها همهٔ فیلدها را پُر میکنند؛
|
|
در آن حالت پاسخ موفقِ قلابی برمیگردد و چیزی ذخیره نمیشود.
|
|
"""
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
from fastapi import APIRouter, Depends, Request
|
|
from sqlalchemy.orm import Session
|
|
|
|
from .. import models
|
|
from ..database import get_db
|
|
from ..rate_limit import contact_limiter, newsletter_limiter
|
|
from ..schemas import ContactIn, ContactMessageOut, NewsletterIn, SubscriberOut
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _fake_contact(payload: ContactIn) -> ContactMessageOut:
|
|
"""پاسخ موفق بدون ذخیره — تا ربات متوجه تله نشود."""
|
|
return ContactMessageOut(
|
|
id=0,
|
|
name=payload.name.strip(),
|
|
phone=payload.phone.strip(),
|
|
note="",
|
|
is_read=True,
|
|
created_at=datetime.now(timezone.utc),
|
|
)
|
|
|
|
|
|
def _fake_subscriber(payload: NewsletterIn) -> SubscriberOut:
|
|
return SubscriberOut(
|
|
id=0,
|
|
email=str(payload.email).lower(),
|
|
is_active=True,
|
|
created_at=datetime.now(timezone.utc),
|
|
)
|
|
|
|
|
|
@router.post("/contact", response_model=ContactMessageOut, status_code=201)
|
|
def submit_contact(payload: ContactIn, request: Request, db: Session = Depends(get_db)):
|
|
contact_limiter.hit(request)
|
|
if payload.website:
|
|
return _fake_contact(payload)
|
|
message = models.ContactMessage(
|
|
name=payload.name.strip(),
|
|
phone=payload.phone.strip(),
|
|
note=payload.note.strip(),
|
|
)
|
|
db.add(message)
|
|
db.commit()
|
|
db.refresh(message)
|
|
return message
|
|
|
|
|
|
@router.post("/newsletter", response_model=SubscriberOut, status_code=201)
|
|
def subscribe(payload: NewsletterIn, request: Request, db: Session = Depends(get_db)):
|
|
newsletter_limiter.hit(request)
|
|
if payload.website:
|
|
return _fake_subscriber(payload)
|
|
|
|
email = str(payload.email).lower()
|
|
subscriber = db.query(models.Subscriber).filter(models.Subscriber.email == email).first()
|
|
|
|
if subscriber is not None:
|
|
if not subscriber.is_active:
|
|
subscriber.is_active = True
|
|
db.commit()
|
|
db.refresh(subscriber)
|
|
return subscriber
|
|
|
|
subscriber = models.Subscriber(email=email)
|
|
db.add(subscriber)
|
|
db.commit()
|
|
db.refresh(subscriber)
|
|
return subscriber
|