İlk commit: Stepstead backend (FastAPI)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
ea049b8ccd
45 changed files with 6751 additions and 0 deletions
267
routers/admin.py
Normal file
267
routers/admin.py
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
"""
|
||||
Admin — asset upload (harita görselleri vb.).
|
||||
|
||||
Sadece ADMIN_EMAILS listesindeki hesaplar kullanabilir (oyun JWT'si ile).
|
||||
Dosyalar /var/www/stepstead.com/assets/uploads/ altına yazılır ve
|
||||
https://stepstead.com/assets/uploads/<ad> ile servis edilir.
|
||||
"""
|
||||
import re
|
||||
import unicodedata
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
|
||||
from fastapi import APIRouter, Depends, Form, HTTPException, UploadFile
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from deps import LEGACY_ADMIN_EMAILS as ADMIN_EMAILS
|
||||
from deps import PERMISSIONS, require_perm
|
||||
from models import Role, User, get_db
|
||||
from security import hash_password
|
||||
|
||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||
UPLOAD_DIR = Path("/var/www/stepstead.com/assets/uploads")
|
||||
MAX_SIZE = 30 * 1024 * 1024 # 30 MB
|
||||
ALLOWED_EXT = {".png", ".jpg", ".jpeg", ".webp", ".svg", ".json", ".zip"}
|
||||
|
||||
|
||||
def _clean_name(s: str) -> str:
|
||||
"""Dosya adı temizliği: Türkçe dahil Unicode harf/rakam korunur (NFC'ye
|
||||
normalize edilir — iOS NFD gönderir), sadece tehlikeli karakterler atılır."""
|
||||
s = unicodedata.normalize("NFC", s)
|
||||
s = re.sub(r"[/\\\x00-\x1f]", "", s)
|
||||
return s.strip().strip(".")[:80]
|
||||
|
||||
|
||||
def _url(name: str) -> str:
|
||||
return "https://stepstead.com/assets/uploads/" + quote(name)
|
||||
|
||||
|
||||
# Geriye dönük: eski kod yolu; artık rol tabanlı require_perm kullanılıyor.
|
||||
def _require_admin(user: User) -> None:
|
||||
if user.email not in ADMIN_EMAILS:
|
||||
raise HTTPException(403, "Yetkin yok")
|
||||
|
||||
|
||||
class UploadOut(BaseModel):
|
||||
filename: str
|
||||
url: str
|
||||
size: int
|
||||
|
||||
|
||||
@router.post("/upload", response_model=UploadOut)
|
||||
async def upload_asset(
|
||||
file: UploadFile,
|
||||
target_name: str | None = Form(None),
|
||||
user: User = Depends(require_perm("upload")),
|
||||
):
|
||||
orig = Path(file.filename or "dosya").name
|
||||
ext = Path(orig).suffix.lower()
|
||||
if ext not in ALLOWED_EXT:
|
||||
raise HTTPException(400, f"İzinli uzantılar: {', '.join(sorted(ALLOWED_EXT))}")
|
||||
if target_name:
|
||||
# Asset listesinden yükleme: "up_<asset adı>.<ext>" — up_ öneki
|
||||
# "buradan geldi, boyut/uygunluk kontrolü bekliyor" demek.
|
||||
clean = _clean_name(target_name)
|
||||
if not clean:
|
||||
raise HTTPException(400, "Geçersiz hedef ad")
|
||||
name = f"up_{clean}{ext}"
|
||||
else:
|
||||
stem = _clean_name(Path(orig).stem) or "dosya"
|
||||
name = f"{stem}{ext}"
|
||||
|
||||
data = await file.read()
|
||||
if len(data) > MAX_SIZE:
|
||||
raise HTTPException(400, "Dosya çok büyük (max 30 MB)")
|
||||
if not data:
|
||||
raise HTTPException(400, "Boş dosya")
|
||||
|
||||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
(UPLOAD_DIR / name).write_bytes(data)
|
||||
return UploadOut(filename=name, url=_url(name), size=len(data))
|
||||
|
||||
|
||||
class FileRow(BaseModel):
|
||||
filename: str
|
||||
url: str
|
||||
size: int
|
||||
|
||||
|
||||
@router.get("/uploads", response_model=list[FileRow])
|
||||
async def list_uploads(user: User = Depends(require_perm("upload"))):
|
||||
out: list[FileRow] = []
|
||||
if UPLOAD_DIR.exists():
|
||||
for p in sorted(UPLOAD_DIR.iterdir()):
|
||||
if p.is_file():
|
||||
out.append(FileRow(filename=p.name, url=_url(p.name),
|
||||
size=p.stat().st_size))
|
||||
return out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Kullanıcı & rol yönetimi — sadece "users" izni (admin)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class RoleOut(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
permissions: list[str]
|
||||
is_system: bool
|
||||
user_count: int = 0
|
||||
|
||||
|
||||
class RoleIn(BaseModel):
|
||||
name: str = Field(min_length=2, max_length=40)
|
||||
permissions: list[str] = []
|
||||
|
||||
|
||||
class AdminUserOut(BaseModel):
|
||||
id: int
|
||||
email: str
|
||||
display_name: str | None
|
||||
role_id: int | None
|
||||
role_name: str | None
|
||||
is_active: bool
|
||||
created_at: str
|
||||
|
||||
|
||||
class UserCreateIn(BaseModel):
|
||||
email: EmailStr
|
||||
password: str = Field(min_length=8, max_length=128)
|
||||
display_name: str | None = Field(default=None, max_length=64)
|
||||
role_id: int | None = None
|
||||
|
||||
|
||||
class UserUpdateIn(BaseModel):
|
||||
role_id: int | None = None
|
||||
is_active: bool | None = None
|
||||
|
||||
|
||||
def _valid_perms(perms: list[str]) -> list[str]:
|
||||
bad = [p for p in perms if p not in PERMISSIONS and p != "*"]
|
||||
if bad:
|
||||
raise HTTPException(400, f"Bilinmeyen izin: {', '.join(bad)}")
|
||||
return sorted(set(perms))
|
||||
|
||||
|
||||
@router.get("/permissions")
|
||||
async def list_permissions(user: User = Depends(require_perm("users"))):
|
||||
return PERMISSIONS
|
||||
|
||||
|
||||
@router.get("/roles", response_model=list[RoleOut])
|
||||
async def list_roles(user: User = Depends(require_perm("users")),
|
||||
db: AsyncSession = Depends(get_db)):
|
||||
counts = dict((await db.execute(
|
||||
select(User.role_id, func.count()).where(User.role_id.isnot(None)).group_by(User.role_id)
|
||||
)).all())
|
||||
roles = (await db.execute(select(Role).order_by(Role.id))).scalars().all()
|
||||
return [RoleOut(id=r.id, name=r.name, permissions=list(r.permissions or []),
|
||||
is_system=r.is_system, user_count=counts.get(r.id, 0)) for r in roles]
|
||||
|
||||
|
||||
@router.post("/roles", response_model=RoleOut)
|
||||
async def create_role(data: RoleIn, user: User = Depends(require_perm("users")),
|
||||
db: AsyncSession = Depends(get_db)):
|
||||
exists = (await db.execute(select(Role).where(Role.name == data.name))).scalar_one_or_none()
|
||||
if exists:
|
||||
raise HTTPException(409, "Bu isimde rol zaten var")
|
||||
role = Role(name=data.name, permissions=_valid_perms(data.permissions))
|
||||
db.add(role)
|
||||
await db.commit()
|
||||
await db.refresh(role)
|
||||
return RoleOut(id=role.id, name=role.name, permissions=list(role.permissions),
|
||||
is_system=role.is_system)
|
||||
|
||||
|
||||
@router.post("/roles/{role_id}", response_model=RoleOut)
|
||||
async def update_role(role_id: int, data: RoleIn,
|
||||
user: User = Depends(require_perm("users")),
|
||||
db: AsyncSession = Depends(get_db)):
|
||||
role = (await db.execute(select(Role).where(Role.id == role_id))).scalar_one_or_none()
|
||||
if not role:
|
||||
raise HTTPException(404, "Rol yok")
|
||||
if role.is_system:
|
||||
raise HTTPException(400, "Sistem rolü (admin) değiştirilemez")
|
||||
role.name = data.name
|
||||
role.permissions = _valid_perms(data.permissions)
|
||||
await db.commit()
|
||||
return RoleOut(id=role.id, name=role.name, permissions=list(role.permissions),
|
||||
is_system=role.is_system)
|
||||
|
||||
|
||||
@router.post("/roles/{role_id}/delete")
|
||||
async def delete_role(role_id: int, user: User = Depends(require_perm("users")),
|
||||
db: AsyncSession = Depends(get_db)):
|
||||
role = (await db.execute(select(Role).where(Role.id == role_id))).scalar_one_or_none()
|
||||
if not role:
|
||||
raise HTTPException(404, "Rol yok")
|
||||
if role.is_system:
|
||||
raise HTTPException(400, "Sistem rolü silinemez")
|
||||
in_use = (await db.execute(select(func.count()).where(User.role_id == role_id))).scalar()
|
||||
if in_use:
|
||||
raise HTTPException(400, f"Rol {in_use} kullanıcıda atanmış — önce onları değiştir")
|
||||
await db.delete(role)
|
||||
await db.commit()
|
||||
return {"deleted": role_id}
|
||||
|
||||
|
||||
def _user_out(u: User, role_name: str | None) -> AdminUserOut:
|
||||
return AdminUserOut(id=u.id, email=u.email, display_name=u.display_name,
|
||||
role_id=u.role_id, role_name=role_name,
|
||||
is_active=u.is_active, created_at=u.created_at.isoformat())
|
||||
|
||||
|
||||
@router.get("/users", response_model=list[AdminUserOut])
|
||||
async def list_users(user: User = Depends(require_perm("users")),
|
||||
db: AsyncSession = Depends(get_db)):
|
||||
rows = (await db.execute(
|
||||
select(User, Role.name).outerjoin(Role, Role.id == User.role_id).order_by(User.id)
|
||||
)).all()
|
||||
return [_user_out(u, rn) for u, rn in rows]
|
||||
|
||||
|
||||
@router.post("/users", response_model=AdminUserOut)
|
||||
async def create_user(data: UserCreateIn, user: User = Depends(require_perm("users")),
|
||||
db: AsyncSession = Depends(get_db)):
|
||||
email = str(data.email).lower()
|
||||
exists = (await db.execute(select(User).where(User.email == email))).scalar_one_or_none()
|
||||
if exists:
|
||||
raise HTTPException(409, "Bu e-posta zaten kayıtlı")
|
||||
role_name = None
|
||||
if data.role_id is not None:
|
||||
role = (await db.execute(select(Role).where(Role.id == data.role_id))).scalar_one_or_none()
|
||||
if not role:
|
||||
raise HTTPException(400, "Rol yok")
|
||||
role_name = role.name
|
||||
u = User(email=email, password_hash=hash_password(data.password),
|
||||
display_name=data.display_name, email_verified=True, role_id=data.role_id)
|
||||
db.add(u)
|
||||
await db.commit()
|
||||
await db.refresh(u)
|
||||
return _user_out(u, role_name)
|
||||
|
||||
|
||||
@router.post("/users/{user_id}", response_model=AdminUserOut)
|
||||
async def update_user(user_id: int, data: UserUpdateIn,
|
||||
user: User = Depends(require_perm("users")),
|
||||
db: AsyncSession = Depends(get_db)):
|
||||
u = (await db.execute(select(User).where(User.id == user_id))).scalar_one_or_none()
|
||||
if not u:
|
||||
raise HTTPException(404, "Kullanıcı yok")
|
||||
if "role_id" in data.model_fields_set: # role_id:null gönderilirse rol kaldırılır
|
||||
if data.role_id is not None:
|
||||
role = (await db.execute(select(Role).where(Role.id == data.role_id))).scalar_one_or_none()
|
||||
if not role:
|
||||
raise HTTPException(400, "Rol yok")
|
||||
u.role_id = data.role_id
|
||||
if data.is_active is not None:
|
||||
if u.id == user.id and not data.is_active:
|
||||
raise HTTPException(400, "Kendini pasifleştiremezsin")
|
||||
u.is_active = data.is_active
|
||||
await db.commit()
|
||||
role_name = None
|
||||
if u.role_id:
|
||||
role_name = (await db.execute(select(Role.name).where(Role.id == u.role_id))).scalar_one_or_none()
|
||||
return _user_out(u, role_name)
|
||||
Loading…
Add table
Add a link
Reference in a new issue