From 897fac4f15792ad88fa294f4ada66a02151a9f7a Mon Sep 17 00:00:00 2001 From: MMD Date: Sun, 6 Sep 2026 20:49:01 +0330 Subject: [PATCH] =?UTF-8?q?=D8=B0=D8=AE=DB=8C=D8=B1=D9=87=E2=80=8C=D8=B3?= =?UTF-8?q?=D8=A7=D8=B2=DB=8C=20=D8=AA=D8=B5=D8=A7=D9=88=DB=8C=D8=B1=20?= =?UTF-8?q?=D8=A8=D8=A7=20MinIO=20(S3)=20=D9=88=20=D8=A7=D9=86=D8=AA=D9=87?= =?UTF-8?q?=D8=A7=DB=8C=E2=80=8C=D9=86=D9=82=D8=B7=D9=87=20=D8=A2=D9=BE?= =?UTF-8?q?=D9=84=D9=88=D8=AF=20=D8=A7=D8=AF=D9=85=DB=8C=D9=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - POST /api/admin/upload با احراز هویت JWT: ذخیره فایل در باکت S3 و برگرداندن URL نسبی /media/<سال>/<ماه>/.<پسوند> - فرمت‌های مجاز: jpg، png، webp، gif، avif — حداکثر ۱۰ مگابایت (قابل تنظیم) - افزودن boto3 و python-multipart به وابستگی‌ها - تنظیمات جدید: S3_ENDPOINT، S3_ACCESS_KEY، S3_SECRET_KEY، S3_BUCKET، MEDIA_PUBLIC_BASE، MEDIA_MAX_UPLOAD_MB (قابل تعریف در .env) - URL نسبی برمی‌گردد تا بک‌اند ذخیره‌سازی (MinIO/R2/هر S3) بدون تغییر محتوای دیتابیس قابل تعویض باشد - به‌روزرسانی .env.example و README (بخش تصاویر و MinIO) --- backend/.env.example | 12 ++++++++ backend/README.md | 15 ++++++++-- backend/app/api/admin.py | 63 +++++++++++++++++++++++++++++++++++++++- backend/app/config.py | 9 ++++++ backend/requirements.txt | 2 ++ 5 files changed, 97 insertions(+), 4 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index f88754f..1429894 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -15,3 +15,15 @@ CORS_ORIGINS=["http://localhost:5173","http://127.0.0.1:5173"] # مشخصات ادمین پیش‌فرض که هنگام seed ساخته می‌شود ADMIN_USERNAME=admin ADMIN_PASSWORD=aramesh123 + +# Object Storage (MinIO) برای آپلود تصاویر پنل مدیریت +S3_ENDPOINT=http://localhost:9000 +S3_ACCESS_KEY=aramesh +S3_SECRET_KEY=change-me-minio-secret +S3_BUCKET=aramesh-media + +# پیشوند عمومی مسیر تصاویر — پروکسی Reverse باید آن را به باکت برساند +MEDIA_PUBLIC_BASE=/media + +# حداکثر حجم آپلود تصویر (مگابایت) +MEDIA_MAX_UPLOAD_MB=10 diff --git a/backend/README.md b/backend/README.md index 1407cdb..3371252 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,14 +1,15 @@ # بک‌اند گروه ساختمانی آرامش (Aramesh Building Group API) -بک‌اند سایت گروه ساختمانی آرامش — ساخته‌شده با **FastAPI** + **PostgreSQL** + **SQLAlchemy 2** + **JWT**. +بک‌اند سایت گروه ساختمانی آرامش — ساخته‌شده با **FastAPI** + **PostgreSQL** + **SQLAlchemy 2** + **JWT** + **MinIO** (ذخیره تصاویر) + **Traefik** (مسیریابی). ## امکانات -- **API عمومی محتوا**: برند، خدمات، پروژه‌ها، مقالات، نظرات مشتریان، سوالات متداول، تیم، تایم‌لاین، آمار، چرا آرامش، و بخش قبل/بعد +- **API عمومی محتوا**: برند، منو، هیرو، داستان، تیتر بخش‌ها، هیرو صفحات، خدمات، پروژه‌ها، مقالات، نظرات مشتریان، سوالات متداول، تیم، تایم‌لاین، آمار، چرا آرامش، قبل/بعد، مدل‌های سرمایه‌گذاری، مراحل کار، دفاتر، گالری، فرم تماس، خبرنامه و فوتر — **تمام متن‌های سایت در دیتابیس است** - **فرم تماس**: ثبت پیام مشتریان از سایت (`POST /api/contact`) - **خبرنامه**: عضویت و مدیریت مشترکین (`POST /api/newsletter`) - **پنل مدیریت**: ورود با نام کاربری/رمز عبور و دریافت توکن JWT، سپس CRUD کامل همه محتواها، مدیریت پیام‌های تماس و مشترکین -- **Seed خودکار**: همه محتوای فعلی سایت با یک دستور در دیتابیس قرار می‌گیرد +- **آپلود تصویر**: `POST /api/admin/upload` — فایل در MinIO ذخیره و URL نسبی `/media/...` برمی‌گردد +- **Seed خودکار**: همه محتوای فعلی سایت با یک دستور در دیتابیس قرار می‌گیرد (فقط جدول‌های خالی پر می‌شوند — ارتقا بدون از دست رفتن داده) - مستندات تعاملی Swagger در مسیر `/docs` ## راه‌اندازی @@ -124,6 +125,14 @@ Authorization: Bearer | DELETE | `/api/admin/messages/{id}` | حذف پیام | | GET | `/api/admin/subscribers` | مشترکین خبرنامه | | DELETE | `/api/admin/subscribers/{id}` | حذف مشترک | +| POST | `/api/admin/upload` | آپلود تصویر (multipart، حداکثر ۱۰MB — jpg/png/webp/gif/avif) → `{"url": "/media/..."}` | + +### تصاویر و MinIO + +فایل‌های آپلودشده در باکت `aramesh-media` روی MinIO ذخیره می‌شوند و با مسیر نسبی `/media/<سال>/<ماه>/<فایل>` برگردانده می‌شوند. Traefik مسیر `/media` را به MinIO می‌برد، پس URLها مستقل از بک‌اند ذخیره‌سازی هستند — بعداً برای مهاجرت به R2/S3 فقط تنظیمات را عوض کنید، نه محتوای دیتابیس را. + +- کنسول MinIO: `http://localhost:9001` (کاربر/رمز از `MINIO_ROOT_USER` / `MINIO_ROOT_PASSWORD` در compose) +- تصاویر عمومی و قابل نمایش‌اند؛ آپلود فقط با توکن ادمین انجام می‌شود. ## اتصال به فرانت‌اند diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py index 7b0c8eb..4c84321 100644 --- a/backend/app/api/admin.py +++ b/backend/app/api/admin.py @@ -1,11 +1,16 @@ """انتهای پنل مدیریت — همه مسیرها نیازمند توکن JWT معتبر هستند.""" -from fastapi import APIRouter, Depends, HTTPException, status +import uuid +from datetime import datetime, timezone + +import boto3 +from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, 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 ..config import settings from ..database import get_db from ..schemas import ( ArticleIn, @@ -158,6 +163,62 @@ register_crud("newsletter", models.NewsletterSectionContent, NewsletterOut, News register_crud("footer", models.FooterContent, FooterOut, FooterIn) +# آپلود تصویر به Object Storage (MinIO) — URL نسبی برمی‌گرداند تا بک‌اند ذخیره‌سازی قابل تعویض باشد +ALLOWED_IMAGE_TYPES = { + "image/jpeg": "jpg", + "image/png": "png", + "image/webp": "webp", + "image/gif": "gif", + "image/avif": "avif", +} + + +@router.post("/upload") +async def upload_media( + file: UploadFile = File(...), + _admin: models.AdminUser = Depends(get_current_admin), +): + content_type = (file.content_type or "").lower() + ext = ALLOWED_IMAGE_TYPES.get(content_type) + if ext is None: + raise HTTPException( + status_code=415, + detail="فرمت فایل پشتیبانی نمی‌شود (مجاز: jpg، png، webp، gif، avif)", + ) + + max_bytes = settings.media_max_upload_mb * 1024 * 1024 + data = await file.read() + if len(data) == 0: + raise HTTPException(status_code=400, detail="فایل خالی است") + if len(data) > max_bytes: + raise HTTPException( + status_code=413, + detail=f"حجم فایل بیش از {settings.media_max_upload_mb} مگابایت است", + ) + + now = datetime.now(timezone.utc) + key = f"{now:%Y/%m}/{uuid.uuid4().hex}.{ext}" + + 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", + ) + try: + s3.put_object(Bucket=settings.s3_bucket, Key=key, Body=data, ContentType=content_type) + except Exception: + raise HTTPException(status_code=502, detail="ذخیره فایل در Object Storage ناموفق بود") + + return { + "url": f"{settings.media_public_base}/{key}", + "key": key, + "size": len(data), + "content_type": content_type, + } + + # پیام‌های تماس @router.get("/messages", response_model=list[ContactMessageOut]) def list_messages(unread_only: bool = False, db: Session = Depends(get_db)): diff --git a/backend/app/config.py b/backend/app/config.py index 641e5aa..e425adc 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -18,5 +18,14 @@ class Settings(BaseSettings): admin_username: str = "admin" admin_password: str = "aramesh123" + # Object Storage (MinIO / هر S3 سازگار) برای آپلود تصاویر + s3_endpoint: str = "http://localhost:9000" + s3_access_key: str = "aramesh" + s3_secret_key: str = "aramesh-minio-secret" + s3_bucket: str = "aramesh-media" + # پیشوند عمومی مسیر تصاویر — nginx آن را به باکت پروکسی می‌کند + media_public_base: str = "/media" + media_max_upload_mb: int = 10 + settings = Settings() diff --git a/backend/requirements.txt b/backend/requirements.txt index 76b1ecc..b941c1b 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -7,3 +7,5 @@ pydantic-settings>=2.6.1 PyJWT>=2.10.1 bcrypt>=4.2.1 email-validator>=2.2.0 +boto3>=1.35.0 +python-multipart>=0.0.18