Full app rebuild: FastAPI backend + React Native mobile with auth, championships, admin

Backend (FastAPI + SQLAlchemy + SQLite):
- JWT auth with access/refresh tokens, bcrypt password hashing
- User model with member/organizer/admin roles, auto-approve members
- Championship, Registration, ParticipantList, Notification models
- Alembic async migrations, seed data with test users
- Registration endpoint returns tokens for members, pending for organizers
- /registrations/my returns championship title/date/location via eager loading
- Admin endpoints: list users, approve/reject organizers

Mobile (React Native + Expo + TypeScript):
- Zustand auth store, Axios client with token refresh interceptor
- Role-based registration (Member vs Organizer) with contextual form labels
- Tab navigation with Ionicons, safe area headers, admin tab for admin role
- Championships list with status badges, detail screen with registration progress
- My Registrations with championship title, progress bar, and tap-to-navigate
- Admin panel with pending/all filter, approve/reject with confirmation
- Profile screen with role badge, Ionicons info rows, sign out
- Password visibility toggle (Ionicons), keyboard flow hints (returnKeyType)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dianaka123
2026-02-25 22:46:50 +03:00
parent 9eb68695e9
commit 789d2bf0a6
81 changed files with 16283 additions and 310 deletions

View File

@@ -0,0 +1,3 @@
from app.crud import crud_user, crud_championship, crud_registration, crud_participant
__all__ = ["crud_user", "crud_championship", "crud_registration", "crud_participant"]

View File

@@ -0,0 +1,63 @@
import json
import uuid
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.championship import Championship
from app.schemas.championship import ChampionshipCreate, ChampionshipUpdate
def _serialize(value) -> str | None:
if value is None:
return None
return json.dumps(value)
async def get(db: AsyncSession, champ_id: str | uuid.UUID) -> Championship | None:
cid = champ_id if isinstance(champ_id, uuid.UUID) else uuid.UUID(str(champ_id))
result = await db.execute(select(Championship).where(Championship.id == cid))
return result.scalar_one_or_none()
async def list_all(
db: AsyncSession,
status: str | None = None,
skip: int = 0,
limit: int = 50,
) -> list[Championship]:
q = select(Championship).order_by(Championship.event_date.asc())
if status:
q = q.where(Championship.status == status)
q = q.offset(skip).limit(limit)
result = await db.execute(q)
return list(result.scalars().all())
async def create(db: AsyncSession, data: ChampionshipCreate) -> Championship:
payload = data.model_dump(exclude={"judges", "categories"})
payload["judges"] = _serialize(data.judges)
payload["categories"] = _serialize(data.categories)
champ = Championship(**payload)
db.add(champ)
await db.commit()
await db.refresh(champ)
return champ
async def update(db: AsyncSession, champ: Championship, data: ChampionshipUpdate) -> Championship:
raw = data.model_dump(exclude_none=True, exclude={"judges", "categories"})
for field, value in raw.items():
setattr(champ, field, value)
if data.judges is not None:
champ.judges = _serialize(data.judges)
if data.categories is not None:
champ.categories = _serialize(data.categories)
await db.commit()
await db.refresh(champ)
return champ
async def delete(db: AsyncSession, champ: Championship) -> None:
await db.delete(champ)
await db.commit()

View File

@@ -0,0 +1,34 @@
import uuid
from datetime import UTC, datetime
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.participant import ParticipantList
async def get_for_championship(db: AsyncSession, championship_id: str | uuid.UUID) -> ParticipantList | None:
cid = championship_id if isinstance(championship_id, uuid.UUID) else uuid.UUID(str(championship_id))
result = await db.execute(select(ParticipantList).where(ParticipantList.championship_id == cid))
return result.scalar_one_or_none()
async def create_or_get(db: AsyncSession, championship_id: uuid.UUID, published_by: uuid.UUID) -> ParticipantList:
existing = await get_for_championship(db, championship_id)
if existing:
return existing
pl = ParticipantList(championship_id=championship_id, published_by=published_by)
db.add(pl)
await db.commit()
await db.refresh(pl)
return pl
async def publish(db: AsyncSession, pl: ParticipantList, notes: str | None = None) -> ParticipantList:
pl.is_published = True
pl.published_at = datetime.now(UTC)
if notes is not None:
pl.notes = notes
await db.commit()
await db.refresh(pl)
return pl

