feat: add auth system, boss pokemon details, moves/abilities API, and run ownership
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:
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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})>"
|
||||
|
||||
@@ -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}')>"
|
||||
)
|
||||
|
||||
26
backend/src/app/models/boss_result_team.py
Normal file
26
backend/src/app/models/boss_result_team.py
Normal 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})>"
|
||||
)
|
||||
@@ -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")
|
||||
|
||||
29
backend/src/app/models/user.py
Normal file
29
backend/src/app/models/user.py
Normal 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}')>"
|
||||
Reference in New Issue
Block a user