一键导入
api-endpoints
REST API endpoints, route structure, auth guards, request/response contracts, error patterns, and the full API surface for web SPA and mobile clients.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
REST API endpoints, route structure, auth guards, request/response contracts, error patterns, and the full API surface for web SPA and mobile clients.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Photo upload, tagging, verification status, summary generation, XP calculation, AddTagsToPhotoAction, UploadPhotoController, and the VerificationStatus enum.
OpenLitterMap v5 architecture reference. Use this skill whenever working on OLM backend code, Laravel controllers, services, events, tests, Redis, metrics, tags, photos, teams, admin, leaderboards, or any part of the OpenLitterMap codebase. Also trigger when the user mentions MetricsService, VerificationStatus, PhotoTags, AddTagsToPhotoAction, TagsVerifiedByAdmin, rewardXpToAdmin, school pipeline, teacher approval, clustering, or any OLM-specific concept. This skill should be used BEFORE writing any OLM code to avoid architectural mistakes. When in doubt about OLM patterns, read this skill.
ProfileController, ApiSettingsController, DeleteAccountController, user stats (Redis + MySQL fallback), LevelService, privacy toggles, account deletion with Redis cleanup, and the Profile Vue page.
PhotoTag, PhotoTagExtraTags, categories, litter objects, materials, brands, ClassifyTagsService, GeneratePhotoSummaryService, tag migration, and the v4-to-v5 conversion.
The olm:v5 migration script, UpdateTagsService, batch processing, migrated_at, ClassifyTagsService deprecated mappings, and data migration from v4 category tables.
LeaderboardController, Redis sorted sets for all-time XP rankings, per-user metrics rows for time-filtered rankings, rewardXpToAdmin, and leaderboard privacy.
| name | api-endpoints |
| description | REST API endpoints, route structure, auth guards, request/response contracts, error patterns, and the full API surface for web SPA and mobile clients. |
All API routes live in routes/api.php. The API serves two clients: the Vue 3 SPA (session auth) and a mobile app (Bearer token auth). Current version is v3. All legacy v1/v2 endpoints have been removed (2026-03-01).
routes/api.php — All route definitions (~146 active routes)readme/API.md — Comprehensive endpoint documentation with request/response contracts for every endpoint (2080+ lines). READ THIS FIRST before modifying any API endpoint.app/Http/Controllers/Auth/AuthTokenController.php — Mobile token login (Sanctum)app/Http/Controllers/Auth/LoginController.php — SPA session loginapp/Http/Controllers/Auth/RegisterController.php — User registration (both clients)app/Http/Controllers/Uploads/UploadPhotoController.php — v3 upload (web EXIF + mobile explicit coords)app/Http/Controllers/API/Tags/PhotoTagsController.php — v3 tag CRUD (POST add, PUT replace)app/Http/Controllers/API/Tags/GetTagsController.php — Public tag catalogapp/Http/Controllers/User/ProfileController.php — Profile (auth + public)app/Http/Controllers/User/Photos/UsersUploadsController.php — v3 user photos (paginated, filterable)app/Http/Controllers/Leaderboard/LeaderboardController.php — Leaderboard (time-filtered, location-scoped)app/Http/Controllers/Points/PointsController.php — Map points GeoJSONapp/Http/Controllers/Points/PointsStatsController.php — Map viewport statsapp/Http/Controllers/Clusters/ClusterController.php — Map clusters GeoJSON (ETag cached)app/Http/Controllers/Location/LocationController.php — Location hierarchy APIapp/Http/Controllers/Location/TagController.php — Location tag analyticsapp/Http/Controllers/API/TeamsController.php — Team CRUD + membershipapp/Http/Controllers/Teams/TeamPhotosController.php — Team photo management + school approvalapp/Http/Controllers/Teams/TeamsLeaderboardController.php — Teams leaderboardapp/Http/Controllers/Teams/TeamsClusterController.php — Team map clusters/pointsapp/Http/Controllers/API/DeleteAccountController.php — GDPR account deletionapp/Http/Controllers/UsersController.php — Settings, privacy toggles, detailsapp/Http/Controllers/ApiSettingsController.php — Privacy settings (maps/leaderboard/createdby)app/Http/Controllers/Achievements/AchievementsController.php — User achievementsapp/Http/Controllers/API/GlobalStatsController.php — World totals (total_tags, total_images, total_users, new_users_*)app/Http/Controllers/CommunityController.php — Community statsapp/Http/Controllers/Cleanups/ — 4 cleanup controllers (create, list, join, leave)app/Http/Requests/ — Form request validation classes (one per endpoint that needs validation)| Prefix | Middleware | Purpose |
|---|---|---|
/api/v3 | auth:sanctum | Current API (upload, tags, user photos) |
/api (public) | none | Tags catalog, points, stats, locations, clusters, leaderboard |
/api/auth | varies | Login, register, logout, password reset |
/api (auth) | auth:sanctum | Profile, settings, achievements |
/api/teams | auth:sanctum | Team CRUD, photos, approval, leaderboard |
/api/admin | admin | Admin queue, verify, reset |
/api/bbox | can_bbox | Bounding box annotation |
/api/participant | varies | School participant session endpoints |
auth:sanctum routes accept both SPA session cookies and mobile Bearer tokens. The SPA uses POST /api/auth/login (session), mobile uses POST /api/auth/token (Sanctum token).mobile. AuthTokenController creates tokens named mobile and revokes previous mobile tokens on each login (prevents buildup).name = NULL. Ignores any name field in the request. Auto-generates username if omitted (pattern: {adjective}-{noun}-{number}).readme/API.md is the source of truth. Every endpoint's exact request/response contract is documented there. Always consult it before modifying an endpoint.{ "msg": "..." }, others return { "message": "..." }. This is a known legacy issue — match the existing pattern for each controller. UploadPhotoRequest overrides failedValidation() to return { success, error, message, errors } with a typed error code field (no_exif, no_gps, no_datetime, invalid_coordinates, validation_error). Duplicates are no longer a validation error — the controller returns idempotent 200 success with the existing photo_id. Handler::unauthenticated() returns { message: "Unauthenticated." } without an error code (known inconsistency). PhotoTagsRequest uses Laravel's default failedValidation() (no custom error codes). Admin routes (/api/admin/*) require the admin middleware which already wraps auth:sanctum — do not add a redundant auth:sanctum wrapper.is_public = true. All map/points/global/community endpoints use Photo::public() scope or explicit where('is_public', true).locations/location_type keys. Not children/children_type. The {type} parameter accepts country, state, or city.total_tags, total_photos, total_members, created_at, updated_at. Never use old names like total_litter, total_images, tags, photos, contributors.page (not current_page). Frontend normalizes to current_page in pointsHelper.getPaginationData().user_id set to NULL via DB CASCADE). Redis leaderboards and per-user metrics are cleaned up.POST /api/auth/token → { token, user }
// Include on all subsequent requests:
Authorization: Bearer <token>
// Validate token is still valid:
POST /api/validate-token → { message: "valid" }
GET /sanctum/csrf-cookie → Sets XSRF-TOKEN cookie
POST /api/auth/login → Sets session cookie, returns { success, user }
GET /api/user/profile/index → Returns user + stats + level + rank
POST /api/auth/logout → Destroys session
POST /api/v3/upload → Upload photo (web: EXIF; mobile: explicit lat/lon/date)
Idempotent: a duplicate (user_id+datetime) returns 200
{ success, photo_id: <existing>, already_uploaded: true,
tagged: <bool>, xp_awarded: 0 } — NOT a 422.
On validation failure: { success: false, error: <code>, message, errors }
Error codes: no_exif | no_gps | no_datetime |
invalid_coordinates | validation_error
GET /api/v3/user/photos → List user's photos (paginated, filterable, per_page up to 100)
Response includes `is_public` (boolean) and `school_team` (boolean)
for each photo. Owner sees all photos including private ones.
GET /api/v3/user/photos/stats → Aggregate counts (totalPhotos, totalTags, leftToTag)
POST /api/v3/tags → Add tags to untagged photo (PhotoTagsRequest — default Laravel errors)
PUT /api/v3/tags → Replace all tags on tagged photo (edit mode, accepts empty tags: [])
PATCH /api/v3/photos/{id}/visibility → Toggle is_public for a single photo (owner only, auth:sanctum).
Returns { is_public: bool }. Blocked for school team photos (403).
PhotoObserver marks dirty tiles on change.
POST /api/profile/photos/delete → Delete single photo { "photoid": 123 } (soft delete)
GET /api/leaderboard?timeFilter=all-time&locationType=country&locationId=1&page=1
// timeFilter: all-time | today | yesterday | this-month | last-month | this-year | last-year
// locationType: country | state | city (optional)
// locationId: numeric ID (required if locationType set)
// All time filters use MySQL metrics table. Per page hardcoded to 100.
// Public endpoint (no auth required). Optional auth adds currentUserRank.
GET /api/teams/photos?team_id=X&status=pending → List photos (with new_tags CLO format)
GET /api/teams/photos/{photo}?team_id=X → Single photo (with new_tags)
GET /api/teams/photos/member-stats?team_id=X → Per-student stats (leader only, safeguarding)
GET /api/teams/photos/map?team_id=X → Map points (up to 5000)
POST /api/teams/photos/approve { photo_ids, team_id } → Approve (fires TagsVerifiedByAdmin)
POST /api/teams/photos/revoke { photo_ids, team_id } → Revoke approval (reverses metrics)
PATCH /api/teams/photos/{photo}/tags { tags: [...] } → Edit tags (CLO format, leader/school_manager)
DELETE /api/teams/photos/{photo}?team_id=X → Delete (reverses metrics first)
# Participant management (auth:sanctum, team leader only)
GET /api/teams/{team}/participants → List slots with photo_count
POST /api/teams/{team}/participants { count: N } → Create slots in bulk
POST /api/teams/{team}/participants/{id}/deactivate → Revoke session
POST /api/teams/{team}/participants/{id}/activate → Re-enable session
POST /api/teams/{team}/participants/{id}/reset-token → Regenerate token (returns new token)
DELETE /api/teams/{team}/participants/{id} → Hard delete slot
# Participant session (public/token auth)
POST /api/participant/session { token: "64-char" } → Validate token, return session info (public)
POST /api/participant/upload → Upload photo (X-Participant-Token header)
POST /api/participant/tags { photo_id, tags } → Tag own photo (X-Participant-Token header)
GET /api/participant/photos → List own photos (X-Participant-Token header)
DELETE /api/participant/photos/{photo} → Delete own pre-approval photo
GET /api/user/profile/{id}
// Returns stats, level, rank, achievements, recent locations
// Respects privacy: returns { public: false } if profile is private
// Respects show_name/show_username flags
GET /api/v3/user/photos?tagged=false&per_page=100&page=1&picked_up=true
// tagged: true | false (omit for all)
// picked_up: true | false (omit for all) — filters by photo-level picked_up status
// per_page: 1-100 (default 8)
// Untagged = WHERE summary IS NULL (NOT doesntHave('photoTags'))
// Returns: { photos: [...], pagination: { current_page, last_page, per_page, total }, user }
Response includes picked_up (boolean, never null) and remaining (deprecated inverse). Use picked_up. Also includes is_public (boolean) and school_team (boolean) — use these to show visibility state and to gate the per-photo toggle (PATCH /api/v3/photos/{id}/visibility is blocked for school_team = true).
new_tags response shape: Each tag includes category_litter_object_id, litter_object_type_id, quantity, picked_up (bool, cast with fallback to photo-level), category (object or null), object (object or null), extra_tags (array). For loose/extra-tag-only tags, category, object, and category_litter_object_id are null. filename field on photo is a full URL, usable directly as image source.
PUT /api/v3/tags accepts empty tags. ReplacePhotoTagsRequest validates tags as present|array (not required|array|min:1). Sending tags: [] clears all tags from a photo (resets summary, XP, verified to untagged state).
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [lon, lat] },
"properties": { "id": 1, "summary": {...}, "username": "..." }
}
]
}
GET /api/clusters?zoom=5&bbox=-180,-90,180,90
// Response includes ETag header
// Client sends If-None-Match on next request
// Returns 304 Not Modified if unchanged
readme/API.md before modifying an endpoint. The full request/response contract is documented there. Changing a response shape without updating the docs breaks the mobile agent.auth:api instead of auth:sanctum. Passport guards (auth:api) are legacy. New routes use auth:sanctum which supports both session and token auth.'tags' instead of 'new_tags' from UsersUploadsController. The frontend reads photo.new_tags for tag counts. Wrong key = broken UI.litter_object_type_id from new_tags response. UsersUploadsController::getNewTags() must include it so the frontend can preserve the type dimension on edit round-trips.readme/API.md when changing an endpoint. The API docs must stay in sync with actual controller behavior.is_public filtering. Any query that returns photo data to unauthenticated users MUST use Photo::public() scope.->value for comparisons: $photo->verified->value >= VerificationStatus::ADMIN_APPROVED->value.geom in API responses. Binary spatial data — keep it in Photo model's $hidden array.doesntHave('photoTags') or WHERE verified = 0 for untagged filter. Use whereNull('summary') — summary is set by GeneratePhotoSummaryService when tags are added, regardless of verification status.msg, others use message. Check the specific controller before asserting response keys in tests.team_id query parameter on team endpoints. Most team endpoints require ?team_id=X — forgetting it returns 422 or wrong team's data.flag field on points responses. GET /api/points/{id} returns a flag field from the user's settings. Mobile clients display this./api/photos/submit, /api/add-tags, /api/v2/*) were removed 2026-03-01. Mobile uses v3 endpoints only.PATCH /api/teams/photos/{photo}/tags uses CLO format (category_litter_object_id), same as POST /api/v3/tags. Not the old { category, object } string format.new_tags in team photo responses. Both index() and show() return new_tags with CLO IDs + extra_tags for the facilitator queue tag editor.remaining instead of picked_up. remaining is deprecated (inverse boolean). Use picked_up (boolean, never null at photo level). Per-tag new_tags[].picked_up is separate and nullable (true/false/null).date field. Backend expects seconds: Carbon::createFromTimestamp((int) $dateInput). JS Date.now() returns milliseconds — divide by 1000.POST /api/profile/photos/delete expects photoid (all lowercase, no underscore). Not photoId or photo_id.