""" Lore notları — "lore" izni (admin + loremaster). Hikaye için küçük notlar; sonra derlenir. Hepsi admin-only (GET dahil — özel içerik). WAF nedeniyle sadece GET/POST. GET /lore → notlar (yeniden eskiye) POST /lore → yeni not POST /lore/{id} → güncelle POST /lore/{id}/delete → sil """ from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession from deps import require_perm, get_user_perms, has_perm from models import User, get_db router = APIRouter(prefix="/lore", tags=["lore"]) async def _assert_owner_or_admin(db: AsyncSession, user: User, table: str, row_id: int): """Admin (perm '*') her şeyi düzenler/siler; diğerleri sadece kendi kaydını.""" perms = await get_user_perms(user, db) if has_perm(perms, "*"): return owner = (await db.execute(text( f"SELECT author_id FROM {table} WHERE id=:id" ), {"id": row_id})).scalar_one_or_none() if owner is None: raise HTTPException(404, "Kayıt yok") if owner != user.id: raise HTTPException(403, "Sadece kendi kaydını düzenleyebilir/silebilirsin") class NoteIn(BaseModel): body: str = Field(min_length=1, max_length=20000) body_en: str | None = Field(default=None, max_length=20000) tag: str | None = Field(default=None, max_length=60) subject_kind: str | None = Field(default=None, max_length=16) # 'npc' | 'place' | None subject_id: int | None = None subject_label: str | None = Field(default=None, max_length=120) class NoteOut(BaseModel): id: int body: str body_en: str | None tag: str | None subject_kind: str | None subject_id: int | None subject_label: str | None author: str | None author_id: int | None created_at: str updated_at: str _NOTE_COLS = ("id,body,body_en,tag,subject_kind,subject_id,subject_label," "author,author_id,created_at,updated_at") def _author(user: User) -> str: return (user.display_name or "").strip() or user.email def _row(r) -> NoteOut: d = dict(r) d["created_at"] = d["created_at"].isoformat() d["updated_at"] = d["updated_at"].isoformat() return NoteOut(**d) @router.get("", response_model=list[NoteOut]) async def list_notes(user: User = Depends(require_perm("lore")), db: AsyncSession = Depends(get_db)): rows = (await db.execute(text( f"SELECT {_NOTE_COLS} FROM lore_notes ORDER BY id DESC" ))).mappings().all() return [_row(r) for r in rows] @router.get("/subjects") async def list_subjects(user: User = Depends(require_perm("lore")), db: AsyncSession = Depends(get_db)): """Not bağlanabilecek konular: kişiler (npcs) + yerler (map_tags).""" npcs = (await db.execute(text( "SELECT id, name, role FROM npcs ORDER BY name" ))).mappings().all() places = (await db.execute(text( "SELECT id, name, category FROM map_tags ORDER BY name" ))).mappings().all() return { "npc": [{"id": r["id"], "label": r["name"], "sub": r["role"] or ""} for r in npcs], "place": [{"id": r["id"], "label": r["name"], "sub": r["category"] or ""} for r in places], } @router.post("", response_model=NoteOut) async def add_note(n: NoteIn, user: User = Depends(require_perm("lore")), db: AsyncSession = Depends(get_db)): row = (await db.execute(text( "INSERT INTO lore_notes(body,body_en,tag,subject_kind,subject_id,subject_label,author,author_id) " "VALUES(:body,:body_en,:tag,:sk,:sid,:slabel,:author,:aid) " f"RETURNING {_NOTE_COLS}" ), {"body": n.body, "body_en": n.body_en, "tag": n.tag, "sk": n.subject_kind, "sid": n.subject_id, "slabel": n.subject_label, "author": _author(user), "aid": user.id})).mappings().one() await db.commit() return _row(row) # ---- Temel Hikaye — uzun metin bölümleri (lore_story) ---- # ÖNEMLİ: /lore/story yolları /lore/{note_id}'den ÖNCE tanımlı olmalı (route sırası). class StoryIn(BaseModel): title: str | None = Field(default=None, max_length=120) body: str = Field(min_length=1, max_length=200000) body_en: str | None = Field(default=None, max_length=200000) class StoryOut(BaseModel): id: int title: str | None body: str body_en: str | None author: str | None author_id: int | None created_at: str updated_at: str def _srow(r) -> StoryOut: d = dict(r) d["created_at"] = d["created_at"].isoformat() d["updated_at"] = d["updated_at"].isoformat() return StoryOut(**d) @router.get("/story", response_model=list[StoryOut]) async def list_story(user: User = Depends(require_perm("lore")), db: AsyncSession = Depends(get_db)): rows = (await db.execute(text( "SELECT id,title,body,body_en,author,author_id,created_at,updated_at FROM lore_story ORDER BY sort_order,id" ))).mappings().all() return [_srow(r) for r in rows] @router.post("/story", response_model=StoryOut) async def add_story(s: StoryIn, user: User = Depends(require_perm("lore")), db: AsyncSession = Depends(get_db)): row = (await db.execute(text( "INSERT INTO lore_story(title,body,body_en,author,author_id) VALUES(:title,:body,:body_en,:author,:aid) " "RETURNING id,title,body,body_en,author,author_id,created_at,updated_at" ), {"title": s.title, "body": s.body, "body_en": s.body_en, "author": _author(user), "aid": user.id})).mappings().one() await db.commit() return _srow(row) @router.post("/story/{story_id}", response_model=StoryOut) async def update_story(story_id: int, s: StoryIn, user: User = Depends(require_perm("lore")), db: AsyncSession = Depends(get_db)): await _assert_owner_or_admin(db, user, "lore_story", story_id) row = (await db.execute(text( "UPDATE lore_story SET title=:title,body=:body,body_en=:body_en,updated_at=now() WHERE id=:id " "RETURNING id,title,body,body_en,author,author_id,created_at,updated_at" ), {"id": story_id, "title": s.title, "body": s.body, "body_en": s.body_en})).mappings().one_or_none() if not row: raise HTTPException(404, "Bölüm yok") await db.commit() return _srow(row) @router.post("/story/{story_id}/delete") async def delete_story(story_id: int, user: User = Depends(require_perm("lore")), db: AsyncSession = Depends(get_db)): await _assert_owner_or_admin(db, user, "lore_story", story_id) res = await db.execute(text("DELETE FROM lore_story WHERE id=:id"), {"id": story_id}) await db.commit() if res.rowcount == 0: raise HTTPException(404, "Bölüm yok") return {"deleted": story_id} @router.post("/{note_id}", response_model=NoteOut) async def update_note(note_id: int, n: NoteIn, user: User = Depends(require_perm("lore")), db: AsyncSession = Depends(get_db)): await _assert_owner_or_admin(db, user, "lore_notes", note_id) row = (await db.execute(text( "UPDATE lore_notes SET body=:body,body_en=:body_en,tag=:tag," "subject_kind=:sk,subject_id=:sid,subject_label=:slabel,updated_at=now() WHERE id=:id " f"RETURNING {_NOTE_COLS}" ), {"id": note_id, "body": n.body, "body_en": n.body_en, "tag": n.tag, "sk": n.subject_kind, "sid": n.subject_id, "slabel": n.subject_label})).mappings().one_or_none() if not row: raise HTTPException(404, "Not yok") await db.commit() return _row(row) @router.post("/{note_id}/delete") async def delete_note(note_id: int, user: User = Depends(require_perm("lore")), db: AsyncSession = Depends(get_db)): await _assert_owner_or_admin(db, user, "lore_notes", note_id) res = await db.execute(text("DELETE FROM lore_notes WHERE id=:id"), {"id": note_id}) await db.commit() if res.rowcount == 0: raise HTTPException(404, "Not yok") return {"deleted": note_id}