وبلاگ و سئو: پیشنویس/انتشار مقاله + کلمه کلیدی سئو در دیتابیس + OG/canonical/JSON-LD و متای سمت سرور + تغییر رمز و محدودسازی ورود + تستها
- مقالهها ستون 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 سرگردان فرانت
This commit is contained in:
@@ -4,15 +4,17 @@ import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import boto3
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile, status
|
||||
from sqlalchemy import func
|
||||
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 ..auth import create_access_token, get_current_admin, hash_password, verify_password
|
||||
from ..config import settings
|
||||
from ..database import get_db
|
||||
from ..rate_limit import clear as clear_rate_limit
|
||||
from ..rate_limit import ensure_allowed, record_failure
|
||||
from ..schemas import (
|
||||
ArticleIn,
|
||||
ArticleOut,
|
||||
@@ -23,6 +25,7 @@ from ..schemas import (
|
||||
BrandIn,
|
||||
BrandOut,
|
||||
BrandPatch,
|
||||
ChangePasswordIn,
|
||||
ContactSectionIn,
|
||||
ContactSectionOut,
|
||||
ContactSectionPatch,
|
||||
@@ -105,16 +108,33 @@ router = APIRouter(dependencies=[Depends(get_current_admin)])
|
||||
|
||||
|
||||
@auth_router.post("/auth/login", response_model=TokenOut)
|
||||
def login(payload: LoginIn, db: Session = Depends(get_db)):
|
||||
def login(payload: LoginIn, request: Request, db: Session = Depends(get_db)):
|
||||
ensure_allowed(request)
|
||||
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):
|
||||
record_failure(request)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="نام کاربری یا رمز عبور اشتباه است",
|
||||
)
|
||||
clear_rate_limit(request)
|
||||
return TokenOut(access_token=create_access_token(user.username))
|
||||
|
||||
|
||||
@auth_router.post("/auth/change-password")
|
||||
def change_password(
|
||||
payload: ChangePasswordIn,
|
||||
admin: models.AdminUser = Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""تغییر رمز ادمین واردشده — رمز فعلی باید درست باشد."""
|
||||
if not verify_password(payload.current_password, admin.password_hash):
|
||||
raise HTTPException(status_code=400, detail="رمز عبور فعلی اشتباه است")
|
||||
admin.password_hash = hash_password(payload.new_password)
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
def register_crud(path: str, model, out_schema, create_schema, patch_schema=None) -> None:
|
||||
"""ثبت مسیرهای استاندارد CRUD (لیست، ساخت، دریافت، ویرایش کامل، ویرایش جزئی،
|
||||
حذف و ترتیببندی) روی روتر مدیریت."""
|
||||
|
||||
Reference in New Issue
Block a user