stepstead-backend/routers/lore.py
jts ea049b8ccd İlk commit: Stepstead backend (FastAPI)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 11:30:24 +03:00

203 lines
7.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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}