View File

@@ -0,0 +1,86 @@
import uuid
from datetime import UTC, datetime
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.registration import Registration
from app.schemas.registration import RegistrationCreate, RegistrationUpdate
async def get(db: AsyncSession, reg_id: str | uuid.UUID) -> Registration | None:
rid = reg_id if isinstance(reg_id, uuid.UUID) else uuid.UUID(str(reg_id))
result = await db.execute(
select(Registration).where(Registration.id == rid).options(selectinload(Registration.user))
)
return result.scalar_one_or_none()
async def get_by_user_and_championship(
db: AsyncSession, user_id: uuid.UUID, championship_id: uuid.UUID
) -> Registration | None:
result = await db.execute(
select(Registration).where(
Registration.user_id == user_id,
Registration.championship_id == championship_id,
)
)
return result.scalar_one_or_none()
async def list_for_championship(
db: AsyncSession, championship_id: str | uuid.UUID, skip: int = 0, limit: int = 100
) -> list[Registration]:
cid = championship_id if isinstance(championship_id, uuid.UUID) else uuid.UUID(str(championship_id))
result = await db.execute(
select(Registration)
.where(Registration.championship_id == cid)
.options(selectinload(Registration.user))
.offset(skip)
.limit(limit)
)
return list(result.scalars().all())
async def list_for_user(db: AsyncSession, user_id: uuid.UUID, skip: int = 0, limit: int = 50) -> list[Registration]:
result = await db.execute(
select(Registration)
.where(Registration.user_id == user_id)
.options(selectinload(Registration.championship))
.order_by(Registration.submitted_at.desc())
.offset(skip)
.limit(limit)
)
return list(result.scalars().all())
async def create(db: AsyncSession, user_id: uuid.UUID, data: RegistrationCreate) -> Registration:
reg = Registration(
championship_id=data.championship_id,
user_id=user_id,
category=data.category,
level=data.level,
notes=data.notes,
status="submitted",
)
db.add(reg)
await db.commit()
await db.refresh(reg)
return reg
async def update(db: AsyncSession, reg: Registration, data: RegistrationUpdate) -> Registration:
raw = data.model_dump(exclude_none=True)
for field, value in raw.items():
setattr(reg, field, value)
if "status" in raw and raw["status"] in ("accepted", "rejected", "waitlisted"):
reg.decided_at = datetime.now(UTC)
await db.commit()
await db.refresh(reg)
return reg
async def delete(db: AsyncSession, reg: Registration) -> None:
await db.delete(reg)
await db.commit()

View File

@@ -0,0 +1,57 @@
import uuid
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.user import User
from app.schemas.user import UserRegister, UserUpdate
from app.services.auth_service import hash_password
async def get_by_id(db: AsyncSession, user_id: str | uuid.UUID) -> User | None:
uid = user_id if isinstance(user_id, uuid.UUID) else uuid.UUID(str(user_id))
result = await db.execute(select(User).where(User.id == uid))
return result.scalar_one_or_none()
async def get_by_email(db: AsyncSession, email: str) -> User | None:
result = await db.execute(select(User).where(User.email == email.lower()))
return result.scalar_one_or_none()
async def create(db: AsyncSession, data: UserRegister) -> User:
user = User(
email=data.email.lower(),
hashed_password=hash_password(data.password),
full_name=data.full_name,
phone=data.phone,
role=data.requested_role,
organization_name=data.organization_name,
instagram_handle=data.instagram_handle,
# Members are auto-approved; organizers require admin review
status="approved" if data.requested_role == "member" else "pending",
)
db.add(user)
await db.commit()
await db.refresh(user)
return user
async def update(db: AsyncSession, user: User, data: UserUpdate) -> User:
for field, value in data.model_dump(exclude_none=True).items():
setattr(user, field, value)
await db.commit()
await db.refresh(user)
return user
async def set_status(db: AsyncSession, user: User, status: str) -> User:
user.status = status
await db.commit()
await db.refresh(user)
return user
async def list_all(db: AsyncSession, skip: int = 0, limit: int = 100) -> list[User]:
result = await db.execute(select(User).offset(skip).limit(limit))
return list(result.scalars().all())