feat: add auth system, boss pokemon details, moves/abilities API, and run ownership
Some checks failed
CI / backend-tests (push) Failing after 1m16s
CI / frontend-tests (push) Successful in 57s

Add user authentication with login/signup/protected routes, boss pokemon
detail fields and result team tracking, moves and abilities selector
components and API, run ownership and visibility controls, and various
UI improvements across encounters, run list, and journal pages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-20 21:41:38 +01:00
parent a6cb309b8b
commit 0a519e356e
69 changed files with 3574 additions and 693 deletions

View File

@@ -2,6 +2,7 @@ from app.models.ability import Ability
from app.models.boss_battle import BossBattle
from app.models.boss_pokemon import BossPokemon
from app.models.boss_result import BossResult
from app.models.boss_result_team import BossResultTeam
from app.models.encounter import Encounter
from app.models.evolution import Evolution
from app.models.game import Game
@@ -13,6 +14,7 @@ from app.models.nuzlocke_run import NuzlockeRun
from app.models.pokemon import Pokemon
from app.models.route import Route
from app.models.route_encounter import RouteEncounter
from app.models.user import User
from app.models.version_group import VersionGroup
__all__ = [
@@ -20,6 +22,7 @@ __all__ = [
"BossBattle",
"BossPokemon",
"BossResult",
"BossResultTeam",
"Encounter",
"Evolution",
"Game",
@@ -32,5 +35,6 @@ __all__ = [
"Pokemon",
"Route",
"RouteEncounter",
"User",
"VersionGroup",
]

View File

@@ -1,8 +1,18 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey, SmallInteger, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
if TYPE_CHECKING:
from app.models.ability import Ability
from app.models.boss_battle import BossBattle
from app.models.move import Move
from app.models.pokemon import Pokemon
class BossPokemon(Base):
__tablename__ = "boss_pokemon"
@@ -16,8 +26,24 @@ class BossPokemon(Base):
order: Mapped[int] = mapped_column(SmallInteger)
condition_label: Mapped[str | None] = mapped_column(String(100))
# Detail fields
ability_id: Mapped[int | None] = mapped_column(
ForeignKey("abilities.id"), index=True
)
held_item: Mapped[str | None] = mapped_column(String(50))
nature: Mapped[str | None] = mapped_column(String(20))
move1_id: Mapped[int | None] = mapped_column(ForeignKey("moves.id"), index=True)
move2_id: Mapped[int | None] = mapped_column(ForeignKey("moves.id"), index=True)
move3_id: Mapped[int | None] = mapped_column(ForeignKey("moves.id"), index=True)
move4_id: Mapped[int | None] = mapped_column(ForeignKey("moves.id"), index=True)
boss_battle: Mapped[BossBattle] = relationship(back_populates="pokemon")
pokemon: Mapped[Pokemon] = relationship()
ability: Mapped[Ability | None] = relationship()
move1: Mapped[Move | None] = relationship(foreign_keys=[move1_id])
move2: Mapped[Move | None] = relationship(foreign_keys=[move2_id])
move3: Mapped[Move | None] = relationship(foreign_keys=[move3_id])
move4: Mapped[Move | None] = relationship(foreign_keys=[move4_id])
def __repr__(self) -> str:
return f"<BossPokemon(id={self.id}, boss_battle_id={self.boss_battle_id}, pokemon_id={self.pokemon_id})>"

View File

@@ -25,6 +25,12 @@ class BossResult(Base):
run: Mapped[NuzlockeRun] = relationship(back_populates="boss_results")
boss_battle: Mapped[BossBattle] = relationship()
team: Mapped[list[BossResultTeam]] = relationship(
back_populates="boss_result", cascade="all, delete-orphan"
)
def __repr__(self) -> str:
return f"<BossResult(id={self.id}, run_id={self.run_id}, boss_battle_id={self.boss_battle_id}, result='{self.result}')>"
return (
f"<BossResult(id={self.id}, run_id={self.run_id}, "
f"boss_battle_id={self.boss_battle_id}, result='{self.result}')>"
)

View File

@@ -0,0 +1,26 @@
from sqlalchemy import ForeignKey, SmallInteger
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class BossResultTeam(Base):
__tablename__ = "boss_result_team"
id: Mapped[int] = mapped_column(primary_key=True)
boss_result_id: Mapped[int] = mapped_column(
ForeignKey("boss_results.id", ondelete="CASCADE"), index=True
)
encounter_id: Mapped[int] = mapped_column(
ForeignKey("encounters.id", ondelete="CASCADE"), index=True
)
level: Mapped[int] = mapped_column(SmallInteger)
boss_result: Mapped[BossResult] = relationship(back_populates="team")
encounter: Mapped[Encounter] = relationship()
def __repr__(self) -> str:
return (
f"<BossResultTeam(id={self.id}, boss_result_id={self.boss_result_id}, "
f"encounter_id={self.encounter_id}, level={self.level})>"
)

View File

@@ -1,21 +1,46 @@
from datetime import datetime
from __future__ import annotations
from sqlalchemy import DateTime, ForeignKey, String, func
from datetime import datetime
from enum import StrEnum
from typing import TYPE_CHECKING
from uuid import UUID
from sqlalchemy import DateTime, Enum, ForeignKey, String, func
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
if TYPE_CHECKING:
from app.models.boss_result import BossResult
from app.models.encounter import Encounter
from app.models.game import Game
from app.models.journal_entry import JournalEntry
from app.models.user import User
class RunVisibility(StrEnum):
PUBLIC = "public"
PRIVATE = "private"
class NuzlockeRun(Base):
__tablename__ = "nuzlocke_runs"
id: Mapped[int] = mapped_column(primary_key=True)
game_id: Mapped[int] = mapped_column(ForeignKey("games.id"), index=True)
owner_id: Mapped[UUID | None] = mapped_column(
ForeignKey("users.id", ondelete="SET NULL"), index=True
)
name: Mapped[str] = mapped_column(String(100))
status: Mapped[str] = mapped_column(
String(20), index=True
) # active, completed, failed
visibility: Mapped[RunVisibility] = mapped_column(
Enum(RunVisibility, name="run_visibility", create_constraint=False),
default=RunVisibility.PUBLIC,
server_default="public",
)
rules: Mapped[dict] = mapped_column(JSONB, default=dict)
started_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
@@ -25,6 +50,7 @@ class NuzlockeRun(Base):
naming_scheme: Mapped[str | None] = mapped_column(String(50), nullable=True)
game: Mapped[Game] = relationship(back_populates="runs")
owner: Mapped[User | None] = relationship(back_populates="runs")
encounters: Mapped[list[Encounter]] = relationship(back_populates="run")
boss_results: Mapped[list[BossResult]] = relationship(back_populates="run")
journal_entries: Mapped[list[JournalEntry]] = relationship(back_populates="run")

View File

@@ -0,0 +1,29 @@
from __future__ import annotations
from datetime import datetime
from typing import TYPE_CHECKING
from uuid import UUID
from sqlalchemy import DateTime, String, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
if TYPE_CHECKING:
from app.models.nuzlocke_run import NuzlockeRun
class User(Base):
__tablename__ = "users"
id: Mapped[UUID] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
display_name: Mapped[str | None] = mapped_column(String(100))
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
runs: Mapped[list[NuzlockeRun]] = relationship(back_populates="owner")
def __repr__(self) -> str:
return f"<User(id={self.id}, email='{self.email}')>"