-
Notifications
You must be signed in to change notification settings - Fork 0
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.
- Success:
2xxwith 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-TOKENheader (handled byfrontend/src/lib/api.ts). - Dates are
YYYY-MM-DD, timesHH:MM, timestamps ISO-8601. -
Participant={ id, username, name }(name= "Vorname Nachname" or the username as fallback).
| 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.
| 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[] }:
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:
-
maxParticipantsmust be >= the current participant count, else422. - 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
spotandregionare 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.
| 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.
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.
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 }
]
}| 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) |
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" }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 |
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.
FlightMeet
Code
Reference