Add public map with JWT admin login and protect admin APIs.

Keep map reads open; gate admin UI/nav and object mutations behind env-based admin credentials, and default parser batch limit to 10.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-16 23:04:21 +03:00
co-authored by Cursor
parent 52185aeaa8
commit 8ab606747f
23 changed files with 564 additions and 41 deletions
+2 -1
View File
@@ -4,6 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from ..database import get_db
from ..deps import verify_admin
from ..models import Consumer, Event, MapObject, ParseChannel, ParseJob, ParserProfile
from ..schemas import (
AnalyticsSummary,
@@ -37,7 +38,7 @@ from ..services.filtering import (
)
from ..services.jobs import enqueue_parse_job, flatten_pair_config
router = APIRouter(prefix="/admin", tags=["admin"])
router = APIRouter(prefix="/admin", tags=["admin"], dependencies=[Depends(verify_admin)])
def _validated_source_config(source_type: str, source_config: dict) -> dict:
+41
View File
@@ -0,0 +1,41 @@
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from ..auth import (
admin_credentials_configured,
create_access_token,
verify_credentials,
)
from ..deps import verify_admin
router = APIRouter(prefix="/admin/auth", tags=["auth"])
class LoginRequest(BaseModel):
username: str = Field(min_length=1)
password: str = Field(min_length=1)
class TokenResponse(BaseModel):
access_token: str
token_type: str = "bearer"
class MeResponse(BaseModel):
username: str
role: str = "admin"
@router.post("/login", response_model=TokenResponse)
def login(payload: LoginRequest):
if not admin_credentials_configured():
raise HTTPException(status_code=503, detail="Admin auth is not configured")
if not verify_credentials(payload.username, payload.password):
raise HTTPException(status_code=401, detail="Invalid username or password")
token = create_access_token(username=payload.username.strip())
return TokenResponse(access_token=token)
@router.get("/me", response_model=MeResponse)
def me(username: str = Depends(verify_admin)):
return MeResponse(username=username)
+18 -3
View File
@@ -3,6 +3,7 @@ from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from ..database import get_db
from ..deps import verify_admin
from ..models import MapObject, ObjectMedia
from ..schemas import MapObjectCreate, MapObjectRead, MapObjectUpdate, ObjectMediaRead
from ..storage import (
@@ -48,7 +49,11 @@ def get_object(object_id: int, db: Session = Depends(get_db)):
@router.post("/api/objects", response_model=MapObjectRead, status_code=201)
def create_object(payload: MapObjectCreate, db: Session = Depends(get_db)):
def create_object(
payload: MapObjectCreate,
db: Session = Depends(get_db),
_: str = Depends(verify_admin),
):
data = payload.model_dump()
created_at = data.pop("created_at", None)
obj = MapObject(**data)
@@ -65,6 +70,7 @@ def update_object(
object_id: int,
payload: MapObjectUpdate,
db: Session = Depends(get_db),
_: str = Depends(verify_admin),
):
obj = db.query(MapObject).filter(MapObject.id == object_id).first()
if not obj:
@@ -79,7 +85,11 @@ def update_object(
@router.delete("/api/objects/{object_id}", status_code=204)
def delete_object(object_id: int, db: Session = Depends(get_db)):
def delete_object(
object_id: int,
db: Session = Depends(get_db),
_: str = Depends(verify_admin),
):
obj = db.query(MapObject).filter(MapObject.id == object_id).first()
if not obj:
raise HTTPException(status_code=404, detail="Объект не найден")
@@ -111,6 +121,7 @@ async def upload_object_media(
object_id: int,
file: UploadFile = File(...),
db: Session = Depends(get_db),
_: str = Depends(verify_admin),
):
obj = db.query(MapObject).filter(MapObject.id == object_id).first()
if not obj:
@@ -159,7 +170,11 @@ def get_media_file(media_id: int, db: Session = Depends(get_db)):
@router.delete("/api/media/{media_id}", status_code=204)
def delete_media(media_id: int, db: Session = Depends(get_db)):
def delete_media(
media_id: int,
db: Session = Depends(get_db),
_: str = Depends(verify_admin),
):
media = db.query(ObjectMedia).filter(ObjectMedia.id == media_id).first()
if not media:
raise HTTPException(status_code=404, detail="Медиафайл не найден")
@@ -6,10 +6,15 @@ from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from ..database import get_db
from ..deps import verify_admin
from ..models import ParseChannel, ParseJob
from ..schemas import ParseChannelCreate, ParseChannelRead, ParseChannelUpdate
router = APIRouter(prefix="/admin/parse-channels", tags=["parse-channels"])
router = APIRouter(
prefix="/admin/parse-channels",
tags=["parse-channels"],
dependencies=[Depends(verify_admin)],
)
ALLOWED_SOURCE_TYPES = {"telegram"}
@@ -11,11 +11,16 @@ from sqlalchemy.orm import Session
from contracts.heuristic_profile import HeuristicProfile
from ..database import get_db
from ..deps import verify_admin
from ..models import ParseJob, ParserProfile
from ..schemas import ParserProfileCreate, ParserProfileRead, ParserProfileUpdate
from ..services import parser_builder as builder
router = APIRouter(prefix="/admin/parser-profiles", tags=["parser-profiles"])
router = APIRouter(
prefix="/admin/parser-profiles",
tags=["parser-profiles"],
dependencies=[Depends(verify_admin)],
)
class GenerateRequest(BaseModel):