اولین نسخه بکاند آرامش پلازا
This commit is contained in:
+59
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user