Public teams endpoint — GET /public/teams #176

Closed
opened 2026-03-27 03:12:39 +00:00 by forgejo_admin · 2 comments

Type

Feature

Lineage

Enables dynamic teams page on westside-app public site.

Repo

forgejo_admin/basketball-api

User Story

As a visitor on the public website, I want to see team rosters so that I can learn about the program and its players without needing to log in.

story:WS-S26

Context

All existing team/roster endpoints require auth (require_admin or require_role). The public site needs an unauthenticated endpoint that returns only publicly-safe player data.

The westside-playground teams.html @svelte-notes define the data contract:

  • Sort: 17U→16U→15U, then Elite→Select→Local within each age group
  • Only return teams with at least one assigned player
  • Render all players regardless of profile completeness — missing fields omit gracefully

Security — Field Allowlist

CRITICAL: Do NOT reuse existing admin response schemas. Create dedicated public schemas.

Public response MUST include ONLY:

class PublicPlayerResponse(BaseModel):
    name: str
    jersey_number: str | None
    position: str | None
    height: str | None
    current_school: str | None

class PublicTeamResponse(BaseModel):
    id: int
    name: str
    coach_name: str | None
    players: list[PublicPlayerResponse]

class PublicTeamsResponse(BaseModel):
    teams: list[PublicTeamResponse]

MUST NOT expose: parent_id, parent_name, parent_email, parent_phone, date_of_birth, payment_status, stripe_customer_id, stripe_subscription_id, contract_status, contract_token, monthly_fee, jersey_size, jersey_option, address, hometown, target_schools, photo_url, tryout_number.

File Targets

  • New file: src/basketball_api/routes/public.py — public endpoints (no auth)
  • Modify: src/basketball_api/main.py — register public router
  • New schemas in the route file (not in shared models)

Acceptance Criteria

  • GET /public/teams returns teams with nested players, no auth required
  • Response schema is PublicTeamsResponse — allowlisted fields only
  • Teams sorted 17U→16U→15U, Elite→Select→Local
  • Teams with zero assigned players are excluded
  • No sensitive data in response (parent info, payment, contracts, Stripe)
  • Endpoint accessible without Bearer token

Test Expectations

  • Test: unauthenticated request returns 200
  • Test: response contains only allowlisted fields
  • Test: team with no players is excluded
  • Test: sort order correct

Constraints

  • No auth dependency — no Depends(require_role) or Depends(require_admin)
  • Hardcode tenant_id=1 for now (single-tenant)
  • Coach name comes from joining coaches table via team.coach_id

Checklist

  • Public schemas created (not reusing admin schemas)
  • Router registered in main.py
  • Tests pass
  • No sensitive data leaks verified
  • Lucas review
  • westside-playground teams.html @svelte-notes — data contract
  • convention-sveltekit-spa — frontend consuming this endpoint
  • westside-app #96 — Svelte promotion prep
### Type Feature ### Lineage Enables dynamic teams page on westside-app public site. ### Repo `forgejo_admin/basketball-api` ### User Story As a visitor on the public website, I want to see team rosters so that I can learn about the program and its players without needing to log in. story:WS-S26 ### Context All existing team/roster endpoints require auth (`require_admin` or `require_role`). The public site needs an unauthenticated endpoint that returns only publicly-safe player data. The westside-playground teams.html `@svelte-notes` define the data contract: - Sort: 17U→16U→15U, then Elite→Select→Local within each age group - Only return teams with at least one assigned player - Render all players regardless of profile completeness — missing fields omit gracefully ### Security — Field Allowlist **CRITICAL: Do NOT reuse existing admin response schemas.** Create dedicated public schemas. Public response MUST include ONLY: ```python class PublicPlayerResponse(BaseModel): name: str jersey_number: str | None position: str | None height: str | None current_school: str | None class PublicTeamResponse(BaseModel): id: int name: str coach_name: str | None players: list[PublicPlayerResponse] class PublicTeamsResponse(BaseModel): teams: list[PublicTeamResponse] ``` **MUST NOT expose:** parent_id, parent_name, parent_email, parent_phone, date_of_birth, payment_status, stripe_customer_id, stripe_subscription_id, contract_status, contract_token, monthly_fee, jersey_size, jersey_option, address, hometown, target_schools, photo_url, tryout_number. ### File Targets - New file: `src/basketball_api/routes/public.py` — public endpoints (no auth) - Modify: `src/basketball_api/main.py` — register public router - New schemas in the route file (not in shared models) ### Acceptance Criteria - [ ] `GET /public/teams` returns teams with nested players, no auth required - [ ] Response schema is `PublicTeamsResponse` — allowlisted fields only - [ ] Teams sorted 17U→16U→15U, Elite→Select→Local - [ ] Teams with zero assigned players are excluded - [ ] No sensitive data in response (parent info, payment, contracts, Stripe) - [ ] Endpoint accessible without Bearer token ### Test Expectations - [ ] Test: unauthenticated request returns 200 - [ ] Test: response contains only allowlisted fields - [ ] Test: team with no players is excluded - [ ] Test: sort order correct ### Constraints - No auth dependency — no `Depends(require_role)` or `Depends(require_admin)` - Hardcode tenant_id=1 for now (single-tenant) - Coach name comes from joining coaches table via team.coach_id ### Checklist - [ ] Public schemas created (not reusing admin schemas) - [ ] Router registered in main.py - [ ] Tests pass - [ ] No sensitive data leaks verified - [ ] Lucas review ### Related - westside-playground teams.html `@svelte-notes` — data contract - `convention-sveltekit-spa` — frontend consuming this endpoint - westside-app #96 — Svelte promotion prep
Author
Owner

Scope Addition

Add is_public boolean column to players table (default false). The GET /public/teams endpoint should only return players where is_public = true. This gives coaches/admin control over which player profiles appear on the public website.

Migration needed: ALTER TABLE players ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT false;

Admin will need a way to toggle this — but that's a separate ticket. For now, the column + filter is enough.

## Scope Addition Add `is_public` boolean column to `players` table (default `false`). The `GET /public/teams` endpoint should only return players where `is_public = true`. This gives coaches/admin control over which player profiles appear on the public website. Migration needed: `ALTER TABLE players ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT false;` Admin will need a way to toggle this — but that's a separate ticket. For now, the column + filter is enough.
Author
Owner

Scope Review: READY

Review note: review-429-2026-03-26
Scope is solid — all 12 template sections present, traceability triangle complete, all file targets verified against codebase, security allowlist is thorough with explicit deny list. Acceptance criteria are fully testable by an agent. No blockers found; sibling ticket #177 (public coaches) shares public.py but no conflict. Downstream consumer (westside-app SvelteKit) correctly declares depends:bb-176+bb-177.

## Scope Review: READY Review note: `review-429-2026-03-26` Scope is solid — all 12 template sections present, traceability triangle complete, all file targets verified against codebase, security allowlist is thorough with explicit deny list. Acceptance criteria are fully testable by an agent. No blockers found; sibling ticket #177 (public coaches) shares `public.py` but no conflict. Downstream consumer (westside-app SvelteKit) correctly declares `depends:bb-176+bb-177`.
forgejo_admin 2026-03-27 03:29:30 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
forgejo_admin/basketball-api#176
No description provided.