test news
This commit is contained in:
86
app/api/news.py
Normal file
86
app/api/news.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException, Query, Depends, Form
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from app.services.auth import AuthService
|
||||||
|
from app.services.news import NewsService
|
||||||
|
from app.models.news import NewsCreate, NewsUpdate, NewsInDB
|
||||||
|
|
||||||
|
router = APIRouter(tags=["News"])
|
||||||
|
|
||||||
|
news_service = NewsService()
|
||||||
|
|
||||||
|
# --- Публичные эндпоинты для лаунчера ---
|
||||||
|
|
||||||
|
@router.get("/news", response_model=List[NewsInDB])
|
||||||
|
async def list_news(
|
||||||
|
limit: int = Query(20, ge=1, le=100),
|
||||||
|
skip: int = Query(0, ge=0),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Список опубликованных новостей (для лаунчера).
|
||||||
|
"""
|
||||||
|
return await news_service.list_news(limit=limit, skip=skip, include_unpublished=False)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/news/{news_id}", response_model=NewsInDB)
|
||||||
|
async def get_news(news_id: str):
|
||||||
|
"""
|
||||||
|
Получить одну новость по id (для лаунчера).
|
||||||
|
"""
|
||||||
|
return await news_service.get_news(news_id)
|
||||||
|
|
||||||
|
# --- Админские эндпоинты (создание/редактирование) ---
|
||||||
|
|
||||||
|
async def validate_admin(accessToken: str, clientToken: str):
|
||||||
|
auth = AuthService()
|
||||||
|
if not await auth.is_admin(accessToken, clientToken):
|
||||||
|
raise HTTPException(status_code=403, detail="Admin privileges required")
|
||||||
|
|
||||||
|
@router.post("/news", response_model=NewsInDB)
|
||||||
|
async def create_news(
|
||||||
|
accessToken: str = Form(...),
|
||||||
|
clientToken: str = Form(...),
|
||||||
|
title: str = Form(...),
|
||||||
|
markdown: str = Form(...),
|
||||||
|
preview: str = Form(""),
|
||||||
|
is_published: bool = Form(True),
|
||||||
|
):
|
||||||
|
await validate_admin(accessToken, clientToken)
|
||||||
|
payload = NewsCreate(
|
||||||
|
title=title,
|
||||||
|
markdown=markdown,
|
||||||
|
preview=preview or None,
|
||||||
|
is_published=is_published,
|
||||||
|
tags=[],
|
||||||
|
)
|
||||||
|
return await news_service.create_news(payload)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/news/{news_id}", response_model=NewsInDB)
|
||||||
|
async def update_news(
|
||||||
|
news_id: str,
|
||||||
|
accessToken: str = Form(...),
|
||||||
|
clientToken: str = Form(...),
|
||||||
|
title: str | None = Form(None),
|
||||||
|
markdown: str | None = Form(None),
|
||||||
|
preview: str | None = Form(None),
|
||||||
|
is_published: bool | None = Form(None),
|
||||||
|
):
|
||||||
|
await validate_admin(accessToken, clientToken)
|
||||||
|
payload = NewsUpdate(
|
||||||
|
title=title,
|
||||||
|
markdown=markdown,
|
||||||
|
preview=preview,
|
||||||
|
is_published=is_published,
|
||||||
|
)
|
||||||
|
return await news_service.update_news(news_id, payload)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/news/{news_id}")
|
||||||
|
async def delete_news(
|
||||||
|
news_id: str,
|
||||||
|
accessToken: str,
|
||||||
|
clientToken: str,
|
||||||
|
):
|
||||||
|
await validate_admin(accessToken, clientToken)
|
||||||
|
return await news_service.delete_news(news_id)
|
||||||
28
app/models/news.py
Normal file
28
app/models/news.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Optional, List
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class NewsBase(BaseModel):
|
||||||
|
title: str = Field(..., max_length=200)
|
||||||
|
markdown: str # полный текст в Markdown
|
||||||
|
preview: Optional[str] = None # краткий текст/анонс (тоже можно в MD)
|
||||||
|
tags: List[str] = []
|
||||||
|
is_published: bool = True
|
||||||
|
|
||||||
|
class NewsCreate(NewsBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NewsUpdate(BaseModel):
|
||||||
|
title: Optional[str] = None
|
||||||
|
markdown: Optional[str] = None
|
||||||
|
preview: Optional[str] = None
|
||||||
|
tags: Optional[List[str]] = None
|
||||||
|
is_published: Optional[bool] = None
|
||||||
|
|
||||||
|
class NewsInDB(NewsBase):
|
||||||
|
id: str
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
@ -25,6 +25,7 @@ class UserInDB(BaseModel):
|
|||||||
telegram_id: Optional[str] = None
|
telegram_id: Optional[str] = None
|
||||||
is_verified: bool = False
|
is_verified: bool = False
|
||||||
code_expires_at: Optional[datetime] = None
|
code_expires_at: Optional[datetime] = None
|
||||||
|
is_admin: bool = False
|
||||||
class Session(BaseModel):
|
class Session(BaseModel):
|
||||||
access_token: str
|
access_token: str
|
||||||
client_token: str
|
client_token: str
|
||||||
|
|||||||
@ -42,7 +42,8 @@ class AuthService:
|
|||||||
uuid=user_uuid,
|
uuid=user_uuid,
|
||||||
is_verified=False,
|
is_verified=False,
|
||||||
code=None,
|
code=None,
|
||||||
code_expires_at=None
|
code_expires_at=None,
|
||||||
|
is_admin=False
|
||||||
)
|
)
|
||||||
await users_collection.insert_one(new_user.dict())
|
await users_collection.insert_one(new_user.dict())
|
||||||
return {"status": "success", "uuid": user_uuid}
|
return {"status": "success", "uuid": user_uuid}
|
||||||
@ -131,6 +132,17 @@ class AuthService:
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
async def is_admin(self, access_token: str, client_token: str) -> bool:
|
||||||
|
session = await sessions_collection.find_one({
|
||||||
|
"access_token": access_token,
|
||||||
|
"client_token": client_token,
|
||||||
|
})
|
||||||
|
if not session:
|
||||||
|
return False
|
||||||
|
|
||||||
|
user = await users_collection.find_one({"uuid": session["user_uuid"]})
|
||||||
|
return user and user.get("is_admin") is True
|
||||||
|
|
||||||
async def refresh(self, access_token: str, client_token: str):
|
async def refresh(self, access_token: str, client_token: str):
|
||||||
if not await self.validate(access_token, client_token):
|
if not await self.validate(access_token, client_token):
|
||||||
return None
|
return None
|
||||||
|
|||||||
97
app/services/news.py
Normal file
97
app/services/news.py
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Optional
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from app import db
|
||||||
|
from bson import ObjectId
|
||||||
|
from app.models.news import NewsCreate, NewsUpdate, NewsInDB
|
||||||
|
|
||||||
|
news_collection = db["news"]
|
||||||
|
|
||||||
|
class NewsService:
|
||||||
|
@staticmethod
|
||||||
|
def _to_news_in_db(doc) -> NewsInDB:
|
||||||
|
return NewsInDB(
|
||||||
|
id=str(doc["_id"]),
|
||||||
|
title=doc["title"],
|
||||||
|
markdown=doc["markdown"],
|
||||||
|
preview=doc.get("preview"),
|
||||||
|
tags=doc.get("tags", []),
|
||||||
|
is_published=doc.get("is_published", True),
|
||||||
|
created_at=doc["created_at"],
|
||||||
|
updated_at=doc["updated_at"],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def list_news(self, limit: int = 20, skip: int = 0, include_unpublished: bool = False) -> List[NewsInDB]:
|
||||||
|
query = {}
|
||||||
|
if not include_unpublished:
|
||||||
|
query["is_published"] = True
|
||||||
|
|
||||||
|
cursor = (
|
||||||
|
news_collection
|
||||||
|
.find(query)
|
||||||
|
.sort("created_at", -1)
|
||||||
|
.skip(skip)
|
||||||
|
.limit(limit)
|
||||||
|
)
|
||||||
|
docs = await cursor.to_list(length=limit)
|
||||||
|
return [self._to_news_in_db(d) for d in docs]
|
||||||
|
|
||||||
|
async def get_news(self, news_id: str) -> NewsInDB:
|
||||||
|
try:
|
||||||
|
oid = ObjectId(news_id)
|
||||||
|
except:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid news id")
|
||||||
|
|
||||||
|
doc = await news_collection.find_one({"_id": oid})
|
||||||
|
if not doc:
|
||||||
|
raise HTTPException(status_code=404, detail="News not found")
|
||||||
|
return self._to_news_in_db(doc)
|
||||||
|
|
||||||
|
async def create_news(self, payload: NewsCreate) -> NewsInDB:
|
||||||
|
now = datetime.utcnow()
|
||||||
|
doc = {
|
||||||
|
"title": payload.title,
|
||||||
|
"markdown": payload.markdown,
|
||||||
|
"preview": payload.preview,
|
||||||
|
"tags": payload.tags,
|
||||||
|
"is_published": payload.is_published,
|
||||||
|
"created_at": now,
|
||||||
|
"updated_at": now,
|
||||||
|
}
|
||||||
|
result = await news_collection.insert_one(doc)
|
||||||
|
doc["_id"] = result.inserted_id
|
||||||
|
return self._to_news_in_db(doc)
|
||||||
|
|
||||||
|
async def update_news(self, news_id: str, payload: NewsUpdate) -> NewsInDB:
|
||||||
|
try:
|
||||||
|
oid = ObjectId(news_id)
|
||||||
|
except:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid news id")
|
||||||
|
|
||||||
|
update_data = {k: v for k, v in payload.dict(exclude_unset=True).items()}
|
||||||
|
if not update_data:
|
||||||
|
return await self.get_news(news_id)
|
||||||
|
|
||||||
|
update_data["updated_at"] = datetime.utcnow()
|
||||||
|
|
||||||
|
result = await news_collection.find_one_and_update(
|
||||||
|
{"_id": oid},
|
||||||
|
{"$set": update_data},
|
||||||
|
return_document=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
raise HTTPException(status_code=404, detail="News not found")
|
||||||
|
|
||||||
|
return self._to_news_in_db(result)
|
||||||
|
|
||||||
|
async def delete_news(self, news_id: str):
|
||||||
|
try:
|
||||||
|
oid = ObjectId(news_id)
|
||||||
|
except:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid news id")
|
||||||
|
|
||||||
|
result = await news_collection.delete_one({"_id": oid})
|
||||||
|
if result.deleted_count == 0:
|
||||||
|
raise HTTPException(status_code=404, detail="News not found")
|
||||||
|
return {"status": "success"}
|
||||||
Reference in New Issue
Block a user