İlk commit: Stepstead backend (FastAPI)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jts 2026-07-11 11:30:24 +03:00
commit ea049b8ccd
45 changed files with 6751 additions and 0 deletions

203
routers/lore.py Normal file
View file

@ -0,0 +1,203 @@
"""
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}