اولین نسخه بک‌اند آرامش پلازا

This commit is contained in:
Behrad
2026-08-27 15:15:12 +03:30
commit 854309f976
26 changed files with 1861 additions and 0 deletions
View File
+59
View File
@@ -0,0 +1,59 @@
from datetime import datetime, timedelta, timezone
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from passlib.context import CryptContext
from sqlalchemy.orm import Session
from app.config import settings
from app.database import get_db
from app.models import AdminUser
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def create_access_token(data: dict) -> str:
to_encode = data.copy()
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm="HS256")
def authenticate_admin(db: Session, username: str, password: str) -> AdminUser | None:
user = db.query(AdminUser).filter(AdminUser.username == username).first()
if not user or not verify_password(password, user.hashed_password):
return None
return user
def get_current_admin(
token: str = Depends(oauth2_scheme),
db: Session = Depends(get_db),
) -> AdminUser:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="توکن نامعتبر یا منقضی شده است",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
username: str | None = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = db.query(AdminUser).filter(AdminUser.username == username).first()
if user is None:
raise credentials_exception
return user
+17
View File
@@ -0,0 +1,17 @@
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
DATABASE_URL: str
SECRET_KEY: str
ACCESS_TOKEN_EXPIRE_MINUTES: int = 1440
CORS_ORIGINS: str = "http://localhost:5173"
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
@property
def cors_origins_list(self) -> list[str]:
return [origin.strip() for origin in self.CORS_ORIGINS.split(",") if origin.strip()]
settings = Settings()
+81
View File
@@ -0,0 +1,81 @@
"""
یک فکتوری عمومی برای ساخت روترهای CRUD.
چون خیلی از منابع (خدمات، آمار، تایم‌لاین، تیم، نظرات مشتریان، چرا ما، FAQ)
ساختار مشابهی دارن (لیست عمومی + مدیریت فقط برای ادمین)، این تابع کد تکراری
رو حذف می‌کنه. برای منابعی که پیچیده‌تر هستن (پروژه‌ها، مقالات، تماس، خبرنامه)
روتر جداگانه با منطق اختصاصی نوشته شده.
"""
from typing import Type
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.auth import get_current_admin
from app.database import get_db
def build_crud_router(
*,
prefix: str,
tag: str,
model,
create_schema: Type,
update_schema: Type,
out_schema: Type,
order_field: str = "order",
) -> APIRouter:
router = APIRouter(prefix=prefix, tags=[tag])
@router.get("", response_model=list[out_schema])
def list_items(db: Session = Depends(get_db)):
return db.query(model).order_by(getattr(model, order_field)).all()
@router.get("/{item_id}", response_model=out_schema)
def get_item(item_id: int, db: Session = Depends(get_db)):
item = db.get(model, item_id)
if not item:
raise HTTPException(status.HTTP_404_NOT_FOUND, "مورد پیدا نشد")
return item
@router.post("", response_model=out_schema, status_code=status.HTTP_201_CREATED)
def create_item(
payload: create_schema,
db: Session = Depends(get_db),
_admin=Depends(get_current_admin),
):
item = model(**payload.model_dump())
db.add(item)
db.commit()
db.refresh(item)
return item
@router.put("/{item_id}", response_model=out_schema)
def update_item(
item_id: int,
payload: update_schema,
db: Session = Depends(get_db),
_admin=Depends(get_current_admin),
):
item = db.get(model, item_id)
if not item:
raise HTTPException(status.HTTP_404_NOT_FOUND, "مورد پیدا نشد")
for field, value in payload.model_dump(exclude_unset=True).items():
setattr(item, field, value)
db.commit()
db.refresh(item)
return item
@router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_item(
item_id: int,
db: Session = Depends(get_db),
_admin=Depends(get_current_admin),
):
item = db.get(model, item_id)
if not item:
raise HTTPException(status.HTTP_404_NOT_FOUND, "مورد پیدا نشد")
db.delete(item)
db.commit()
return None
return router
+19
View File
@@ -0,0 +1,19 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, DeclarativeBase
from app.config import settings
engine = create_engine(settings.DATABASE_URL, pool_pre_ping=True)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
class Base(DeclarativeBase):
pass
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
+47
View File
@@ -0,0 +1,47 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.config import settings
from app.routers import auth, projects, articles, brand, contact, newsletter
from app.routers.simple_resources import (
services_router,
stats_router,
timeline_router,
why_us_router,
testimonials_router,
team_router,
faqs_router,
)
app = FastAPI(
title="Aramesh Plaza API",
description="بک‌اند سایت آرامش پلازا — مدیریت محتوا، فرم تماس و خبرنامه",
version="1.0.0",
)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins_list,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(auth.router)
app.include_router(brand.router)
app.include_router(services_router)
app.include_router(projects.router)
app.include_router(stats_router)
app.include_router(timeline_router)
app.include_router(why_us_router)
app.include_router(testimonials_router)
app.include_router(team_router)
app.include_router(articles.router)
app.include_router(faqs_router)
app.include_router(contact.router)
app.include_router(newsletter.router)
@app.get("/", tags=["Health"])
def health_check():
return {"status": "ok", "message": "Aramesh Plaza API در حال اجراست"}
+160
View File
@@ -0,0 +1,160 @@
import enum
from datetime import datetime
from sqlalchemy import String, Integer, Text, DateTime, Boolean, Enum, JSON
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class ProjectStatus(str, enum.Enum):
done = "done"
progress = "progress"
class AdminUser(Base):
__tablename__ = "admin_users"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
username: Mapped[str] = mapped_column(String(64), unique=True, index=True)
hashed_password: Mapped[str] = mapped_column(String(255))
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
class Brand(Base):
"""اطلاعات کلی برند - همیشه فقط یک رکورد (id=1) دارد."""
__tablename__ = "brand"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(255), default="")
logo: Mapped[str] = mapped_column(String(1024), default="")
hero: Mapped[str] = mapped_column(String(1024), default="")
contact_bg: Mapped[str] = mapped_column(String(1024), default="")
phone: Mapped[str] = mapped_column(String(64), default="")
phone_href: Mapped[str] = mapped_column(String(64), default="")
email: Mapped[str] = mapped_column(String(255), default="")
address: Mapped[str] = mapped_column(String(1024), default="")
class Service(Base):
__tablename__ = "services"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
title: Mapped[str] = mapped_column(String(255))
desc: Mapped[str] = mapped_column(Text)
icon: Mapped[str] = mapped_column(String(64))
order: Mapped[int] = mapped_column(Integer, default=0)
class Project(Base):
__tablename__ = "projects"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
slug: Mapped[str] = mapped_column(String(255), unique=True, index=True)
title: Mapped[str] = mapped_column(String(255))
location: Mapped[str] = mapped_column(String(255))
year: Mapped[str] = mapped_column(String(64))
status: Mapped[ProjectStatus] = mapped_column(Enum(ProjectStatus), default=ProjectStatus.progress)
image: Mapped[str] = mapped_column(String(1024))
gallery: Mapped[list] = mapped_column(JSON, default=list)
area: Mapped[str] = mapped_column(String(128))
floors: Mapped[str] = mapped_column(String(128))
units: Mapped[str] = mapped_column(String(128))
client: Mapped[str] = mapped_column(String(255))
summary: Mapped[str] = mapped_column(Text)
body: Mapped[list] = mapped_column(JSON, default=list)
order: Mapped[int] = mapped_column(Integer, default=0)
class Stat(Base):
__tablename__ = "stats"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
value: Mapped[int] = mapped_column(Integer)
suffix: Mapped[str] = mapped_column(String(32), default="")
label: Mapped[str] = mapped_column(String(255))
order: Mapped[int] = mapped_column(Integer, default=0)
class TimelineItem(Base):
__tablename__ = "timeline_items"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
year: Mapped[str] = mapped_column(String(64))
title: Mapped[str] = mapped_column(String(255))
desc: Mapped[str] = mapped_column(Text)
image: Mapped[str] = mapped_column(String(1024))
order: Mapped[int] = mapped_column(Integer, default=0)
class WhyUsItem(Base):
__tablename__ = "why_us_items"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
title: Mapped[str] = mapped_column(String(255))
desc: Mapped[str] = mapped_column(Text)
order: Mapped[int] = mapped_column(Integer, default=0)
class Testimonial(Base):
__tablename__ = "testimonials"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(255))
role: Mapped[str] = mapped_column(String(255))
quote: Mapped[str] = mapped_column(Text)
order: Mapped[int] = mapped_column(Integer, default=0)
class TeamMember(Base):
__tablename__ = "team_members"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(255))
role: Mapped[str] = mapped_column(String(255))
bio: Mapped[str] = mapped_column(Text)
photo: Mapped[str] = mapped_column(String(1024), default="")
order: Mapped[int] = mapped_column(Integer, default=0)
class Article(Base):
__tablename__ = "articles"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
slug: Mapped[str] = mapped_column(String(255), unique=True, index=True)
title: Mapped[str] = mapped_column(String(255))
date: Mapped[str] = mapped_column(String(64))
category: Mapped[str] = mapped_column(String(128))
image: Mapped[str] = mapped_column(String(1024))
read: Mapped[str] = mapped_column(String(32))
excerpt: Mapped[str] = mapped_column(Text)
body: Mapped[list] = mapped_column(JSON, default=list)
order: Mapped[int] = mapped_column(Integer, default=0)
class Faq(Base):
__tablename__ = "faqs"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
q: Mapped[str] = mapped_column(String(512))
a: Mapped[str] = mapped_column(Text)
order: Mapped[int] = mapped_column(Integer, default=0)
class ContactLead(Base):
__tablename__ = "contact_leads"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(255), default="")
phone: Mapped[str] = mapped_column(String(64))
note: Mapped[str] = mapped_column(Text, default="")
is_read: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
class NewsletterSubscriber(Base):
__tablename__ = "newsletter_subscribers"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
View File
+70
View File
@@ -0,0 +1,70 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.auth import get_current_admin
from app.database import get_db
from app import models, schemas
router = APIRouter(prefix="/api/articles", tags=["Articles"])
@router.get("", response_model=list[schemas.ArticleOut])
def list_articles(category: str | None = None, db: Session = Depends(get_db)):
query = db.query(models.Article)
if category:
query = query.filter(models.Article.category == category)
return query.order_by(models.Article.order).all()
@router.get("/{slug}", response_model=schemas.ArticleOut)
def get_article(slug: str, db: Session = Depends(get_db)):
article = db.query(models.Article).filter(models.Article.slug == slug).first()
if not article:
raise HTTPException(status.HTTP_404_NOT_FOUND, "مقاله پیدا نشد")
return article
@router.post("", response_model=schemas.ArticleOut, status_code=status.HTTP_201_CREATED)
def create_article(
payload: schemas.ArticleCreate,
db: Session = Depends(get_db),
_admin=Depends(get_current_admin),
):
if db.query(models.Article).filter(models.Article.slug == payload.slug).first():
raise HTTPException(status.HTTP_400_BAD_REQUEST, "این slug قبلاً استفاده شده است")
article = models.Article(**payload.model_dump())
db.add(article)
db.commit()
db.refresh(article)
return article
@router.put("/{article_id}", response_model=schemas.ArticleOut)
def update_article(
article_id: int,
payload: schemas.ArticleUpdate,
db: Session = Depends(get_db),
_admin=Depends(get_current_admin),
):
article = db.get(models.Article, article_id)
if not article:
raise HTTPException(status.HTTP_404_NOT_FOUND, "مقاله پیدا نشد")
for field, value in payload.model_dump(exclude_unset=True).items():
setattr(article, field, value)
db.commit()
db.refresh(article)
return article
@router.delete("/{article_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_article(
article_id: int,
db: Session = Depends(get_db),
_admin=Depends(get_current_admin),
):
article = db.get(models.Article, article_id)
if not article:
raise HTTPException(status.HTTP_404_NOT_FOUND, "مقاله پیدا نشد")
db.delete(article)
db.commit()
return None
+25
View File
@@ -0,0 +1,25 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app.auth import authenticate_admin, create_access_token
from app.database import get_db
from app.schemas import Token
router = APIRouter(prefix="/api/auth", tags=["Auth"])
@router.post("/login", response_model=Token)
def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
"""
ورود ادمین. با فرم استاندارد OAuth2 (username, password) کار می‌کند —
یعنی از Swagger UI (/docs) هم مستقیماً قابل تست است.
"""
user = authenticate_admin(db, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="نام کاربری یا رمز عبور اشتباه است",
)
token = create_access_token({"sub": user.username})
return Token(access_token=token)
+38
View File
@@ -0,0 +1,38 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.auth import get_current_admin
from app.database import get_db
from app import models, schemas
router = APIRouter(prefix="/api/brand", tags=["Brand"])
def _get_or_create_brand(db: Session) -> models.Brand:
brand = db.get(models.Brand, 1)
if not brand:
brand = models.Brand(id=1)
db.add(brand)
db.commit()
db.refresh(brand)
return brand
@router.get("", response_model=schemas.BrandOut)
def get_brand(db: Session = Depends(get_db)):
"""اطلاعات کلی برند (نام، لوگو، تلفن، ایمیل، آدرس و...) — یک رکورد ثابت."""
return _get_or_create_brand(db)
@router.put("", response_model=schemas.BrandOut)
def update_brand(
payload: schemas.BrandUpdate,
db: Session = Depends(get_db),
_admin=Depends(get_current_admin),
):
brand = _get_or_create_brand(db)
for field, value in payload.model_dump(exclude_unset=True).items():
setattr(brand, field, value)
db.commit()
db.refresh(brand)
return brand
+59
View File
@@ -0,0 +1,59 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.auth import get_current_admin
from app.database import get_db
from app import models, schemas
router = APIRouter(prefix="/api/contact", tags=["Contact"])
@router.post("", response_model=schemas.ContactOut, status_code=status.HTTP_201_CREATED)
def submit_contact(payload: schemas.ContactCreate, db: Session = Depends(get_db)):
"""این اندپوینت عمومی است — فرم تماس سایت آن را صدا می‌زند."""
lead = models.ContactLead(**payload.model_dump())
db.add(lead)
db.commit()
db.refresh(lead)
return lead
@router.get("", response_model=list[schemas.ContactOut])
def list_contacts(
db: Session = Depends(get_db),
_admin=Depends(get_current_admin),
):
"""فقط ادمین: لیست همه درخواست‌های تماس، جدیدترین اول."""
return db.query(models.ContactLead).order_by(models.ContactLead.created_at.desc()).all()
@router.put("/{lead_id}", response_model=schemas.ContactOut)
def update_contact(
lead_id: int,
payload: schemas.ContactUpdate,
db: Session = Depends(get_db),
_admin=Depends(get_current_admin),
):
"""فقط ادمین: مثلاً علامت زدن به‌عنوان خوانده‌شده."""
lead = db.get(models.ContactLead, lead_id)
if not lead:
raise HTTPException(status.HTTP_404_NOT_FOUND, "درخواست پیدا نشد")
for field, value in payload.model_dump(exclude_unset=True).items():
setattr(lead, field, value)
db.commit()
db.refresh(lead)
return lead
@router.delete("/{lead_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_contact(
lead_id: int,
db: Session = Depends(get_db),
_admin=Depends(get_current_admin),
):
lead = db.get(models.ContactLead, lead_id)
if not lead:
raise HTTPException(status.HTTP_404_NOT_FOUND, "درخواست پیدا نشد")
db.delete(lead)
db.commit()
return None
+48
View File
@@ -0,0 +1,48 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.auth import get_current_admin
from app.database import get_db
from app import models, schemas
router = APIRouter(prefix="/api/newsletter", tags=["Newsletter"])
@router.post("", response_model=schemas.NewsletterOut, status_code=status.HTTP_201_CREATED)
def subscribe(payload: schemas.NewsletterCreate, db: Session = Depends(get_db)):
"""اندپوینت عمومی — فرم خبرنامه سایت این را صدا می‌زند."""
existing = db.query(models.NewsletterSubscriber).filter(
models.NewsletterSubscriber.email == payload.email
).first()
if existing:
raise HTTPException(status.HTTP_400_BAD_REQUEST, "این ایمیل قبلاً ثبت شده است")
sub = models.NewsletterSubscriber(email=payload.email)
db.add(sub)
db.commit()
db.refresh(sub)
return sub
@router.get("", response_model=list[schemas.NewsletterOut])
def list_subscribers(
db: Session = Depends(get_db),
_admin=Depends(get_current_admin),
):
"""فقط ادمین: لیست همه مشترکین خبرنامه."""
return db.query(models.NewsletterSubscriber).order_by(
models.NewsletterSubscriber.created_at.desc()
).all()
@router.delete("/{sub_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_subscriber(
sub_id: int,
db: Session = Depends(get_db),
_admin=Depends(get_current_admin),
):
sub = db.get(models.NewsletterSubscriber, sub_id)
if not sub:
raise HTTPException(status.HTTP_404_NOT_FOUND, "مشترک پیدا نشد")
db.delete(sub)
db.commit()
return None
+70
View File
@@ -0,0 +1,70 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.auth import get_current_admin
from app.database import get_db
from app import models, schemas
router = APIRouter(prefix="/api/projects", tags=["Projects"])
@router.get("", response_model=list[schemas.ProjectOut])
def list_projects(status_filter: str | None = None, db: Session = Depends(get_db)):
query = db.query(models.Project)
if status_filter:
query = query.filter(models.Project.status == status_filter)
return query.order_by(models.Project.order).all()
@router.get("/{slug}", response_model=schemas.ProjectOut)
def get_project(slug: str, db: Session = Depends(get_db)):
project = db.query(models.Project).filter(models.Project.slug == slug).first()
if not project:
raise HTTPException(status.HTTP_404_NOT_FOUND, "پروژه پیدا نشد")
return project
@router.post("", response_model=schemas.ProjectOut, status_code=status.HTTP_201_CREATED)
def create_project(
payload: schemas.ProjectCreate,
db: Session = Depends(get_db),
_admin=Depends(get_current_admin),
):
if db.query(models.Project).filter(models.Project.slug == payload.slug).first():
raise HTTPException(status.HTTP_400_BAD_REQUEST, "این slug قبلاً استفاده شده است")
project = models.Project(**payload.model_dump())
db.add(project)
db.commit()
db.refresh(project)
return project
@router.put("/{project_id}", response_model=schemas.ProjectOut)
def update_project(
project_id: int,
payload: schemas.ProjectUpdate,
db: Session = Depends(get_db),
_admin=Depends(get_current_admin),
):
project = db.get(models.Project, project_id)
if not project:
raise HTTPException(status.HTTP_404_NOT_FOUND, "پروژه پیدا نشد")
for field, value in payload.model_dump(exclude_unset=True).items():
setattr(project, field, value)
db.commit()
db.refresh(project)
return project
@router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_project(
project_id: int,
db: Session = Depends(get_db),
_admin=Depends(get_current_admin),
):
project = db.get(models.Project, project_id)
if not project:
raise HTTPException(status.HTTP_404_NOT_FOUND, "پروژه پیدا نشد")
db.delete(project)
db.commit()
return None
+65
View File
@@ -0,0 +1,65 @@
from app.crud_router import build_crud_router
from app import models, schemas
services_router = build_crud_router(
prefix="/api/services",
tag="Services",
model=models.Service,
create_schema=schemas.ServiceCreate,
update_schema=schemas.ServiceUpdate,
out_schema=schemas.ServiceOut,
)
stats_router = build_crud_router(
prefix="/api/stats",
tag="Stats",
model=models.Stat,
create_schema=schemas.StatCreate,
update_schema=schemas.StatUpdate,
out_schema=schemas.StatOut,
)
timeline_router = build_crud_router(
prefix="/api/timeline",
tag="Timeline",
model=models.TimelineItem,
create_schema=schemas.TimelineCreate,
update_schema=schemas.TimelineUpdate,
out_schema=schemas.TimelineOut,
)
why_us_router = build_crud_router(
prefix="/api/why-us",
tag="WhyUs",
model=models.WhyUsItem,
create_schema=schemas.WhyUsCreate,
update_schema=schemas.WhyUsUpdate,
out_schema=schemas.WhyUsOut,
)
testimonials_router = build_crud_router(
prefix="/api/testimonials",
tag="Testimonials",
model=models.Testimonial,
create_schema=schemas.TestimonialCreate,
update_schema=schemas.TestimonialUpdate,
out_schema=schemas.TestimonialOut,
)
team_router = build_crud_router(
prefix="/api/team",
tag="Team",
model=models.TeamMember,
create_schema=schemas.TeamMemberCreate,
update_schema=schemas.TeamMemberUpdate,
out_schema=schemas.TeamMemberOut,
)
faqs_router = build_crud_router(
prefix="/api/faqs",
tag="FAQs",
model=models.Faq,
create_schema=schemas.FaqCreate,
update_schema=schemas.FaqUpdate,
out_schema=schemas.FaqOut,
)
+323
View File
@@ -0,0 +1,323 @@
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, ConfigDict, EmailStr, field_validator
from app.models import ProjectStatus
# ---------- Auth ----------
class Token(BaseModel):
access_token: str
token_type: str = "bearer"
class LoginRequest(BaseModel):
username: str
password: str
# ---------- Brand ----------
class BrandBase(BaseModel):
name: str = ""
logo: str = ""
hero: str = ""
contact_bg: str = ""
phone: str = ""
phone_href: str = ""
email: str = ""
address: str = ""
class BrandOut(BrandBase):
model_config = ConfigDict(from_attributes=True)
id: int
class BrandUpdate(BrandBase):
pass
# ---------- Service ----------
class ServiceBase(BaseModel):
title: str
desc: str
icon: str
order: int = 0
class ServiceCreate(ServiceBase):
pass
class ServiceUpdate(BaseModel):
title: Optional[str] = None
desc: Optional[str] = None
icon: Optional[str] = None
order: Optional[int] = None
class ServiceOut(ServiceBase):
model_config = ConfigDict(from_attributes=True)
id: int
# ---------- Project ----------
class ProjectBase(BaseModel):
slug: str
title: str
location: str
year: str
status: ProjectStatus
image: str
gallery: list[str] = []
area: str
floors: str
units: str
client: str
summary: str
body: list[str] = []
order: int = 0
class ProjectCreate(ProjectBase):
pass
class ProjectUpdate(BaseModel):
slug: Optional[str] = None
title: Optional[str] = None
location: Optional[str] = None
year: Optional[str] = None
status: Optional[ProjectStatus] = None
image: Optional[str] = None
gallery: Optional[list[str]] = None
area: Optional[str] = None
floors: Optional[str] = None
units: Optional[str] = None
client: Optional[str] = None
summary: Optional[str] = None
body: Optional[list[str]] = None
order: Optional[int] = None
class ProjectOut(ProjectBase):
model_config = ConfigDict(from_attributes=True)
id: int
# ---------- Stat ----------
class StatBase(BaseModel):
value: int
suffix: str = ""
label: str
order: int = 0
class StatCreate(StatBase):
pass
class StatUpdate(BaseModel):
value: Optional[int] = None
suffix: Optional[str] = None
label: Optional[str] = None
order: Optional[int] = None
class StatOut(StatBase):
model_config = ConfigDict(from_attributes=True)
id: int
# ---------- Timeline ----------
class TimelineBase(BaseModel):
year: str
title: str
desc: str
image: str
order: int = 0
class TimelineCreate(TimelineBase):
pass
class TimelineUpdate(BaseModel):
year: Optional[str] = None
title: Optional[str] = None
desc: Optional[str] = None
image: Optional[str] = None
order: Optional[int] = None
class TimelineOut(TimelineBase):
model_config = ConfigDict(from_attributes=True)
id: int
# ---------- WhyUs ----------
class WhyUsBase(BaseModel):
title: str
desc: str
order: int = 0
class WhyUsCreate(WhyUsBase):
pass
class WhyUsUpdate(BaseModel):
title: Optional[str] = None
desc: Optional[str] = None
order: Optional[int] = None
class WhyUsOut(WhyUsBase):
model_config = ConfigDict(from_attributes=True)
id: int
# ---------- Testimonial ----------
class TestimonialBase(BaseModel):
name: str
role: str
quote: str
order: int = 0
class TestimonialCreate(TestimonialBase):
pass
class TestimonialUpdate(BaseModel):
name: Optional[str] = None
role: Optional[str] = None
quote: Optional[str] = None
order: Optional[int] = None
class TestimonialOut(TestimonialBase):
model_config = ConfigDict(from_attributes=True)
id: int
# ---------- Team ----------
class TeamMemberBase(BaseModel):
name: str
role: str
bio: str
photo: str = ""
order: int = 0
class TeamMemberCreate(TeamMemberBase):
pass
class TeamMemberUpdate(BaseModel):
name: Optional[str] = None
role: Optional[str] = None
bio: Optional[str] = None
photo: Optional[str] = None
order: Optional[int] = None
class TeamMemberOut(TeamMemberBase):
model_config = ConfigDict(from_attributes=True)
id: int
# ---------- Article ----------
class ArticleBase(BaseModel):
slug: str
title: str
date: str
category: str
image: str
read: str
excerpt: str
body: list[str] = []
order: int = 0
class ArticleCreate(ArticleBase):
pass
class ArticleUpdate(BaseModel):
slug: Optional[str] = None
title: Optional[str] = None
date: Optional[str] = None
category: Optional[str] = None
image: Optional[str] = None
read: Optional[str] = None
excerpt: Optional[str] = None
body: Optional[list[str]] = None
order: Optional[int] = None
class ArticleOut(ArticleBase):
model_config = ConfigDict(from_attributes=True)
id: int
# ---------- FAQ ----------
class FaqBase(BaseModel):
q: str
a: str
order: int = 0
class FaqCreate(FaqBase):
pass
class FaqUpdate(BaseModel):
q: Optional[str] = None
a: Optional[str] = None
order: Optional[int] = None
class FaqOut(FaqBase):
model_config = ConfigDict(from_attributes=True)
id: int
# ---------- Contact ----------
class ContactCreate(BaseModel):
name: str = ""
phone: str
note: str = ""
@field_validator("phone")
@classmethod
def phone_min_length(cls, v: str) -> str:
if len(v.strip()) < 8:
raise ValueError("شماره تلفن معتبر نیست")
return v.strip()
class ContactOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
phone: str
note: str
is_read: bool
created_at: datetime
class ContactUpdate(BaseModel):
is_read: Optional[bool] = None
# ---------- Newsletter ----------
class NewsletterCreate(BaseModel):
email: EmailStr
class NewsletterOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
email: str
created_at: datetime