Files
crewli/api/app/Http/Resources/Api/V1/MeResource.php
bert.hausmans a2760ffd64 feat(auth): add contexts + platform.is_super_admin to /auth/me, factory role-category states
Additive enrichment to MeResource — existing fields untouched, MeTest stays green.
New fields:
- contexts.available: list<'portal'|'organizer'> derived from Person + Organisation memberships
- contexts.default: precedence super_admin > organizer > portal > fallback portal
- platform.is_super_admin: bool promoted from app_roles
- organisations[].roles: 1-element array form alongside the legacy scalar role,
  forward-compatible for the multi-role pivot work tracked in TECH-PIVOT-ROLES-MULTI

UserFactory gains volunteer(), orgAdmin(), volunteerAndOrganizer(), superAdmin()
state methods — codified role categories for reuse across future workstreams.

Adds forbidden.vue placeholder (PublicLayout) for the context-failure landing in
the upcoming guard rewrite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:15:10 +02:00

115 lines
4.3 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Http\Resources\Api\V1;
use App\Models\Person;
use App\Models\User;
use App\Services\MfaService;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
final class MeResource extends JsonResource
{
public function toArray(Request $request): array
{
/** @var User $user */
$user = $this->resource;
$contexts = $this->resolveContexts($user);
return [
'id' => $this->id,
'first_name' => $this->first_name,
'last_name' => $this->last_name,
'full_name' => $this->full_name,
'date_of_birth' => $this->date_of_birth?->toDateString(),
'email' => $this->email,
'phone' => $this->phone,
'timezone' => $this->timezone,
'locale' => $this->locale,
'avatar' => $this->avatar,
'email_verified_at' => $this->email_verified_at?->toIso8601String(),
'organisations' => $this->whenLoaded('organisations', fn () => $this->organisations->map(fn ($org) => [
'id' => $org->id,
'name' => $org->name,
'slug' => $org->slug,
'role' => $org->pivot->role,
// Forward-compatible array form. The pivot stores a single
// role today; B2a emits it as a 1-element array so the
// frontend can treat the field as multi-role from day one.
// Multi-role pivot resolution is tracked in BACKLOG.md as
// TECH-PIVOT-ROLES-MULTI (ARCH discussion, not just a
// schema-column expansion).
'roles' => [$org->pivot->role],
])
),
'app_roles' => $this->getRoleNames()->values()->all(),
'permissions' => $this->getAllPermissions()->pluck('name')->values()->all(),
'portal_events' => $this->whenLoaded('persons', fn () => $this->persons->map(fn (Person $person) => [
'event_id' => $person->event_id,
'event_name' => $person->event->name,
'event_slug' => $person->event->slug,
'organisation_name' => $person->event->organisation->name,
'person_id' => $person->id,
'person_status' => $person->status,
'start_date' => $person->event->start_date?->toDateString(),
'end_date' => $person->event->end_date?->toDateString(),
])
),
'mfa' => [
'enabled' => $this->mfa_enabled,
'method' => $this->mfa_method,
'confirmed_at' => $this->mfa_confirmed_at?->toIso8601String(),
'setup_required' => app(MfaService::class)->isMfaRequired($this->resource) && ! $this->mfa_enabled,
],
'platform' => [
'is_super_admin' => $user->hasRole('super_admin'),
],
'contexts' => $contexts,
];
}
/**
* Compute available + default UI contexts for this user.
*
* - portal: user has at least one Person record (volunteer-side).
* - organizer: super_admin OR membership in any Organisation pivot.
*
* Default precedence: super_admin → organizer; otherwise the first
* available context wins (organizer before portal, mirroring the
* "familiar context wins on first login" rule from
* ARCH-CONSOLIDATION-2026-04 §4.3). When neither context is
* available, default falls back to 'portal' so the post-login
* landing logic has a safe target to resolve against.
*
* @return array{available: list<string>, default: string}
*/
private function resolveContexts(User $user): array
{
$hasPortal = $user->persons->isNotEmpty();
$hasOrganizer = $user->hasRole('super_admin') || $user->organisations->isNotEmpty();
$available = [];
if ($hasPortal) {
$available[] = 'portal';
}
if ($hasOrganizer) {
$available[] = 'organizer';
}
$default = match (true) {
$user->hasRole('super_admin') => 'organizer',
$hasOrganizer => 'organizer',
$hasPortal => 'portal',
default => 'portal',
};
return [
'available' => $available,
'default' => $default,
];
}
}