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>
115 lines
4.3 KiB
PHP
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,
|
|
];
|
|
}
|
|
}
|