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.
190 lines
7.9 KiB
Python
190 lines
7.9 KiB
Python
"""انتهای عمومی — خواندن محتوای سایت بدون نیاز به احراز هویت."""
|
|
|
|
import boto3
|
|
from botocore.config import Config as BotoConfig
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from fastapi.responses import JSONResponse
|
|
from sqlalchemy import text
|
|
from sqlalchemy.orm import Session
|
|
|
|
from ..config import settings
|
|
from ..database import get_db
|
|
from .. import models
|
|
from ..schemas import (
|
|
ArticleOut,
|
|
BeforeAfterOut,
|
|
BrandOut,
|
|
FaqOut,
|
|
ProjectOut,
|
|
ServiceOut,
|
|
StatOut,
|
|
TeamMemberOut,
|
|
TestimonialOut,
|
|
TimelineItemOut,
|
|
WhyUsItemOut,
|
|
)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/live")
|
|
def live():
|
|
"""زندهبودن پروسه — بدون وابستگی؛ برای restart gate استفاده نمیشود."""
|
|
return {"status": "ok"}
|
|
|
|
|
|
@router.get("/health")
|
|
def health(db: Session = Depends(get_db)):
|
|
"""بررسی عمق سلامت — اتصال DB و MinIO واقعاً آزموده میشود؛
|
|
در صورت خرابی هر وابستگی، 503 با جزئیات برمیگردد."""
|
|
checks: dict[str, str] = {}
|
|
|
|
try:
|
|
db.execute(text("SELECT 1"))
|
|
checks["database"] = "ok"
|
|
except Exception:
|
|
checks["database"] = "error"
|
|
|
|
try:
|
|
s3 = boto3.client(
|
|
"s3",
|
|
endpoint_url=settings.s3_endpoint,
|
|
aws_access_key_id=settings.s3_access_key,
|
|
aws_secret_access_key=settings.s3_secret_key,
|
|
region_name="us-east-1",
|
|
config=BotoConfig(connect_timeout=2, read_timeout=2, retries={"max_attempts": 1}),
|
|
)
|
|
s3.head_bucket(Bucket=settings.s3_bucket)
|
|
checks["storage"] = "ok"
|
|
except Exception:
|
|
checks["storage"] = "error"
|
|
|
|
if any(state != "ok" for state in checks.values()):
|
|
return JSONResponse(
|
|
status_code=503,
|
|
content={"status": "degraded", "checks": checks},
|
|
)
|
|
return {"status": "ok", "app": "Aramesh Building Group API", "checks": checks}
|
|
|
|
|
|
@router.get("/brand", response_model=BrandOut)
|
|
def get_brand(db: Session = Depends(get_db)):
|
|
brand = db.query(models.Brand).first()
|
|
if brand is None:
|
|
raise HTTPException(status_code=404, detail="اطلاعات برند یافت نشد")
|
|
return brand
|
|
|
|
|
|
@router.get("/services", response_model=list[ServiceOut])
|
|
def get_services(db: Session = Depends(get_db)):
|
|
return db.query(models.Service).order_by(models.Service.position, models.Service.id).all()
|
|
|
|
|
|
@router.get("/projects", response_model=list[ProjectOut])
|
|
def get_projects(db: Session = Depends(get_db)):
|
|
return db.query(models.Project).order_by(models.Project.position, models.Project.id).all()
|
|
|
|
|
|
@router.get("/projects/{slug}", response_model=ProjectOut)
|
|
def get_project(slug: str, db: Session = Depends(get_db)):
|
|
project = db.query(models.Project).filter(models.Project.slug == slug).first()
|
|
if project is None:
|
|
raise HTTPException(status_code=404, detail="پروژه یافت نشد")
|
|
return project
|
|
|
|
|
|
@router.get("/articles", response_model=list[ArticleOut])
|
|
def get_articles(db: Session = Depends(get_db)):
|
|
return (
|
|
db.query(models.Article)
|
|
.filter(models.Article.status == "published")
|
|
.order_by(models.Article.position, models.Article.id)
|
|
.all()
|
|
)
|
|
|
|
|
|
@router.get("/articles/{slug}", response_model=ArticleOut)
|
|
def get_article(slug: str, db: Session = Depends(get_db)):
|
|
article = (
|
|
db.query(models.Article)
|
|
.filter(models.Article.slug == slug, models.Article.status == "published")
|
|
.first()
|
|
)
|
|
if article is None:
|
|
raise HTTPException(status_code=404, detail="مقاله یافت نشد")
|
|
return article
|
|
|
|
|
|
@router.get("/testimonials", response_model=list[TestimonialOut])
|
|
def get_testimonials(db: Session = Depends(get_db)):
|
|
return db.query(models.Testimonial).order_by(models.Testimonial.position, models.Testimonial.id).all()
|
|
|
|
|
|
@router.get("/faqs", response_model=list[FaqOut])
|
|
def get_faqs(db: Session = Depends(get_db)):
|
|
return db.query(models.Faq).order_by(models.Faq.position, models.Faq.id).all()
|
|
|
|
|
|
@router.get("/team", response_model=list[TeamMemberOut])
|
|
def get_team(db: Session = Depends(get_db)):
|
|
return db.query(models.TeamMember).order_by(models.TeamMember.position, models.TeamMember.id).all()
|
|
|
|
|
|
@router.get("/timeline", response_model=list[TimelineItemOut])
|
|
def get_timeline(db: Session = Depends(get_db)):
|
|
return db.query(models.TimelineItem).order_by(models.TimelineItem.position, models.TimelineItem.id).all()
|
|
|
|
|
|
@router.get("/stats", response_model=list[StatOut])
|
|
def get_stats(db: Session = Depends(get_db)):
|
|
return db.query(models.Stat).order_by(models.Stat.position, models.Stat.id).all()
|
|
|
|
|
|
@router.get("/why-us", response_model=list[WhyUsItemOut])
|
|
def get_why_us(db: Session = Depends(get_db)):
|
|
return db.query(models.WhyUsItem).order_by(models.WhyUsItem.position, models.WhyUsItem.id).all()
|
|
|
|
|
|
@router.get("/before-after", response_model=BeforeAfterOut)
|
|
def get_before_after(db: Session = Depends(get_db)):
|
|
item = db.query(models.BeforeAfter).first()
|
|
if item is None:
|
|
raise HTTPException(status_code=404, detail="بخش قبل/بعد یافت نشد")
|
|
return item
|
|
|
|
|
|
@router.get("/site")
|
|
def get_site(db: Session = Depends(get_db)):
|
|
"""همه محتوای سایت در یک درخواست — مناسب برای بارگذاری اولیه فرانتاند."""
|
|
return {
|
|
"brand": db.query(models.Brand).first(),
|
|
"nav": db.query(models.NavItem).order_by(models.NavItem.position, models.NavItem.id).all(),
|
|
"hero": db.query(models.Hero).first(),
|
|
"story": db.query(models.StorySection).first(),
|
|
"section_headings": db.query(models.SectionHeadingContent).order_by(models.SectionHeadingContent.id).all(),
|
|
"page_heroes": db.query(models.PageHeroContent).order_by(models.PageHeroContent.id).all(),
|
|
"services": db.query(models.Service).order_by(models.Service.position, models.Service.id).all(),
|
|
"projects": db.query(models.Project).order_by(models.Project.position, models.Project.id).all(),
|
|
# فقط مقالههای منتشرشده — پیشنویسها از انتهای مدیریتی خوانده میشوند
|
|
"articles": db.query(models.Article)
|
|
.filter(models.Article.status == "published")
|
|
.order_by(models.Article.position, models.Article.id)
|
|
.all(),
|
|
"testimonials": db.query(models.Testimonial).order_by(models.Testimonial.position, models.Testimonial.id).all(),
|
|
"faqs": db.query(models.Faq).order_by(models.Faq.position, models.Faq.id).all(),
|
|
"team": db.query(models.TeamMember).order_by(models.TeamMember.position, models.TeamMember.id).all(),
|
|
"timeline": db.query(models.TimelineItem).order_by(models.TimelineItem.position, models.TimelineItem.id).all(),
|
|
"stats": db.query(models.Stat).order_by(models.Stat.position, models.Stat.id).all(),
|
|
"why_us": db.query(models.WhyUsItem).order_by(models.WhyUsItem.position, models.WhyUsItem.id).all(),
|
|
"before_after": db.query(models.BeforeAfter).first(),
|
|
"investment_models": db.query(models.InvestmentModel).order_by(models.InvestmentModel.position, models.InvestmentModel.id).all(),
|
|
"process_steps": db.query(models.ProcessStep).order_by(models.ProcessStep.position, models.ProcessStep.id).all(),
|
|
"offices": db.query(models.Office).order_by(models.Office.position, models.Office.id).all(),
|
|
"gallery": db.query(models.GalleryImage).order_by(models.GalleryImage.position, models.GalleryImage.id).all(),
|
|
"status_labels": db.query(models.StatusLabel).order_by(models.StatusLabel.id).all(),
|
|
"contact_section": db.query(models.ContactSectionContent).first(),
|
|
"newsletter": db.query(models.NewsletterSectionContent).first(),
|
|
"footer": db.query(models.FooterContent).first(),
|
|
"seo_meta": db.query(models.SeoMeta).order_by(models.SeoMeta.id).all(),
|
|
}
|