Files
pokedexter/backend/app/routers/admin.py
T

378 lines
16 KiB
Python
Raw Normal View History

import csv
import io
import os
import uuid
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile, File, Form, status
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
2026-07-01 21:10:06 +02:00
from fastapi.templating import Jinja2Templates
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
2026-07-01 21:10:06 +02:00
from pydantic import BaseModel
from ..database import get_db
from ..models import User, Serie, Set, Card
from ..auth import require_admin, hash_password
2026-07-01 21:10:06 +02:00
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
2026-07-01 21:10:06 +02:00
UPLOAD_DIR = Path("app/static/images")
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
2026-07-01 21:10:06 +02:00
# ─── Pydantic schemas ───────────────────────────────────────────────
2026-07-01 21:10:06 +02:00
class SerieCreate(BaseModel):
name: str
description: str | None = None
class SetCreate(BaseModel):
2026-07-01 21:10:06 +02:00
code: str
name: str
release_date: str | None = None
description: str | None = None
class CardCreate(BaseModel):
2026-07-01 21:10:06 +02:00
number: str
name: str
image: str | None = None # URL
class NewUserForm(BaseModel):
username: str
password: str
role: str = "user"
2026-07-01 21:10:06 +02:00
# ─── Admin HTML pages ───────────────────────────────────────────────
2026-07-01 21:10:06 +02:00
@router.get("/admin", response_class=HTMLResponse)
async def admin_dashboard(request: Request, user: User = Depends(require_admin)):
return templates.TemplateResponse("admin/dashboard.html", {"request": request})
@router.get("/admin/series", response_class=HTMLResponse)
async def admin_series(request: Request, db: AsyncSession = Depends(get_db),
user: User = Depends(require_admin)):
result = await db.execute(select(Serie).options(selectinload(Serie.sets)).order_by(Serie.name))
series = result.scalars().all()
return templates.TemplateResponse("admin/series.html", {"request": request, "series": series})
2026-07-01 21:10:06 +02:00
@router.get("/admin/sets", response_class=HTMLResponse)
async def admin_sets_page(request: Request, db: AsyncSession = Depends(get_db),
user: User = Depends(require_admin)):
result = await db.execute(
select(Set).options(selectinload(Set.serie), selectinload(Set.cards)).order_by(Set.code)
)
sets = result.scalars().all()
series_result = await db.execute(select(Serie).order_by(Serie.name))
series = series_result.scalars().all()
return templates.TemplateResponse("admin/sets.html", {"request": request, "sets": sets, "series": series})
2026-07-01 21:10:06 +02:00
@router.get("/admin/cards/{set_code}", response_class=HTMLResponse)
async def admin_cards_page(request: Request, set_code: str, db: AsyncSession = Depends(get_db),
user: User = Depends(require_admin)):
result = await db.execute(
select(Set).where(Set.code == set_code.upper()).options(
selectinload(Set.cards), selectinload(Set.serie)
)
)
s = result.scalar_one_or_none()
if not s:
return templates.TemplateResponse("404.html", {"request": request}, status_code=404)
return templates.TemplateResponse("admin/cards.html", {"request": request, "set": s})
# ─── Users (admin) ─────────────────────────────────────────────────
2026-07-01 21:10:06 +02:00
@router.post("/api/admin/users")
async def create_user(data: NewUserForm, db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
exists = await db.execute(select(User).where(User.username == data.username))
if exists.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Bruger findes allerede")
user = User(username=data.username, password_hash=hash_password(data.password), role=data.role)
db.add(user)
await db.commit()
return {"ok": True, "username": user.username, "role": user.role}
@router.get("/api/admin/users")
async def list_users(db: AsyncSession = Depends(get_db), _: User = Depends(require_admin)):
result = await db.execute(select(User).order_by(User.username))
return [{"id": u.id, "username": u.username, "role": u.role, "created_at": str(u.created_at)}
for u in result.scalars().all()]
# ─── API: Series ────────────────────────────────────────────────────
@router.post("/api/admin/series")
async def create_serie(data: SerieCreate, db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
result = await db.execute(select(Serie).where(Serie.name == data.name))
if result.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Serie findes allerede")
serie = Serie(name=data.name, description=data.description)
db.add(serie)
await db.commit()
return {"ok": True, "id": serie.id, "name": serie.name}
@router.get("/api/admin/series")
async def list_series(db: AsyncSession = Depends(get_db), _: User = Depends(require_admin)):
result = await db.execute(select(Serie).options(selectinload(Serie.sets)).order_by(Serie.name))
return [{"id": s.id, "name": s.name, "description": s.description or "",
"set_count": len(s.sets)} for s in result.scalars().all()]
@router.delete("/api/admin/series/{serie_id}")
async def delete_serie(serie_id: int, db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
result = await db.execute(select(Serie).where(Serie.id == serie_id))
s = result.scalar_one_or_none()
if not s:
raise HTTPException(status_code=404, detail="Serie ikke fundet")
await db.delete(s)
await db.commit()
return {"ok": True}
@router.post("/api/admin/series/import")
async def import_series(file: UploadFile = File(...), db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
content = await file.read()
text = content.decode("utf-8-sig")
reader = csv.DictReader(io.StringIO(text))
created = 0
errors = []
for i, row in enumerate(reader, start=2):
name = row.get("name", "").strip()
if not name:
errors.append(f"Linje {i}: mangler navn")
continue
try:
exists = await db.execute(select(Serie).where(Serie.name == name))
if exists.scalar_one_or_none():
errors.append(f"Linje {i}: '{name}' findes allerede")
continue
serie = Serie(name=name, description=row.get("description", "").strip() or None)
db.add(serie)
created += 1
except Exception as e:
errors.append(f"Linje {i}: {str(e)}")
await db.commit()
return {"ok": True, "created": created, "errors": errors}
2026-07-01 21:10:06 +02:00
# ─── API: Sets ──────────────────────────────────────────────────────
@router.post("/api/admin/series/{serie_name}/sets")
async def create_set(serie_name: str, data: SetCreate, db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
result = await db.execute(select(Serie).where(Serie.name == serie_name))
serie = result.scalar_one_or_none()
if not serie:
raise HTTPException(status_code=404, detail="Serie ikke fundet")
2026-07-01 21:10:06 +02:00
exists = await db.execute(select(Set).where(Set.code == data.code.upper()))
if exists.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Set-kode findes allerede")
s = Set(serie_id=serie.id, code=data.code.upper(), name=data.name,
release_date=data.release_date, description=data.description)
2026-07-01 21:10:06 +02:00
db.add(s)
await db.commit()
return {"ok": True, "id": s.id, "code": s.code, "name": s.name}
2026-07-01 21:10:06 +02:00
@router.get("/api/admin/series/{serie_name}/sets")
async def list_sets(serie_name: str, db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
result = await db.execute(
select(Serie).where(Serie.name == serie_name).options(selectinload(Serie.sets))
)
serie = result.scalar_one_or_none()
if not serie:
raise HTTPException(status_code=404, detail="Serie ikke fundet")
return [{"id": s.id, "code": s.code, "name": s.name, "card_count": len(s.cards)}
for s in serie.sets]
2026-07-01 21:10:06 +02:00
@router.get("/api/admin/sets")
async def list_all_sets(db: AsyncSession = Depends(get_db), _: User = Depends(require_admin)):
result = await db.execute(
select(Set).options(selectinload(Set.serie), selectinload(Set.cards)).order_by(Set.code)
)
2026-07-01 21:10:06 +02:00
sets = result.scalars().all()
return [{"id": s.id, "code": s.code, "name": s.name, "serie": s.serie.name,
"card_count": len(s.cards), "release_date": s.release_date or ""}
for s in sets]
2026-07-01 21:10:06 +02:00
@router.delete("/api/admin/sets/{set_id}")
async def delete_set(set_id: int, db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
result = await db.execute(select(Set).where(Set.id == set_id))
s = result.scalar_one_or_none()
2026-07-01 21:10:06 +02:00
if not s:
raise HTTPException(status_code=404, detail="Set ikke fundet")
await db.delete(s)
await db.commit()
return {"ok": True}
@router.post("/api/admin/sets/import")
async def import_sets(file: UploadFile = File(...), db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
content = await file.read()
text = content.decode("utf-8-sig")
reader = csv.DictReader(io.StringIO(text))
created = 0
errors = []
for i, row in enumerate(reader, start=2):
code = row.get("code", "").strip().upper()
name = row.get("name", "").strip()
serie_name = row.get("serie", "").strip()
if not code or not name or not serie_name:
errors.append(f"Linje {i}: mangler kode, navn eller serie")
continue
try:
serie = await db.execute(select(Serie).where(Serie.name == serie_name))
serie = serie.scalar_one_or_none()
if not serie:
errors.append(f"Linje {i}: serie '{serie_name}' ikke fundet")
continue
exists = await db.execute(select(Set).where(Set.code == code))
if exists.scalar_one_or_none():
errors.append(f"Linje {i}: kode '{code}' findes allerede")
continue
s = Set(serie_id=serie.id, code=code, name=name,
release_date=row.get("release_date", "").strip() or None,
description=row.get("description", "").strip() or None)
db.add(s)
created += 1
except Exception as e:
errors.append(f"Linje {i}: {str(e)}")
await db.commit()
return {"ok": True, "created": created, "errors": errors}
2026-07-01 21:10:06 +02:00
# ─── API: Cards ─────────────────────────────────────────────────────
2026-07-01 21:10:06 +02:00
@router.post("/api/admin/sets/{set_code}/cards")
async def create_card(set_code: str, data: CardCreate, db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
2026-07-01 21:10:06 +02:00
result = await db.execute(select(Set).where(Set.code == set_code.upper()))
s = result.scalar_one_or_none()
if not s:
raise HTTPException(status_code=404, detail="Set ikke fundet")
card = Card(set_id=s.id, number=data.number, name=data.name)
if data.image:
card.image_path = data.image
2026-07-01 21:10:06 +02:00
db.add(card)
await db.commit()
return {"ok": True, "id": card.id, "full_code": card.full_code, "name": card.name}
2026-07-01 21:10:06 +02:00
@router.get("/api/admin/sets/{set_code}/cards")
async def list_cards(set_code: str, db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
result = await db.execute(
select(Set).where(Set.code == set_code.upper()).options(
selectinload(Set.cards), selectinload(Set.serie)
)
)
2026-07-01 21:10:06 +02:00
s = result.scalar_one_or_none()
if not s:
raise HTTPException(status_code=404, detail="Set ikke fundet")
return [{"id": c.id, "number": c.number, "name": c.name, "full_code": c.full_code,
"image_url": c.image_url} for c in s.cards]
2026-07-01 21:10:06 +02:00
@router.delete("/api/admin/cards/{card_id}")
async def delete_card(card_id: int, db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
result = await db.execute(select(Card).where(Card.id == card_id))
card = result.scalar_one_or_none()
2026-07-01 21:10:06 +02:00
if not card:
raise HTTPException(status_code=404, detail="Kort ikke fundet")
# Slet billedfil hvis lokal
if card.image_path and not card.image_path.startswith(("http://", "https://")):
fp = Path("app/static") / card.image_path
if fp.exists():
fp.unlink()
2026-07-01 21:10:06 +02:00
await db.delete(card)
await db.commit()
return {"ok": True}
@router.post("/api/admin/cards/import")
async def import_cards(file: UploadFile = File(...), db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
content = await file.read()
text = content.decode("utf-8-sig")
reader = csv.DictReader(io.StringIO(text))
created = 0
errors = []
for i, row in enumerate(reader, start=2):
set_code = row.get("set_code", "").strip().upper()
number = row.get("number", "").strip()
name = row.get("name", "").strip()
if not set_code or not number or not name:
errors.append(f"Linje {i}: mangler set_code, number eller name")
continue
try:
result = await db.execute(select(Set).where(Set.code == set_code))
s = result.scalar_one_or_none()
if not s:
errors.append(f"Linje {i}: set '{set_code}' ikke fundet")
continue
image = row.get("image", "").strip() or None
card = Card(set_id=s.id, number=number, name=name, image_path=image)
db.add(card)
created += 1
except Exception as e:
errors.append(f"Linje {i}: {str(e)}")
await db.commit()
return {"ok": True, "created": created, "errors": errors}
@router.post("/api/admin/cards/{card_id}/upload")
async def upload_card_image(card_id: int, file: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
result = await db.execute(
select(Card).where(Card.id == card_id).options(
selectinload(Card.set).selectinload(Set.serie)
)
)
card = result.scalar_one_or_none()
if not card:
raise HTTPException(status_code=404, detail="Kort ikke fundet")
# Bestem filtype
ext = os.path.splitext(file.filename or "image.png")[1].lower()
if ext not in (".jpg", ".jpeg", ".png", ".gif", ".webp"):
raise HTTPException(status_code=400, detail="Kun .jpg, .png, .gif, .webp tilladt")
serie_name = card.set.serie.name.replace(" ", "_").replace("/", "_")
set_code = card.set.code
number = card.number
folder = UPLOAD_DIR / serie_name / set_code
folder.mkdir(parents=True, exist_ok=True)
filename = f"{number}{ext}"
filepath = folder / filename
content = await file.read()
filepath.write_bytes(content)
# Gem relativ sti
rel_path = f"images/{serie_name}/{set_code}/{filename}"
card.image_path = rel_path
await db.commit()
return {"ok": True, "image_url": f"/static/{rel_path}", "path": rel_path}
# ─── CSV Templates ──────────────────────────────────────────────────
@router.get("/api/admin/templates/series.csv", response_class=PlainTextResponse)
async def series_csv_template(_: User = Depends(require_admin)):
return "name,description\nSword & Shield,The Sword & Shield series\n"
@router.get("/api/admin/templates/sets.csv", response_class=PlainTextResponse)
async def sets_csv_template(_: User = Depends(require_admin)):
return "serie,code,name,release_date,description\nSword & Shield,EVS,Evolving Skies,2021-08-27,\n"
@router.get("/api/admin/templates/cards.csv", response_class=PlainTextResponse)
async def cards_csv_template(_: User = Depends(require_admin)):
return "set_code,number,name,image\nEVS,050,Raichu,\n"