extractToken($request); if ($plainToken === null) { return response()->json(['message' => 'Portal token required.'], 401); } $hashedToken = hash('sha256', $plainToken); // Artist portal token lives on artist_engagements (per RFC-TIMETABLE // v0.2 §5.3); resolve to the engagement's event. $engagement = DB::table('artist_engagements')->where('portal_token', $hashedToken)->first(); if ($engagement) { $event = Event::withoutGlobalScope(OrganisationScope::class)->find($engagement->event_id); if (! $event || in_array($event->status, ['draft', 'closed'], true)) { return response()->json(['message' => 'Portal token required.'], 401); } $request->attributes->set('portal_context', 'artist'); $request->attributes->set('portal_person', $engagement); $request->attributes->set('portal_event', $event); return $next($request); } // Try production_requests table (may not exist yet) try { $productionRequest = DB::table('production_requests')->where('token', $hashedToken)->first(); if ($productionRequest) { $event = Event::withoutGlobalScope(OrganisationScope::class)->find($productionRequest->event_id); if (! $event || in_array($event->status, ['draft', 'closed'], true)) { return response()->json(['message' => 'Portal token required.'], 401); } $request->attributes->set('portal_context', 'supplier'); $request->attributes->set('portal_person', $productionRequest); $request->attributes->set('portal_event', $event); return $next($request); } } catch (\Illuminate\Database\QueryException) { // Table doesn't exist yet — skip } return response()->json(['message' => 'Portal token required.'], 401); } private function extractToken(Request $request): ?string { // Check Authorization: Bearer header $bearer = $request->bearerToken(); if ($bearer !== null && $bearer !== '') { return $bearer; } // Check query parameter $queryToken = $request->query('token'); if (is_string($queryToken) && $queryToken !== '') { return $queryToken; } return null; } }