fix(timetable): wire portal-token auth through artist_engagements
RFC-TIMETABLE v0.2 §5.3 moved portal_token from artists to artist_engagements (one master artist may have multiple per-event portal links). PortalTokenController and PortalTokenMiddleware queried the now-removed artists.portal_token column. Update both lookups to query artist_engagements.portal_token, joining to artists for the master name. Response shape unchanged: data.id = engagement id, data.name = artist name, data.booking_status = engagement status. Middleware sets portal_context='artist' (unchanged); the attached portal_person object now carries the engagement row. PortalTokenSecurityTest seeds artist_engagement rows via a private helper that writes both an Artist (master) and an artist_engagements row with the hashed token; test assertions adjusted to check the new shape (no more milestone fields exposed since they don't exist on the engagement). Out of scope refactor disclaimer: this is a forced schema-migration follow-up, not a Session 2-style controller refactor — the controller queries the new table with minimal change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,18 +18,28 @@ final class PortalTokenController extends Controller
|
||||
{
|
||||
$hashedToken = hash('sha256', $request->validated('token'));
|
||||
|
||||
// Try artists table
|
||||
$artist = DB::table('artists')->where('portal_token', $hashedToken)->first();
|
||||
// Artist portal token lives on artist_engagements (per RFC-TIMETABLE
|
||||
// v0.2 §5.3); join to artists for the master name.
|
||||
$row = DB::table('artist_engagements')
|
||||
->join('artists', 'artists.id', '=', 'artist_engagements.artist_id')
|
||||
->where('artist_engagements.portal_token', $hashedToken)
|
||||
->select(
|
||||
'artist_engagements.id as id',
|
||||
'artist_engagements.event_id as event_id',
|
||||
'artist_engagements.booking_status as booking_status',
|
||||
'artists.name as name',
|
||||
)
|
||||
->first();
|
||||
|
||||
if ($artist) {
|
||||
$event = Event::withoutGlobalScope(OrganisationScope::class)->find($artist->event_id);
|
||||
if ($row) {
|
||||
$event = Event::withoutGlobalScope(OrganisationScope::class)->find($row->event_id);
|
||||
|
||||
return response()->json([
|
||||
'context' => 'artist',
|
||||
'data' => [
|
||||
'id' => $artist->id,
|
||||
'name' => $artist->name,
|
||||
'booking_status' => $artist->booking_status,
|
||||
'id' => $row->id,
|
||||
'name' => $row->name,
|
||||
'booking_status' => $row->booking_status,
|
||||
],
|
||||
'event' => $event ? new PortalEventResource($event) : null,
|
||||
]);
|
||||
|
||||
@@ -23,18 +23,19 @@ final class PortalTokenMiddleware
|
||||
|
||||
$hashedToken = hash('sha256', $plainToken);
|
||||
|
||||
// Try artists table
|
||||
$artist = DB::table('artists')->where('portal_token', $hashedToken)->first();
|
||||
// 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 ($artist) {
|
||||
$event = Event::withoutGlobalScope(OrganisationScope::class)->find($artist->event_id);
|
||||
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', $artist);
|
||||
$request->attributes->set('portal_person', $engagement);
|
||||
$request->attributes->set('portal_event', $event);
|
||||
|
||||
return $next($request);
|
||||
|
||||
Reference in New Issue
Block a user