Skip to content

API Reference

Joël Deffner edited this page Jul 17, 2026 · 2 revisions

API Reference

The complete JSON contract between the React SPA and the CodeIgniter backend. Everything lives under /api/*. The authoritative, always-current version is docs/API_FLIGHTMEET.md; TypeScript mirrors of these shapes live in frontend/src/lib/types.ts.

Conventions

  • Success: 2xx with the documented body.
  • Validation failure: 422 { "errors": { field: "message" } }.
  • Not authenticated: 401 { "error": "..." }; not allowed: 403; missing resource: 404; conflict (meet full, duplicate join): 409.
  • Mutating requests need the X-CSRF-TOKEN header (handled by frontend/src/lib/api.ts).
  • Dates are YYYY-MM-DD, times HH:MM, timestamps ISO-8601.
  • Participant = { id, username, name } (name = "Vorname Nachname" or the username as fallback).

Authentication

Method Path Auth Purpose
GET /api/auth/me public session status + CSRF token
POST /api/auth/register guest create account, log in, return { user, csrf } (201)
POST /api/auth/login public { login, password, remember }{ user, csrf }
POST /api/auth/logout session end the session

Registration body: { username, email, password, vorname?, nachname? }. Creates a Shield account in group user (every account is a pilot by default). Validation mirrors the admin create-user rules (unique username/email, Shield password rules). Login works with email or username.

Meets

Method Path Auth Purpose
GET /api/meets public all meets, ordered by date ascending; search/filter happen client-side
GET /api/meets/{id} public detail including participants
POST /api/meets session create; geocodes the spot if no coordinates given
PUT /api/meets/{id} session edit (creator or admin only)
POST /api/meets/{id}/join session join; 409 when full or already joined
DELETE /api/meets/{id}/join session leave; 409 when not joined
GET /api/meets/{id}/weather public forecast for the meet's coordinates

GET /api/meets{ "data": MeetSummary[] }:

// MeetSummary
{
  "id": 1, "title": "...", "spot": "...", "region": "...",
  "date": "2026-07-19", "time": "17:30", "description": "...",
  "level": "Advanced",              // Beginner | Intermediate | Advanced | All levels
  "maxParticipants": 10,
  "participantCount": 3,            // derived from meet_participants, never stored
  "status": "open",                 // "full" when participantCount == maxParticipants
  "joined": false                   // always false for guests
}

GET /api/meets/{id}{ "meet": MeetDetail } where MeetDetail extends MeetSummary with latitude, longitude (nullable), participants: Participant[], and createdBy: Participant.

Create (POST /api/meets) body: { title, spot, region, date, time, level, maxParticipants, description, latitude?, longitude? }. When coordinates are omitted, the backend geocodes spot (fallback: region) via Open-Meteo; an unresolvable spot leaves them null (weather then shows a friendly "no forecast" note, never a hard error). The date must not be in the past. Returns 201 { "meet": MeetDetail }.

Edit (PUT /api/meets/{id}): allowed for the creator or an admin, otherwise 403 { "error": "Only the organizer can edit this meet." }. Same body and validation as create, plus:

  • maxParticipants must be >= the current participant count, else 422.
  • The date must not be in the past only when it changed (an unchanged past date may be re-saved).
  • Coordinates: if both are provided they are used as-is; if both are empty, stored coordinates are kept when spot and region are unchanged, otherwise re-geocoded like create.

Join/leave return 200 { "meet": MeetDetail } (fresh state, so list and detail views stay consistent).

GET /api/meets/{id}/weather returns the same payload as /api/weather but resolved from the meet's stored coordinates; 409 { "error": "This meet has no coordinates." } when they are null.

Groups

Method Path Auth Purpose
GET /api/groups public all groups
GET /api/groups/{id} public detail including members
POST /api/groups session create (creator auto-joins)
PUT /api/groups/{id} session edit (founder or admin only)
POST /api/groups/{id}/join session join
DELETE /api/groups/{id}/join session leave
// GroupSummary
{
  "id": 1, "name": "Black Forest Soarers", "region": "Black Forest",
  "description": "...", "image": null,   // optional card image URL/path
  "memberCount": 3, "joined": false
}

GET /api/groups/{id}{ "group": GroupDetail } = GroupSummary + members: Participant[] + createdBy: Participant.

POST /api/groups body { name, region, description }201 { "group": GroupDetail }. Join/leave return 200 { "group": GroupDetail }; duplicate join or leaving a group you're not in → 409. PUT edits name/region/description (founder or admin, else 403); the unique-name check ignores the group itself.

Chat

One global "All pilots" channel plus one channel per group (messages.group_id NULL = global). All chat endpoints require a session; group channels additionally require membership (403 otherwise).

Method Path Purpose
GET /api/chat/messages?groupId={id}&after={messageId} list messages
POST /api/chat/messages send
DELETE /api/chat/messages/{id} delete (own message or admin)

groupId absent → global channel. after (optional) returns only messages with id > after; the SPA polls with it every 3 seconds and appends. Response: { "data": ChatMessage[] }, oldest → newest, capped at the latest 100 when after is absent.

// ChatMessage
{
  "id": 7, "body": "...", "createdAt": "2026-07-17T09:12:00+02:00",
  "mine": false,
  "author": { "id": 4, "username": "pilot1", "name": "Lena Vogel" }
}

POST body { body, groupId? }201 { "message": ChatMessage }; body is required, trimmed, max 2000 chars. DELETE returns 200 { "ok": true, "id": <id> }; deleting someone else's message → 403, unknown id → 404.

Pilot profiles

GET /api/users/{username} (public) → 200 { "user": PilotProfile }; unknown username → 404. Exposes only public data (no email or address).

// PilotProfile
{
  "id": 7, "username": "lena",
  "name": "Lena K.", "vorname": "Lena", "nachname": "K.",   // nullable name parts
  "tier": "pilot",              // subscription_tier: pilot | club | school
  "role": "user",               // highest Shield group: admin > moderator > user
  "memberSince": "2026-07-01T09:00:00+02:00",
  "groups": [ { "id": 1, "name": "...", "region": "...", "memberCount": 5 } ],
  "meets": [                    // created OR joined, deduped, date DESC
    { "id": 3, "title": "...", "spot": "...", "region": "...",
      "date": "2026-07-20", "time": "18:00",
      "participantCount": 4, "maxParticipants": 10,
      "organizer": true }
  ]
}

Own profile

Method Path Purpose
GET /api/profile the logged-in user's own data
PUT /api/profile update own data (no group/active/tier changes)
PUT /api/profile/subscription { "tier": "pilot" | "club" | "school" }; switching is free (no payment flow yet)

Activity feed

GET /api/activity (public) → 200 { "data": ActivityEvent[] }: a union of the latest meet creations, meet joins, and global chat messages, sorted by createdAt descending, capped at 10.

{ "type": "meet_created", "createdAt": ISO, "user": {...}, "meet": { "id": 1, "title": "..." } }
{ "type": "meet_joined",  "createdAt": ISO, "user": {...}, "meet": { "id": 1, "title": "..." } }
{ "type": "message",      "createdAt": ISO, "user": {...}, "excerpt": "first 90 chars of body" }

Weather

See Weather Integration for the full behavior (caching, staleness, purging).

Method Path Auth Purpose
GET /api/weather?city=... public current + 7-day forecast; caches resolved city searches
GET /api/weather/reports public every cached forecast report
POST /api/weather/reports/{id}/refresh public re-fetch a cached report

Admin

All admin routes require the adminapi filter (admin.access); each action additionally checks its concrete permission.

Method Path Permission Purpose
GET /api/admin/users users.view list with paging/search/filter/sort
POST /api/admin/users users.create create
GET /api/admin/users/{id} users.view single user
PUT /api/admin/users/{id} users.edit update
DELETE /api/admin/users/{id} users.delete delete (soft-delete)

List query parameters: page, perPage (max 100), search, group, active (1/0), sort, dir. Search covers username, first/last name, city, postal code, and email. Admins cannot delete, deactivate, or de-admin their own account.

Clone this wiki locally