Initial: Pokedexter TCG-Collector webapp
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import Set, Card, User
|
||||
from ..auth import require_admin
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
async def require_admin_html(request: Request) -> User:
|
||||
"""Admin check from cookie-based auth (HTML pages)."""
|
||||
user = request.state.user
|
||||
if not user or user.role != "admin":
|
||||
raise HTTPException(status_code=401, detail="Admin login required")
|
||||
return user
|
||||
templates = Jinja2Templates(directory="app/templates")
|
||||
|
||||
|
||||
class SetForm(BaseModel):
|
||||
code: str
|
||||
name: str
|
||||
release_date: str | None = None
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class CardForm(BaseModel):
|
||||
number: str
|
||||
name: str
|
||||
type: str | None = None
|
||||
rarity: str | None = None
|
||||
hp: int | None = None
|
||||
stage: str | None = None
|
||||
description: str | None = None
|
||||
image_url: str | None = None
|
||||
|
||||
|
||||
# --- Admin pages ---
|
||||
|
||||
@router.get("/admin", response_class=HTMLResponse)
|
||||
async def admin_dashboard(request: Request, user: User = Depends(require_admin_html)):
|
||||
return templates.TemplateResponse("admin/dashboard.html", {"request": request, "user": user})
|
||||
|
||||
|
||||
@router.get("/admin/sets", response_class=HTMLResponse)
|
||||
async def admin_sets(request: Request, user: User = Depends(require_admin_html)):
|
||||
return templates.TemplateResponse("admin/sets.html", {"request": request, "user": user})
|
||||
|
||||
|
||||
@router.get("/admin/cards/{set_code}", response_class=HTMLResponse)
|
||||
async def admin_cards(request: Request, set_code: str, user: User = Depends(require_admin_html)):
|
||||
return templates.TemplateResponse("admin/cards.html", {"request": request, "user": user, "set_code": set_code})
|
||||
|
||||
|
||||
# --- CRUD: Sets ---
|
||||
|
||||
@router.post("/api/admin/sets")
|
||||
async def create_set(
|
||||
data: SetForm,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(require_admin),
|
||||
):
|
||||
exists = await db.execute(select(Set).where(Set.code == data.code.upper()))
|
||||
if exists.scalar_one_or_none():
|
||||
raise HTTPException(status_code=400, detail=f"Set '{data.code.upper()}' findes allerede")
|
||||
|
||||
s = Set(code=data.code.upper(), name=data.name, release_date=data.release_date, description=data.description)
|
||||
db.add(s)
|
||||
await db.commit()
|
||||
await db.refresh(s)
|
||||
return {"ok": True, "id": s.id, "code": s.code}
|
||||
|
||||
|
||||
@router.get("/api/admin/sets")
|
||||
async def list_sets_admin(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(require_admin),
|
||||
):
|
||||
result = await db.execute(select(Set).order_by(Set.code))
|
||||
sets = result.scalars().all()
|
||||
return [{"id": s.id, "code": s.code, "name": s.name, "release_date": s.release_date or ""} for s in sets]
|
||||
|
||||
|
||||
@router.delete("/api/admin/sets/{set_id}")
|
||||
async def delete_set(
|
||||
set_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(require_admin),
|
||||
):
|
||||
s = await db.get(Set, set_id)
|
||||
if not s:
|
||||
raise HTTPException(status_code=404, detail="Set ikke fundet")
|
||||
await db.delete(s)
|
||||
await db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# --- CRUD: Cards ---
|
||||
|
||||
@router.post("/api/admin/sets/{set_code}/cards")
|
||||
async def create_card(
|
||||
set_code: str,
|
||||
data: CardForm,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(require_admin),
|
||||
):
|
||||
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=f"Set '{set_code.upper()}' ikke fundet")
|
||||
|
||||
card = Card(
|
||||
set_id=s.id, number=data.number, name=data.name,
|
||||
type=data.type, rarity=data.rarity, hp=data.hp,
|
||||
stage=data.stage, description=data.description, image_url=data.image_url,
|
||||
)
|
||||
db.add(card)
|
||||
await db.commit()
|
||||
await db.refresh(card)
|
||||
return {"ok": True, "id": card.id, "full_code": card.full_code}
|
||||
|
||||
|
||||
@router.get("/api/admin/sets/{set_code}/cards")
|
||||
async def list_cards_admin(
|
||||
set_code: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(require_admin),
|
||||
):
|
||||
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")
|
||||
return [
|
||||
{"id": c.id, "number": c.number, "name": c.name, "type": c.type or "",
|
||||
"rarity": c.rarity or "", "hp": c.hp, "full_code": c.full_code}
|
||||
for c in s.cards
|
||||
]
|
||||
|
||||
|
||||
@router.delete("/api/admin/cards/{card_id}")
|
||||
async def delete_card(
|
||||
card_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(require_admin),
|
||||
):
|
||||
card = await db.get(Card, card_id)
|
||||
if not card:
|
||||
raise HTTPException(status_code=404, detail="Kort ikke fundet")
|
||||
await db.delete(card)
|
||||
await db.commit()
|
||||
return {"ok": True}
|
||||
@@ -0,0 +1,87 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import User
|
||||
from ..auth import verify_password, create_token, hash_password, require_admin
|
||||
|
||||
router = APIRouter()
|
||||
templates = Jinja2Templates(directory="app/templates")
|
||||
|
||||
|
||||
class LoginForm(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class CreateUserForm(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
role: str = "user"
|
||||
|
||||
|
||||
@router.get("/login", response_class=HTMLResponse)
|
||||
async def login_page(request: Request):
|
||||
return templates.TemplateResponse("login.html", {"request": request})
|
||||
|
||||
|
||||
@router.post("/api/login")
|
||||
async def login(data: LoginForm, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(User).where(User.username == data.username))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user or not verify_password(data.password, user.password_hash):
|
||||
raise HTTPException(status_code=401, detail="Forkert brugernavn eller adgangskode")
|
||||
|
||||
token = create_token(user.username, user.role)
|
||||
resp = RedirectResponse(url="/", status_code=302)
|
||||
resp.set_cookie(
|
||||
key="token", value=token, httponly=True, max_age=30 * 24 * 3600,
|
||||
samesite="lax"
|
||||
)
|
||||
return resp
|
||||
|
||||
|
||||
@router.post("/api/logout")
|
||||
async def logout():
|
||||
resp = RedirectResponse(url="/login", status_code=302)
|
||||
resp.delete_cookie("token")
|
||||
return resp
|
||||
|
||||
|
||||
# --- Admin-only: user management ---
|
||||
|
||||
@router.post("/api/admin/users")
|
||||
async def create_user(
|
||||
data: CreateUserForm,
|
||||
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))
|
||||
users = result.scalars().all()
|
||||
return [
|
||||
{"id": u.id, "username": u.username, "role": u.role, "created_at": str(u.created_at)}
|
||||
for u in users
|
||||
]
|
||||
@@ -0,0 +1,113 @@
|
||||
from fastapi import APIRouter, Depends, Request, Query
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import Set, Card
|
||||
|
||||
router = APIRouter()
|
||||
templates = Jinja2Templates(directory="app/templates")
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(Set).order_by(Set.code))
|
||||
sets = result.scalars().all()
|
||||
return templates.TemplateResponse("index.html", {"request": request, "sets": sets})
|
||||
|
||||
|
||||
@router.get("/sets", response_class=HTMLResponse)
|
||||
async def list_sets(request: Request, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(Set).order_by(Set.code))
|
||||
sets = result.scalars().all()
|
||||
return templates.TemplateResponse("sets.html", {"request": request, "sets": sets})
|
||||
|
||||
|
||||
@router.get("/sets/{set_code}", response_class=HTMLResponse)
|
||||
async def set_detail(request: Request, set_code: str, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(
|
||||
select(Set).where(Set.code == set_code.upper()).options(selectinload(Set.cards))
|
||||
)
|
||||
s = result.scalar_one_or_none()
|
||||
if not s:
|
||||
return templates.TemplateResponse("404.html", {"request": request}, status_code=404)
|
||||
return templates.TemplateResponse("set_detail.html", {"request": request, "set": s})
|
||||
|
||||
|
||||
@router.get("/search", response_class=HTMLResponse)
|
||||
async def search_page(request: Request):
|
||||
return templates.TemplateResponse("search.html", {"request": request})
|
||||
|
||||
|
||||
@router.get("/api/search")
|
||||
async def search_cards(
|
||||
q: str = Query(""),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Search cards by full code (e.g. CRI-068) or by name."""
|
||||
q = q.strip()
|
||||
|
||||
# Try full-code search: CODE-NUMBER
|
||||
if "-" in q:
|
||||
parts = q.split("-", 1)
|
||||
code_part = parts[0].strip().upper()
|
||||
num_part = parts[1].strip().lstrip("0") # remove leading zeros
|
||||
stmt = (
|
||||
select(Card)
|
||||
.join(Card.set)
|
||||
.where(Set.code == code_part, Card.number == num_part)
|
||||
.options(selectinload(Card.set))
|
||||
)
|
||||
else:
|
||||
# Search by name
|
||||
stmt = (
|
||||
select(Card)
|
||||
.join(Card.set)
|
||||
.where(Card.name.ilike(f"%{q}%"))
|
||||
.options(selectinload(Card.set))
|
||||
.limit(50)
|
||||
)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
cards = result.scalars().all()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": c.id,
|
||||
"full_code": c.full_code,
|
||||
"name": c.name,
|
||||
"type": c.type or "",
|
||||
"rarity": c.rarity or "",
|
||||
"set_name": c.set.name,
|
||||
}
|
||||
for c in cards
|
||||
]
|
||||
|
||||
|
||||
@router.get("/api/sets")
|
||||
async def api_list_sets(db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(Set).order_by(Set.code))
|
||||
sets = result.scalars().all()
|
||||
return [
|
||||
{"code": s.code, "name": s.name, "release_date": s.release_date or "",
|
||||
"card_count": len(s.cards)}
|
||||
for s in sets
|
||||
]
|
||||
|
||||
|
||||
@router.get("/api/sets/{set_code}/cards")
|
||||
async def api_set_cards(set_code: str, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(
|
||||
select(Set).where(Set.code == set_code.upper()).options(selectinload(Set.cards))
|
||||
)
|
||||
s = result.scalar_one_or_none()
|
||||
if not s:
|
||||
return []
|
||||
return [
|
||||
{"id": c.id, "number": c.number, "name": c.name, "type": c.type or "",
|
||||
"rarity": c.rarity or "", "hp": c.hp, "full_code": c.full_code}
|
||||
for c in s.cards
|
||||
]
|
||||
Reference in New Issue
Block a user