پروژه کامل: فرانت متصل به API + داکرایز
- اتصال کامل فرانت به API: همه محتوای سایت (پروژهها، مقالات، خدمات، برند و...) از بکاند خوانده میشود (SiteProvider + useSite) - فرم تماس و خبرنامه متصل به بکاند و تستشده در مرورگر - ساختار مونوریپو: frontend/ + backend/ - Dockerfile برای هر دو بخش + docker-compose (PostgreSQL + FastAPI + nginx) - nginx با پروکسی /api — بدون نیاز به CORS در حالت داکر - README کامل فارسی
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
"""انتهای پنل مدیریت — همه مسیرها نیازمند توکن JWT معتبر هستند."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .. import models, schemas
|
||||
from ..auth import create_access_token, get_current_admin, verify_password
|
||||
from ..database import get_db
|
||||
from ..schemas import (
|
||||
ArticleIn,
|
||||
ArticleOut,
|
||||
BeforeAfterIn,
|
||||
BeforeAfterOut,
|
||||
BrandIn,
|
||||
BrandOut,
|
||||
ContactMessageOut,
|
||||
FaqIn,
|
||||
FaqOut,
|
||||
LoginIn,
|
||||
MessageReadIn,
|
||||
ProjectIn,
|
||||
ProjectOut,
|
||||
ServiceIn,
|
||||
ServiceOut,
|
||||
StatIn,
|
||||
StatOut,
|
||||
SubscriberOut,
|
||||
TeamMemberIn,
|
||||
TeamMemberOut,
|
||||
TestimonialIn,
|
||||
TestimonialOut,
|
||||
TimelineItemIn,
|
||||
TimelineItemOut,
|
||||
TokenOut,
|
||||
WhyUsItemIn,
|
||||
WhyUsItemOut,
|
||||
)
|
||||
|
||||
# روتر عمومی برای ورود — بدون نیاز به توکن
|
||||
auth_router = APIRouter()
|
||||
|
||||
# روتر محافظتشده — همه مسیرهای آن فقط با توکن معتبر قابل دسترسی هستند
|
||||
router = APIRouter(dependencies=[Depends(get_current_admin)])
|
||||
|
||||
|
||||
@auth_router.post("/auth/login", response_model=TokenOut)
|
||||
def login(payload: LoginIn, db: Session = Depends(get_db)):
|
||||
user = db.query(models.AdminUser).filter(models.AdminUser.username == payload.username).first()
|
||||
if user is None or not verify_password(payload.password, user.password_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="نام کاربری یا رمز عبور اشتباه است",
|
||||
)
|
||||
return TokenOut(access_token=create_access_token(user.username))
|
||||
|
||||
|
||||
def register_crud(path: str, model, out_schema, create_schema) -> None:
|
||||
"""ثبت پنج مسیر CRUD استاندارد (لیست، ساخت، دریافت، ویرایش، حذف) روی روتر مدیریت."""
|
||||
|
||||
@router.get(f"/{path}", response_model=list[out_schema])
|
||||
def list_items(db: Session = Depends(get_db)):
|
||||
return db.query(model).order_by(model.id).all()
|
||||
|
||||
@router.post(f"/{path}", response_model=out_schema, status_code=201)
|
||||
def create_item(payload: create_schema, db: Session = Depends(get_db)):
|
||||
item = model(**payload.model_dump())
|
||||
db.add(item)
|
||||
try:
|
||||
db.commit()
|
||||
except IntegrityError:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=409, detail="رکوردی با این مقدار تکراری وجود دارد (مثلاً slug یا ایمیل)")
|
||||
db.refresh(item)
|
||||
return item
|
||||
|
||||
@router.get(f"/{path}/{{item_id}}", response_model=out_schema)
|
||||
def get_item(item_id: int, db: Session = Depends(get_db)):
|
||||
item = db.get(model, item_id)
|
||||
if item is None:
|
||||
raise HTTPException(status_code=404, detail="رکورد یافت نشد")
|
||||
return item
|
||||
|
||||
@router.put(f"/{path}/{{item_id}}", response_model=out_schema)
|
||||
def update_item(item_id: int, payload: create_schema, db: Session = Depends(get_db)):
|
||||
item = db.get(model, item_id)
|
||||
if item is None:
|
||||
raise HTTPException(status_code=404, detail="رکورد یافت نشد")
|
||||
for key, value in payload.model_dump().items():
|
||||
setattr(item, key, value)
|
||||
try:
|
||||
db.commit()
|
||||
except IntegrityError:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=409, detail="رکوردی با این مقدار تکراری وجود دارد (مثلاً slug یا ایمیل)")
|
||||
db.refresh(item)
|
||||
return item
|
||||
|
||||
@router.delete(f"/{path}/{{item_id}}", status_code=204)
|
||||
def delete_item(item_id: int, db: Session = Depends(get_db)):
|
||||
item = db.get(model, item_id)
|
||||
if item is None:
|
||||
raise HTTPException(status_code=404, detail="رکورد یافت نشد")
|
||||
db.delete(item)
|
||||
db.commit()
|
||||
|
||||
|
||||
# CRUD محتوا
|
||||
register_crud("brand", models.Brand, BrandOut, BrandIn)
|
||||
register_crud("services", models.Service, ServiceOut, ServiceIn)
|
||||
register_crud("projects", models.Project, ProjectOut, ProjectIn)
|
||||
register_crud("articles", models.Article, ArticleOut, ArticleIn)
|
||||
register_crud("testimonials", models.Testimonial, TestimonialOut, TestimonialIn)
|
||||
register_crud("faqs", models.Faq, FaqOut, FaqIn)
|
||||
register_crud("team", models.TeamMember, TeamMemberOut, TeamMemberIn)
|
||||
register_crud("timeline", models.TimelineItem, TimelineItemOut, TimelineItemIn)
|
||||
register_crud("stats", models.Stat, StatOut, StatIn)
|
||||
register_crud("why-us", models.WhyUsItem, WhyUsItemOut, WhyUsItemIn)
|
||||
register_crud("before-after", models.BeforeAfter, BeforeAfterOut, BeforeAfterIn)
|
||||
|
||||
|
||||
# پیامهای تماس
|
||||
@router.get("/messages", response_model=list[ContactMessageOut])
|
||||
def list_messages(unread_only: bool = False, db: Session = Depends(get_db)):
|
||||
query = db.query(models.ContactMessage)
|
||||
if unread_only:
|
||||
query = query.filter(models.ContactMessage.is_read.is_(False))
|
||||
return query.order_by(models.ContactMessage.created_at.desc()).all()
|
||||
|
||||
|
||||
@router.patch("/messages/{message_id}", response_model=ContactMessageOut)
|
||||
def update_message(message_id: int, payload: MessageReadIn, db: Session = Depends(get_db)):
|
||||
message = db.get(models.ContactMessage, message_id)
|
||||
if message is None:
|
||||
raise HTTPException(status_code=404, detail="پیام یافت نشد")
|
||||
message.is_read = payload.is_read
|
||||
db.commit()
|
||||
db.refresh(message)
|
||||
return message
|
||||
|
||||
|
||||
@router.delete("/messages/{message_id}", status_code=204)
|
||||
def delete_message(message_id: int, db: Session = Depends(get_db)):
|
||||
message = db.get(models.ContactMessage, message_id)
|
||||
if message is None:
|
||||
raise HTTPException(status_code=404, detail="پیام یافت نشد")
|
||||
db.delete(message)
|
||||
db.commit()
|
||||
|
||||
|
||||
# مشترکین خبرنامه
|
||||
@router.get("/subscribers", response_model=list[SubscriberOut])
|
||||
def list_subscribers(db: Session = Depends(get_db)):
|
||||
return db.query(models.Subscriber).order_by(models.Subscriber.created_at.desc()).all()
|
||||
|
||||
|
||||
@router.delete("/subscribers/{subscriber_id}", status_code=204)
|
||||
def delete_subscriber(subscriber_id: int, db: Session = Depends(get_db)):
|
||||
subscriber = db.get(models.Subscriber, subscriber_id)
|
||||
if subscriber is None:
|
||||
raise HTTPException(status_code=404, detail="مشترک یافت نشد")
|
||||
db.delete(subscriber)
|
||||
db.commit()
|
||||
@@ -0,0 +1,42 @@
|
||||
"""انتهای فرمها — ثبت پیام تماس و عضویت در خبرنامه."""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..database import get_db
|
||||
from .. import models
|
||||
from ..schemas import ContactIn, ContactMessageOut, NewsletterIn, SubscriberOut
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/contact", response_model=ContactMessageOut, status_code=201)
|
||||
def submit_contact(payload: ContactIn, db: Session = Depends(get_db)):
|
||||
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, db: Session = Depends(get_db)):
|
||||
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
|
||||
@@ -0,0 +1,122 @@
|
||||
"""انتهای عمومی — خواندن محتوای سایت بدون نیاز به احراز هویت."""
|
||||
|
||||
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": "ArameshPlaza 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).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.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).order_by(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).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).all()
|
||||
|
||||
|
||||
@router.get("/faqs", response_model=list[FaqOut])
|
||||
def get_faqs(db: Session = Depends(get_db)):
|
||||
return db.query(models.Faq).all()
|
||||
|
||||
|
||||
@router.get("/team", response_model=list[TeamMemberOut])
|
||||
def get_team(db: Session = Depends(get_db)):
|
||||
return db.query(models.TeamMember).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.id).all()
|
||||
|
||||
|
||||
@router.get("/stats", response_model=list[StatOut])
|
||||
def get_stats(db: Session = Depends(get_db)):
|
||||
return db.query(models.Stat).all()
|
||||
|
||||
|
||||
@router.get("/why-us", response_model=list[WhyUsItemOut])
|
||||
def get_why_us(db: Session = Depends(get_db)):
|
||||
return db.query(models.WhyUsItem).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(),
|
||||
"services": db.query(models.Service).all(),
|
||||
"projects": db.query(models.Project).order_by(models.Project.id).all(),
|
||||
"articles": db.query(models.Article).order_by(models.Article.id).all(),
|
||||
"testimonials": db.query(models.Testimonial).all(),
|
||||
"faqs": db.query(models.Faq).all(),
|
||||
"team": db.query(models.TeamMember).all(),
|
||||
"timeline": db.query(models.TimelineItem).order_by(models.TimelineItem.id).all(),
|
||||
"stats": db.query(models.Stat).all(),
|
||||
"why_us": db.query(models.WhyUsItem).all(),
|
||||
"before_after": db.query(models.BeforeAfter).first(),
|
||||
}
|
||||
Reference in New Issue
Block a user