267 lines
9.7 KiB
Python
267 lines
9.7 KiB
Python
"""
|
||
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)
|