- مقالهها ستون status (draft/published) گرفتند — GET /api/site و همه انتهای عمومی فقط منتشرشده را برمیگردانند؛ autosave دیگر پیشنویس نیمهکاره را زنده نمیکند - کلمه کلیدی هدف چکلیست سئو فیلد seo_keyphrase شد (قبلاً localStorage بود) - robots.txt و sitemap.xml از بکاند + سرو /blog و /projects با تزریق title/description/OG/JSON-LD از دیتابیس برای خزندههای بدون JS (app/seo_pages.py) - فرانت: applySeo در seo.ts (OG/توییتر/canonical/JSON-LD)، ادغام پیشنویس برای ادمین، برچسب پیشنویس در وبلاگ، حالت خالی وبلاگ، دکمه انتشار در صفحه نوشتن - تغییر رمز ادمین POST /api/auth/change-password + دیالوگ «رمز» در نوار مدیریت - محدودسازی ورود: ۵ تلاش ناموفق در ۱۵ دقیقه از هر IP → 429 - ساخت دکمه + برای ردیف سئوی صفحههای بدون ردیف (SingletonPill) - تستهای pytest (۱۵ مورد) + پینکردن requirements + حذف package.json سرگردان فرانت
150 lines
6.5 KiB
Python
150 lines
6.5 KiB
Python
"""انتهای عمومی — خواندن محتوای سایت بدون نیاز به احراز هویت."""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.orm import Session
|
|
|
|
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("/health")
|
|
def health():
|
|
return {"status": "ok", "app": "Aramesh Building Group API"}
|
|
|
|
|
|
@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(),
|
|
}
|