""" 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/ 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_." — 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)