一键导入
teams-safeguarding
Teams, school teams, team photos, approval flow, TeamPhotosController, privacy, is_public, PhotoObserver, MasksStudentIdentity, and safeguarding.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Teams, school teams, team photos, approval flow, TeamPhotosController, privacy, is_public, PhotoObserver, MasksStudentIdentity, and safeguarding.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | teams-safeguarding |
| description | Teams, school teams, team photos, approval flow, TeamPhotosController, privacy, is_public, PhotoObserver, MasksStudentIdentity, and safeguarding. |
School teams enforce a private-by-default pipeline. Photos are invisible to the public until a teacher approves them. This protects minors and ensures data quality.
app/Http/Controllers/Teams/TeamPhotosController.php — Photo listing (with new_tags), approval, CLO-based tag editing, map points, delete, revoke, member statsapp/Observers/PhotoObserver.php — Sets is_public = false on school team photo creationapp/Traits/MasksStudentIdentity.php — Masks student names as "Student N"app/Models/Teams/Team.php — isSchool(), isLeader(), hasSafeguarding(), hasParticipantSessions()app/Models/Teams/TeamType.php — team column: 'school' or 'community'app/Models/Teams/Participant.php — Participant slot model (token, activation)app/Http/Middleware/ParticipantAuth.php — Token auth middleware for participant workspaceapp/Http/Controllers/Teams/ParticipantController.php — Facilitator CRUD for participant slotsapp/Http/Controllers/Teams/ParticipantSessionController.php — Token validation + session entryapp/Http/Controllers/Teams/ParticipantPhotoController.php — Participant's own photosapp/Actions/Teams/CreateTeamAction.php — Team creation with school-specific fieldsapp/Http/Requests/Teams/CreateTeamRequest.php — Validation + school_manager role checkapp/Mail/SchoolManagerInvite.php — Queued email sent when school_manager role is granted (two CTAs: Upload + Create Team)app/Events/SchoolDataApproved.php — Private broadcast on team.{id} channelapp/Listeners/NotifyTeamOfApproval.php — Notifies team members after approvalresources/js/views/Teams/FacilitatorQueue.vue — 3-panel layout (filters | PhotoViewer | tag editor)resources/js/views/Teams/components/FacilitatorQueueHeader.vue — Navigation, action buttons (Approve/Save Edits/Revoke/Delete)resources/js/views/Teams/components/FacilitatorQueueFilters.vue — Status toggle (pending/approved/all), date rangeresources/js/views/Teams/components/TeamMembersList.vue — Per-student stats tableresources/js/stores/teamPhotos.js — Team photos Pinia store (CRUD, approve, revoke, delete, updateTags, memberStats)resources/js/views/Teams/TeamsHub.vue — Main teams page (replaces TeamsLayout sidebar + TeamDashboard)resources/js/views/Teams/TeamOverview.vue — Overview tab (stats, team info, all teams list)resources/js/views/Teams/TeamSettingsTab.vue — Consolidated settings tab (privacy, edit, download, leave)resources/js/views/Teams/components/ParticipantGrid.vue — Participant slot management gridresources/js/views/Teams/ParticipantEntry.vue — Token entry page (/session)resources/js/views/Teams/ParticipantWorkspace.vue — Participant upload/photos/tag workspacetests/Feature/Teams/TeamPhotosTest.php — 35 tests (new_tags, CLO tag edits, member stats, safeguarding, delete, revoke, approval, map)tests/Feature/Teams/ParticipantSessionTest.php — 28 tests (slots, token auth, photos, queue, metrics)// TeamPhotosController authorization pattern:
// Team leader OR user with 'manage school team' permission
if (! $team->isLeader($user->id) && ! $user->can('manage school team')) {
return response()->json(['success' => false, 'message' => 'unauthorized'], 403);
}
| Role | How assigned | Facilitator access |
|---|---|---|
| Team leader | team.leader = user_id | Yes — $team->isLeader($userId) |
school_manager | php artisan school:assign-manager {email} or admin toggle | Yes — has manage school team permission |
Critical: School managers are NOT admins. They cannot access /api/admin/* endpoints. The admin queue and facilitator queue are completely separate systems with no overlap.
PhotoObserver::creating() sets is_public = false when team.isSchool(). This is non-negotiable and cannot be overridden by the user. Community team photos respect the uploading user's public_photos default (users.public_photos, boolean, default true).Photo::public() or where('is_public', true). Missing this leaks school data to maps, clusters, exports, and points API.is_trusted. Trust bypasses the teacher approval step entirely. School teams default to is_trusted = false.WHERE is_public = false clause prevents double-processing of already-approved photos.team_user.id (creation order), not photo data or pagination.team.{id}). School team names (e.g., "St. X 1st Years 2026") must never appear on public channels.is_public = false photos never appear in /api/admin/photos. School photos go through teacher approval only.user_id = facilitator. MetricsService, XP, leaderboards are untouched. participant_id is for attribution only.PhotoTagsRequest::authorize() checks $photo->participant_id === $participant->id to prevent cross-participant tagging (all photos share user_id = facilitator).hasParticipantSessions() returns participant_sessions_enabled && isSchool() — community teams can never have participant sessions.leaderboards = false. School teams: safeguarding = true enforced, is_trusted = false enforced.// app/Observers/PhotoObserver.php
public function creating(Photo $photo): void
{
if (! $photo->team_id) {
return;
}
$team = Team::find($photo->team_id);
if ($team && $team->isSchool()) {
$photo->is_public = false;
}
}
// TeamPhotosController::approve()
DB::transaction(function () {
// Atomic update — WHERE is_public = false prevents double-processing
Photo::whereIn('id', $approvedIds)
->where('is_public', false)
->update([
'is_public' => true,
'verified' => VerificationStatus::ADMIN_APPROVED->value,
'team_approved_at' => now(),
'team_approved_by' => $user->id,
]);
// Fire metrics for each newly-approved photo
foreach ($affectedPhotos as $photo) {
event(new TagsVerifiedByAdmin(
photo_id: $photo->id,
user_id: $photo->user_id,
country_id: $photo->country_id,
state_id: $photo->state_id,
city_id: $photo->city_id,
team_id: $photo->team_id
));
}
event(new SchoolDataApproved($team, $teacher, $count));
});
// All public photos (excludes unapproved school photos + soft-deleted)
Photo::public() // ->where('is_public', true)
// All photos for a team (private view — members see everything)
Photo::forTeam($teamId)
// Pending teacher approval
Photo::pendingTeamApproval($teamId)
// ->where('team_id', $teamId)->where('is_public', false)
// ->where('verified', '>=', VERIFIED)->whereNull('team_approved_at')
// Already approved by teacher
Photo::teamApproved($teamId)
// ->where('team_id', $teamId)->whereNotNull('team_approved_at')
// MasksStudentIdentity trait
// Builds stable mapping: user_id -> "Student N" from team_user.id order
if ($team->hasSafeguarding() && !$team->isLeader($viewer->id)
&& !$viewer->hasPermissionTo('view student identities')) {
// Mask names to "Student 1", "Student 2", etc.
}
$team->isSchool() // type_name === 'school'
$team->isLeader($userId) // leader === $userId
$team->hasSafeguarding() // (bool) safeguarding
-- Approval queue: team_id + is_public + verified + created_at
INDEX photos_team_approval_idx ON photos(team_id, is_public, verified, created_at)
-- Team photo listing
INDEX photos_team_public_idx ON photos(team_id, is_public)
-- Public queries
INDEX photos_public_verified_idx ON photos(is_public, verified)
// TeamPhotosController::destroy()
// DELETE /api/teams/photos/{photo}?team_id=X
// 1. Check authorization (leader or 'manage school team')
// 2. If processed: MetricsService::deletePhoto() → reverse metrics
// 3. DeletePhotoAction → S3 cleanup
// 4. $photo->delete() → soft-delete
// 5. Decrement photo owner's XP
// TeamPhotosController::revoke()
// POST /api/teams/photos/revoke { team_id, photo_ids? | revoke_all? }
// 1. Check authorization (leader or 'manage school team')
// 2. Query: is_public = true AND team_approved_at IS NOT NULL
// 3. For each processed photo: MetricsService::deletePhoto()
// 4. Atomic update: is_public=false, verified=VERIFIED, clear approval timestamps
// Idempotent: already-private photos filtered by WHERE clause
// PointsController::formatFeatures()
// After building properties array:
if ($photo->team_id && $photo->team && $photo->team->hasSafeguarding()) {
$properties['name'] = null;
$properties['username'] = null;
$properties['social'] = null;
}
// popup.js shows "Contributed by [Team Name]" when name/username are null but team exists
// TeamPhotosController::updateTags()
// PATCH /api/teams/photos/{photo}/tags
// Accepts CLO payload: { tags: [{ category_litter_object_id, litter_object_type_id?, quantity, picked_up?, materials?, brands?, custom_tags? }] }
DB::transaction(function () use ($request, $photo, $user) {
$photo->photoTags()->each(function ($tag) {
$tag->extraTags()->delete();
$tag->delete();
});
$photo->update(['summary' => null, 'xp' => 0, 'verified' => VerificationStatus::UNVERIFIED->value]);
app(AddTagsToPhotoAction::class)->run($user->id, $photo->id, $request->tags);
});
// TeamPhotosController::index() and show() return new_tags
// Same format as UsersUploadsController::getNewTags() and AdminQueueController
// Includes: category_litter_object_id, litter_object_type_id, category, object, extra_tags
// TeamPhotosController::memberStats()
// GET /api/teams/photos/member-stats?team_id=X
// Returns per-student: total_photos, pending, approved, litter_count, last_active
// Applies safeguarding pseudonyms via MasksStudentIdentity trait
// Leader or 'manage school team' permission required
| Key | Action |
|---|---|
| A | Approve current photo |
| D | Delete (with confirmation) |
| E | Save edits (when modified) |
| R | Revoke approval (with confirmation) |
| S / K / ArrowRight | Next photo |
| J / ArrowLeft | Previous photo |
| Escape | Clear search |
is_public = trueMaps/GlobalMapController — global map pointsHomeController — homepage statsCommunityController — community pageLeaderboard/LeaderboardController — leaderboardsDisplayTagsOnMapController — tag mapHistory/GetPaginatedHistoryController — public historyPoints/PointsController — points APIMapController — map clustersUser/ProfileController — public profilePhoto::public() scope on public-facing endpoints. This leaks school team photos.is_trusted = true on school teams. Trusted teams bypass teacher approval. School teams must always be is_trusted = false.SchoolDataApproved must use private channel team.{id}.team_user.id (join order), not photo data.PhotoObserver when creating photos in tests. The observer auto-fires on Photo::create(). If testing non-school behavior, ensure team_id is null or team is community type.WHERE is_public = false clause in the approval query prevents this, but don't remove it.PATCH /api/v3/photos/{id}/visibility must return 403 for photos belonging to a school team. School photo privacy is teacher-controlled only (approve/revoke), never user-controlled.TeamPhotosController::updateTags() uses CLO format (same as PhotoTagsController::update). Payload uses category_litter_object_id, NOT category/object key strings.new_tags in team photo responses. Both index() and show() must include new_tags with category_litter_object_id, litter_object_type_id, and extra_tags for the facilitator queue tag editor to work.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.
REST API endpoints, route structure, auth guards, request/response contracts, error patterns, and the full API surface for web SPA and mobile clients.
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.