From d5c9821bb5a3317343ceaec57e2ea0aaf8d33ff4 Mon Sep 17 00:00:00 2001 From: Adam Dean Date: Tue, 18 Nov 2025 09:28:10 -0700 Subject: [PATCH 01/17] Feature: Add API Endpoints & Documentation - Added API Endpoint for returning all public events - Added API Endpoint for returning public events for a specified Policy ID - Added a rate limiting throttle of 30 requests per minute as a sensible default - Updates to the app header bar to make it more consistent across pages - Add network notice when browsing preprod deployment alerting users to nightly data reset - Add Laravel Scribe for generating API documentation - Add demo database seeder to create some simulated events and tickets in preprod/local environment for testing layout and UI/UX behavior more reliably - Add "Discover Events" page where users can connect their wallet and find all public events they are eligible for - Updates to Docker Compose file to also run the vite development server when using sail for local development - Add Scribe annotations for public API endpoints - Make some features of Fortify like password changing and registration controllable via environment variables so we can keep have different features based on preprod or production - Schedule preprod environments to reset nightly at 00:00 UTC - Add snackbar to the public events page so we can show better error messaging to the user --- app/Console/Commands/RefreshPreprodDemo.php | 32 ++ app/Console/Kernel.php | 4 +- app/Http/Controllers/EventController.php | 267 +++++++--- app/Models/Event.php | 371 ++++++------- composer.json | 1 + composer.lock | 386 +++++++++++++- config/fortify.php | 310 +++++------ config/scribe.php | 246 +++++++++ database/factories/EventFactory.php | 47 +- database/factories/PolicyFactory.php | 27 + database/factories/TicketFactory.php | 29 ++ .../2025_11_17_205744_add_is_public.php | 32 ++ database/seeders/DatabaseSeeder.php | 17 +- database/seeders/DemoEventSeeder.php | 274 ++++++++++ docker-compose.yml | 18 +- package-lock.json | 9 +- package.json | 3 - resources/js/Components/AppHeader.vue | 116 +++-- resources/js/Components/Banner.vue | 53 +- resources/js/Components/NetworkNotice.vue | 19 + resources/js/Layouts/AppLayout.vue | 219 ++++---- resources/js/Layouts/GuestLayout.vue | 7 +- resources/js/Pages/Event/Discover.vue | 490 ++++++++++++++++++ resources/js/Pages/Event/Show.vue | 102 ++-- resources/js/Pages/Welcome.vue | 363 +++++++------ routes/api.php | 42 +- routes/web.php | 114 ++-- vite.config.js | 39 +- 28 files changed, 2737 insertions(+), 900 deletions(-) create mode 100644 app/Console/Commands/RefreshPreprodDemo.php create mode 100644 config/scribe.php create mode 100644 database/factories/PolicyFactory.php create mode 100644 database/factories/TicketFactory.php create mode 100644 database/migrations/2025_11_17_205744_add_is_public.php create mode 100644 database/seeders/DemoEventSeeder.php create mode 100644 resources/js/Components/NetworkNotice.vue create mode 100644 resources/js/Pages/Event/Discover.vue diff --git a/app/Console/Commands/RefreshPreprodDemo.php b/app/Console/Commands/RefreshPreprodDemo.php new file mode 100644 index 0000000..95e3a2b --- /dev/null +++ b/app/Console/Commands/RefreshPreprodDemo.php @@ -0,0 +1,32 @@ +environment('preprod')) { + $this->error('This command is only allowed in the preprod environment.'); + return self::FAILURE; + } + + $this->info('Refreshing preprod database (migrate:fresh --seed)...'); + + Artisan::call('migrate:fresh', [ + '--seed' => true, + '--force' => true, + ]); + + $this->line(Artisan::output()); + + $this->info('Preprod database refresh complete.'); + return self::SUCCESS; + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index e6b9960..96bfc7d 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -12,7 +12,9 @@ class Kernel extends ConsoleKernel */ protected function schedule(Schedule $schedule): void { - // $schedule->command('inspire')->hourly(); + $schedule->command('preprod:refresh-demo') + ->dailyAt('00:00') + ->environments(['preprod']); } /** diff --git a/app/Http/Controllers/EventController.php b/app/Http/Controllers/EventController.php index ec1f673..c811835 100644 --- a/app/Http/Controllers/EventController.php +++ b/app/Http/Controllers/EventController.php @@ -1,76 +1,219 @@ input('policy_hashes', []); - /** - * Display the specified resource. - */ - public function show(Request $request, string $eventUUID) - { - $event = Event::where('uuid', $eventUUID) - ->with([ - 'policies', - 'team' - ]) - ->firstOrFail(); - - return Jetstream::inertia() - ->render($request, 'Event/Show', compact('event')); + if (is_array($policyHashes)) { + $policyHashes = array_values(array_filter($policyHashes)); + sort($policyHashes); // order-independent + } else { + $policyHashes = [$policyHashes]; } - /** - * Show the form for editing the specified resource. - */ - public function edit(Event $event) - { - // - } + $cacheKey = $this->buildCacheKey($policyHashes); - /** - * Update the specified resource in storage. - */ - public function update(UpdateEventRequest $request, Event $event) - { - // - } + $data = Cache::remember($cacheKey, now()->addMinutes(5), function () use ($policyHashes) { + $now = Carbon::now(); + + $query = Event::query() + ->where('is_public', true) + ->where(function ($q) use ($now) { + // Upcoming or currently-running: not ended yet + $q->whereNull('end_date_time') + ->orWhere('end_date_time', '>=', $now); + }) + ->with([ + 'policies:id,hash,name', + ]) + ->orderBy('start_date_time'); + + if (!empty($policyHashes) && is_array($policyHashes)) { + $query->whereHas('policies', function ($q) use ($policyHashes) { + $q->whereIn('hash', $policyHashes); + }); + } - /** - * Remove the specified resource from storage. - */ - public function destroy(Event $event) - { - // + $events = $query->get(); + + return $events->map(function (Event $event) { + return [ + 'uuid' => $event->uuid, + 'name' => $event->name, + 'description' => $event->description, + 'location' => $event->location, + 'date' => $event->event_date?->toDateString(), + 'start' => $event->start_date_time?->toIso8601String(), + 'end' => $event->end_date_time?->toIso8601String(), + 'policies' => $event->policies->map(fn($policy) => [ + 'hash' => $policy->hash, + 'name' => $policy->name, + ])->values(), + 'policy_hashes' => $event->policies->pluck('hash')->values(), + ]; + })->toArray(); + }); + + + return response()->json([ + 'data' => $data, + ]); + } + + /** + * Discover public events for a specific policy. + * + * Returns upcoming public events that accept the given Cardano policy ID. + * This is a convenience wrapper around the main discover endpoint with a single policy filter. + * + * @group Public API + * @unauthenticated + * + * @urlParam policyHash string required The Cardano policy ID to filter by. + * Example: a0028f35d4c7b4f2f0b1d6a0e4c3a9f6d0b0cafe + * + * @response 200 scenario="success" { + * "data": [ + * { + * "uuid": "8f5fa03d-02c6-49f1-985f-d71544de8919", + * "name": "Hydra Launch Party", + * "description": "An exclusive Hydra-based launch event with live demos.", + * "location": "Las Vegas Convention Center", + * "date": "2025-01-24", + * "start": "2025-01-24T18:00:00Z", + * "end": "2025-01-24T21:00:00Z", + * "policies": [ + * { + * "hash": "a0028f35d4c7b4f2f0b1d6a0e4c3a9f6d0b0cafe", + * "name": "HOSKY Token" + * } + * ], + * "policy_hashes": [ + * "a0028f35d4c7b4f2f0b1d6a0e4c3a9f6d0b0cafe" + * ] + * } + * ] + * } + * + * @responseField data[].uuid string The event UUID used in app links. + * @responseField data[].name string The display name of the event. + * @responseField data[].description string|null A short description of the event. + * @responseField data[].location string|null The event venue or location. + * @responseField data[].date string|null Event date in Y-m-d format. + * @responseField data[].start string|null ISO8601 start datetime. + * @responseField data[].end string|null ISO8601 end datetime. + * @responseField data[].policies[].hash string Cardano policy ID accepted for this event. + * @responseField data[].policies[].name string Human-readable label for the policy. + * @responseField data[].policy_hashes string[] Convenience array of all policy hashes for this event. + */ + public function byPolicy(Request $request, string $policyHash): JsonResponse + { + // Reuse the existing logic in index(), but pre-fill policy_hashes[] + $request->merge([ + 'policy_hashes' => [$policyHash], + ]); + + return $this->index($request); + } + + protected function buildCacheKey(array $policyHashes): string + { + if (empty($policyHashes)) { + return 'public_events:all'; } + + return 'public_events:policies:'.md5(json_encode($policyHashes)); + } + + /** + * Display the specified event. + */ + public function show(Request $request, string $eventUUID) + { + $event = Event::where('uuid', $eventUUID) + ->with([ + 'policies', + 'team' + ]) + ->firstOrFail(); + + return Jetstream::inertia() + ->render($request, 'Event/Show', compact('event')); + } + + /** + * Connect your wallet and discover eligible events + */ + public function discoverPage(Request $request) + { + // No props needed initially; the page will call the API itself + return Jetstream::inertia() + ->render($request, 'Event/Discover'); } +} diff --git a/app/Models/Event.php b/app/Models/Event.php index c979a4d..a1a9c56 100644 --- a/app/Models/Event.php +++ b/app/Models/Event.php @@ -1,219 +1,230 @@ 'date', + 'start_date_time' => 'datetime', + 'end_date_time' => 'datetime', + 'is_public' => 'boolean', + ]; + + public function tickets(): HasMany { + return $this->hasMany(Ticket::class); + } - use HasFactory; - use HasProfilePhoto; - use HasBackgroundImage; - - protected $fillable = [ - 'uuid', - 'team_id', - 'user_id', - 'name', - 'nonce_valid_for_minutes', - 'hodl_asset', - 'start_date_time', - 'end_date_time', - 'location', - 'event_start', - 'event_end', - 'event_date', - 'image', - ]; - - protected $dates = [ - 'start_date_time', - 'end_date_time', - ]; - - protected $hidden = [ - 'id', - 'team_id', - 'user_id', - 'created_at', - 'updated_at', - 'checkins', - 'checkouts' - ]; + public function checkins(): HasManyThrough + { + return $this->hasManyThrough(Checkin::class, Ticket::class); + } - protected $appends = [ - 'profile_photo_url', - 'bg_image_url', - ]; + public function checkouts(): HasManyThrough + { + return $this->hasManyThrough(Checkout::class, Ticket::class); + } - public function tickets(): HasMany - { - return $this->hasMany(Ticket::class); - } + public function attendance(): Collection + { + return collect([ + ...$this->checkins, + ...$this->checkouts + ]); +// return $this->checkins +// ->merge($this->checkouts); + } - public function checkins(): HasManyThrough - { - return $this->hasManyThrough(Checkin::class, Ticket::class); - } + public function policies(): BelongsToMany + { + return $this->belongsToMany(Policy::class); + } - public function checkouts(): HasManyThrough - { - return $this->hasManyThrough(Checkout::class, Ticket::class); - } + public function team(): BelongsTo + { + return $this->belongsTo(Team::class); + } - public function attendance(): Collection - { - return collect([ - ...$this->checkins, - ...$this->checkouts - ]); -// return $this->checkins -// ->merge($this->checkouts); - } + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } - public function policies(): BelongsToMany - { - return $this->belongsToMany(Policy::class); + public function description() + { + $description = ""; + if ($this->event_date) { + $description .= date('l, F jS, Y', strtotime($this->event_date)); } - public function team(): BelongsTo - { - return $this->belongsTo(Team::class); + if ($this->event_start && $this->event_end) { + $description .= " " . $this->event_start . " to " . $this->event_end . "."; + } elseif ($this->event_start) { + $description .= " " . $this->event_start; + } elseif ($this->event_end) { + $description .= " until " . $this->event_end; } - public function user(): BelongsTo - { - return $this->belongsTo(User::class); + if ($this->location) { + $description .= " " . $this->location; } - public function description() - { - $description = ""; - if ($this->event_date) { - $description .= date('l, F jS, Y', strtotime($this->event_date)); - } - - if ($this->event_start && $this->event_end) { - $description .= " " . $this->event_start . " to " . $this->event_end . "."; - } elseif ($this->event_start) { - $description .= " " . $this->event_start; - } elseif ($this->event_end) { - $description .= " until " . $this->event_end; - } + return $description; - if ($this->location) { - $description .= " " . $this->location; - } + } - return $description; + public function isTicketingActive(): bool + { + $now = Carbon::now(); + if (!empty($this->start_date_time) && $now->isBefore($this->start_date_time)) { + return false; } - public function isTicketingActive(): bool - { - $now = Carbon::now(); - - if (!empty($this->start_date_time) && $now->isBefore($this->start_date_time)) { - return false; - } - - if ($now->isAfter($this->end_date_time)) { - return false; - } - - return true; + if ($now->isAfter($this->end_date_time)) { + return false; } - public function loadStats(): void - { - $stats = [ - 'tickets' => [ - 'count' => 0, - 'byDate' => [], - 'byPolicy' => [] - ], - 'attendance' => [ - 'unique_checkin' => Ticket::where('event_id', $this->id) - ->has('checkins') - ->count(), - 'unique_checkout' => Ticket::where('event_id', $this->id) - ->has('checkouts') - ->count(), - 'byUser' => [], - 'byTime' => [ - 'labels' => [], - 'datasets' => [ - [ - 'label' => 'Attendance', - 'data' => [] - ] - ] - ] - ], - ]; + return true; + } - $event_tickets = Ticket::where('event_id', $this->id) - ->get(); + public function loadStats(): void + { + $stats = [ + 'tickets' => [ + 'count' => 0, + 'byDate' => [], + 'byPolicy' => [], + ], + 'attendance' => [ + 'unique_checkin' => Ticket::where('event_id', $this->id) + ->has('checkins') + ->count(), + 'unique_checkout' => Ticket::where('event_id', $this->id) + ->has('checkouts') + ->count(), + 'byUser' => [], + 'byTime' => [ + 'labels' => [], + 'datasets' => [ + [ + 'label' => 'Attendance', + 'data' => [], + ], + ], + ], + ], + ]; - foreach ($event_tickets as $ticket) { - $stats['tickets']['count']++; - $ticket_policy = $this->policies() - ->where('id', $ticket->policy_id) - ->firstOrFail(); + $event_tickets = Ticket::where('event_id', $this->id)->get(); - @$stats['tickets']['byPolicy'][$ticket_policy->name]++; + foreach ($event_tickets as $ticket) { + $stats['tickets']['count']++; - $ticket_created = Carbon::parse($ticket->created_at) - ->format('Y-m-d'); + $ticket_policy = $this->policies() + ->where('id', $ticket->policy_id) + ->first(); - @$stats['tickets']['byDate'][$ticket_created]++; + if (!$ticket_policy) { + \Log::warning('Ticket policy not attached to event', [ + 'event_id' => $this->id, + 'event_uuid' => $this->uuid, + 'ticket_id' => $ticket->id, + 'policy_id' => $ticket->policy_id, + ]); + continue; } - foreach ($this->team - ->allUsers() as $team_user) { - @$stats['attendance']['byUser'][$team_user->name]['checkin'] = $this->checkins() - ->where('user_id', $team_user->id) - ->count(); - @$stats['attendance']['byUser'][$team_user->name]['checkout'] = $this->checkouts() - ->where('user_id', $team_user->id) - ->count(); - } + $policyName = $ticket_policy->name; + $stats['tickets']['byPolicy'][$policyName] = ($stats['tickets']['byPolicy'][$policyName] ?? 0) + 1; - $stats['tickets']['policy']['pieChart'] = [ - 'labels' => array_keys($stats['tickets']['byPolicy']), - 'datasets' => [ - [ - 'data' => array_values($stats['tickets']['byPolicy']), - ] - ] - ]; + $ticket_created = \Carbon\Carbon::parse($ticket->created_at)->format('Y-m-d'); + $stats['tickets']['byDate'][$ticket_created] = ($stats['tickets']['byDate'][$ticket_created] ?? 0) + 1; + } - $this->checkins(); - $this->checkouts(); + foreach ($this->team->allUsers() as $team_user) { + $checkinCount = $this->checkins()->where('user_id', $team_user->id)->count(); + $checkoutCount = $this->checkouts()->where('user_id', $team_user->id)->count(); - $att = 0; - $attendance = $this->attendance() - ->sortBy('created_at'); - foreach ($attendance as $entry) { -// $stats['attendance']['byTime']['datasets'][0]['data'][] = $entry; + $stats['attendance']['byUser'][$team_user->name]['checkin'] = $checkinCount; + $stats['attendance']['byUser'][$team_user->name]['checkout'] = $checkoutCount; + } + + $stats['tickets']['policy']['pieChart'] = [ + 'labels' => array_keys($stats['tickets']['byPolicy']), + 'datasets' => [ + [ + 'data' => array_values($stats['tickets']['byPolicy']), + ], + ], + ]; - $att += $entry->direction === 'in' ? 1 : -1; + $this->checkins(); + $this->checkouts(); - $stats['attendance']['byTime']['labels'][] = $entry->created_at->format('Y-m-d H:i'); - $stats['attendance']['byTime']['datasets'][0]['data'][] = $att; - } + $att = 0; + $attendance = $this->attendance()->sortBy('created_at'); - $this->stats = $stats; + foreach ($attendance as $entry) { + $att += $entry->direction === 'in' ? 1 : -1; + $stats['attendance']['byTime']['labels'][] = $entry->created_at->format('Y-m-d H:i'); + $stats['attendance']['byTime']['datasets'][0]['data'][] = $att; } + + $this->stats = $stats; } +} diff --git a/composer.json b/composer.json index b965c93..683ac03 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ }, "require-dev": { "fakerphp/faker": "^1.9.1", + "knuckleswtf/scribe": "^5.5", "laravel/pint": "^1.0", "laravel/sail": "^1.18", "mockery/mockery": "^1.4.4", diff --git a/composer.lock b/composer.lock index 8e21953..478209d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "39de03486f1c906f2b6ffc0bee1bfaa2", + "content-hash": "bf6cea2a8974693050fadf9c11f77453", "packages": [ { "name": "aws/aws-crt-php", @@ -7020,6 +7020,56 @@ } ], "packages-dev": [ + { + "name": "erusev/parsedown", + "version": "1.7.4", + "source": { + "type": "git", + "url": "https://github.com/erusev/parsedown.git", + "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/erusev/parsedown/zipball/cb17b6477dfff935958ba01325f2e8a2bfa6dab3", + "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35" + }, + "type": "library", + "autoload": { + "psr-0": { + "Parsedown": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Emanuil Rusev", + "email": "hello@erusev.com", + "homepage": "http://erusev.com" + } + ], + "description": "Parser for Markdown.", + "homepage": "http://parsedown.org", + "keywords": [ + "markdown", + "parser" + ], + "support": { + "issues": "https://github.com/erusev/parsedown/issues", + "source": "https://github.com/erusev/parsedown/tree/1.7.x" + }, + "time": "2019-12-30T22:54:17+00:00" + }, { "name": "fakerphp/faker", "version": "v1.23.1", @@ -7205,6 +7255,99 @@ }, "time": "2020-07-09T08:09:16+00:00" }, + { + "name": "knuckleswtf/scribe", + "version": "5.5.0", + "source": { + "type": "git", + "url": "https://github.com/knuckleswtf/scribe.git", + "reference": "779f91aa01a18d271b2c8314d5c44635c7375dfc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/knuckleswtf/scribe/zipball/779f91aa01a18d271b2c8314d5c44635c7375dfc", + "reference": "779f91aa01a18d271b2c8314d5c44635c7375dfc", + "shasum": "" + }, + "require": { + "erusev/parsedown": "1.7.4", + "ext-fileinfo": "*", + "ext-json": "*", + "ext-pdo": "*", + "fakerphp/faker": "^1.23.1", + "laravel/framework": "^9.0|^10.0|^11.0|^12.0", + "league/flysystem": "^3.0", + "mpociot/reflection-docblock": "^1.0.1", + "nikic/php-parser": "^5.0", + "nunomaduro/collision": "^6.0|^7.0|^8.0", + "php": ">=8.1", + "ramsey/uuid": "^4.2.2", + "shalvah/clara": "3.3.0", + "shalvah/upgrader": ">=0.6.0", + "symfony/var-exporter": "^6.0|^7.0", + "symfony/yaml": "^6.0|^7.0" + }, + "replace": { + "mpociot/laravel-apidoc-generator": "*" + }, + "require-dev": { + "dms/phpunit-arraysubset-asserts": "^v0.5.0", + "laravel/legacy-factories": "^1.3.0", + "league/fractal": "^0.20", + "nikic/fast-route": "^1.3", + "orchestra/testbench": "^7.0|^8.0|^v9.10.0|^10.0", + "pestphp/pest": "^1.21|^2.0|^3.0", + "phpstan/phpstan": "^2.1.5", + "phpunit/phpunit": "^9.0|^10.0|^11.0", + "spatie/ray": "^1.41", + "symfony/css-selector": "^6.0|^7.0", + "symfony/dom-crawler": "^6.0|^7.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Knuckles\\Scribe\\ScribeServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/Config/helpers.php" + ], + "psr-4": { + "Knuckles\\Camel\\": "camel/", + "Knuckles\\Scribe\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Shalvah" + } + ], + "description": "Generate API documentation for humans from your Laravel codebase.✍", + "homepage": "http://github.com/knuckleswtf/scribe", + "keywords": [ + "api", + "documentation", + "laravel" + ], + "support": { + "issues": "https://github.com/knuckleswtf/scribe/issues", + "source": "https://github.com/knuckleswtf/scribe/tree/5.5.0" + }, + "funding": [ + { + "url": "https://patreon.com/shalvah", + "type": "patreon" + } + ], + "time": "2025-10-25T17:58:31+00:00" + }, { "name": "laravel/pint", "version": "v1.16.2", @@ -7417,6 +7560,59 @@ }, "time": "2024-05-16T03:13:13+00:00" }, + { + "name": "mpociot/reflection-docblock", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/mpociot/reflection-docblock.git", + "reference": "c8b2e2b1f5cebbb06e2b5ccbf2958f2198867587" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mpociot/reflection-docblock/zipball/c8b2e2b1f5cebbb06e2b5ccbf2958f2198867587", + "reference": "c8b2e2b1f5cebbb06e2b5ccbf2958f2198867587", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "suggest": { + "dflydev/markdown": "~1.0", + "erusev/parsedown": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Mpociot": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "mike.vanriel@naenius.com" + } + ], + "support": { + "issues": "https://github.com/mpociot/reflection-docblock/issues", + "source": "https://github.com/mpociot/reflection-docblock/tree/master" + }, + "time": "2016-06-20T20:53:12+00:00" + }, { "name": "myclabs/deep-copy", "version": "1.12.0", @@ -9029,6 +9225,111 @@ ], "time": "2023-02-07T11:34:05+00:00" }, + { + "name": "shalvah/clara", + "version": "3.3.0", + "source": { + "type": "git", + "url": "https://github.com/shalvah/clara.git", + "reference": "6b30f389d71bfdd3f6e09f9b9537205ac7e118de" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/shalvah/clara/zipball/6b30f389d71bfdd3f6e09f9b9537205ac7e118de", + "reference": "6b30f389d71bfdd3f6e09f9b9537205ac7e118de", + "shasum": "" + }, + "require": { + "php": ">=7.4", + "symfony/console": "^4.0|^5.0|^6.0|^7.0" + }, + "require-dev": { + "eloquent/phony-phpunit": "^7.0", + "phpunit/phpunit": "^9.1" + }, + "type": "library", + "autoload": { + "files": [ + "helpers.php" + ], + "psr-4": { + "Shalvah\\Clara\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "🔊 Simple, pretty, testable console output for CLI apps.", + "keywords": [ + "cli", + "log", + "logging" + ], + "support": { + "issues": "https://github.com/shalvah/clara/issues", + "source": "https://github.com/shalvah/clara/tree/3.3.0" + }, + "time": "2025-10-20T22:26:39+00:00" + }, + { + "name": "shalvah/upgrader", + "version": "0.6.0", + "source": { + "type": "git", + "url": "https://github.com/shalvah/upgrader.git", + "reference": "d95ed17fe9f5e1ee7d47ad835595f1af080a867f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/shalvah/upgrader/zipball/d95ed17fe9f5e1ee7d47ad835595f1af080a867f", + "reference": "d95ed17fe9f5e1ee7d47ad835595f1af080a867f", + "shasum": "" + }, + "require": { + "illuminate/support": ">=8.0", + "nikic/php-parser": "^5.0", + "php": ">=8.0" + }, + "require-dev": { + "dms/phpunit-arraysubset-asserts": "^0.2.0", + "pestphp/pest": "^1.21", + "phpstan/phpstan": "^1.0", + "spatie/ray": "^1.33" + }, + "type": "library", + "autoload": { + "psr-4": { + "Shalvah\\Upgrader\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Shalvah", + "email": "hello@shalvah.me" + } + ], + "description": "Create automatic upgrades for your package.", + "homepage": "http://github.com/shalvah/upgrader", + "keywords": [ + "upgrade" + ], + "support": { + "issues": "https://github.com/shalvah/upgrader/issues", + "source": "https://github.com/shalvah/upgrader/tree/0.6.0" + }, + "funding": [ + { + "url": "https://patreon.com/shalvah", + "type": "patreon" + } + ], + "time": "2024-02-20T11:51:46+00:00" + }, { "name": "spatie/backtrace", "version": "1.6.1", @@ -9409,6 +9710,87 @@ ], "time": "2024-06-12T15:01:18+00:00" }, + { + "name": "symfony/var-exporter", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/0f020b544a30a7fe8ba972e53ee48a74c0bc87f4", + "reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "require-dev": { + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T10:12:26+00:00" + }, { "name": "symfony/yaml", "version": "v7.1.1", @@ -9540,5 +9922,5 @@ "php": "^8.1" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/config/fortify.php b/config/fortify.php index 726d83b..c122f94 100644 --- a/config/fortify.php +++ b/config/fortify.php @@ -4,156 +4,164 @@ return [ - /* - |-------------------------------------------------------------------------- - | Fortify Guard - |-------------------------------------------------------------------------- - | - | Here you may specify which authentication guard Fortify will use while - | authenticating users. This value should correspond with one of your - | guards that is already present in your "auth" configuration file. - | - */ - - 'guard' => 'web', - - /* - |-------------------------------------------------------------------------- - | Fortify Password Broker - |-------------------------------------------------------------------------- - | - | Here you may specify which password broker Fortify can use when a user - | is resetting their password. This configured value should match one - | of your password brokers setup in your "auth" configuration file. - | - */ - - 'passwords' => 'users', - - /* - |-------------------------------------------------------------------------- - | Username / Email - |-------------------------------------------------------------------------- - | - | This value defines which model attribute should be considered as your - | application's "username" field. Typically, this might be the email - | address of the users but you are free to change this value here. - | - | Out of the box, Fortify expects forgot password and reset password - | requests to have a field named 'email'. If the application uses - | another name for the field you may define it below as needed. - | - */ - - 'username' => 'email', - - 'email' => 'email', - - /* - |-------------------------------------------------------------------------- - | Lowercase Usernames - |-------------------------------------------------------------------------- - | - | This value defines whether usernames should be lowercased before saving - | them in the database, as some database system string fields are case - | sensitive. You may disable this for your application if necessary. - | - */ - - 'lowercase_usernames' => true, - - /* - |-------------------------------------------------------------------------- - | Home Path - |-------------------------------------------------------------------------- - | - | Here you may configure the path where users will get redirected during - | authentication or password reset when the operations are successful - | and the user is authenticated. You are free to change this value. - | - */ - - 'home' => '/dashboard', - - /* - |-------------------------------------------------------------------------- - | Fortify Routes Prefix / Subdomain - |-------------------------------------------------------------------------- - | - | Here you may specify which prefix Fortify will assign to all the routes - | that it registers with the application. If necessary, you may change - | subdomain under which all of the Fortify routes will be available. - | - */ - - 'prefix' => '', - - 'domain' => null, - - /* - |-------------------------------------------------------------------------- - | Fortify Routes Middleware - |-------------------------------------------------------------------------- - | - | Here you may specify which middleware Fortify will assign to the routes - | that it registers with the application. If necessary, you may change - | these middleware but typically this provided default is preferred. - | - */ - - 'middleware' => ['web'], - - /* - |-------------------------------------------------------------------------- - | Rate Limiting - |-------------------------------------------------------------------------- - | - | By default, Fortify will throttle logins to five requests per minute for - | every email and IP address combination. However, if you would like to - | specify a custom rate limiter to call then you may specify it here. - | - */ - - 'limiters' => [ - 'login' => 'login', - 'two-factor' => 'two-factor', - ], - - /* - |-------------------------------------------------------------------------- - | Register View Routes - |-------------------------------------------------------------------------- - | - | Here you may specify if the routes returning views should be disabled as - | you may not need them when building your own application. This may be - | especially true if you're writing a custom single-page application. - | - */ - - 'views' => true, - - /* - |-------------------------------------------------------------------------- - | Features - |-------------------------------------------------------------------------- - | - | Some of the Fortify features are optional. You may disable the features - | by removing them from this array. You're free to only remove some of - | these features or you can even remove all of these if you need to. - | - */ - - 'features' => [ - Features::registration(), - Features::resetPasswords(), - // Features::emailVerification(), - Features::updateProfileInformation(), - Features::updatePasswords(), - Features::twoFactorAuthentication([ - 'confirm' => true, - 'confirmPassword' => true, - // 'window' => 0, - ]), - ], + /* + |-------------------------------------------------------------------------- + | Fortify Guard + |-------------------------------------------------------------------------- + | + | Here you may specify which authentication guard Fortify will use while + | authenticating users. This value should correspond with one of your + | guards that is already present in your "auth" configuration file. + | + */ + + 'guard' => 'web', + + /* + |-------------------------------------------------------------------------- + | Fortify Password Broker + |-------------------------------------------------------------------------- + | + | Here you may specify which password broker Fortify can use when a user + | is resetting their password. This configured value should match one + | of your password brokers setup in your "auth" configuration file. + | + */ + + 'passwords' => 'users', + + /* + |-------------------------------------------------------------------------- + | Username / Email + |-------------------------------------------------------------------------- + | + | This value defines which model attribute should be considered as your + | application's "username" field. Typically, this might be the email + | address of the users but you are free to change this value here. + | + | Out of the box, Fortify expects forgot password and reset password + | requests to have a field named 'email'. If the application uses + | another name for the field you may define it below as needed. + | + */ + + 'username' => 'email', + + 'email' => 'email', + + /* + |-------------------------------------------------------------------------- + | Lowercase Usernames + |-------------------------------------------------------------------------- + | + | This value defines whether usernames should be lowercased before saving + | them in the database, as some database system string fields are case + | sensitive. You may disable this for your application if necessary. + | + */ + + 'lowercase_usernames' => true, + + /* + |-------------------------------------------------------------------------- + | Home Path + |-------------------------------------------------------------------------- + | + | Here you may configure the path where users will get redirected during + | authentication or password reset when the operations are successful + | and the user is authenticated. You are free to change this value. + | + */ + + 'home' => '/dashboard', + + /* + |-------------------------------------------------------------------------- + | Fortify Routes Prefix / Subdomain + |-------------------------------------------------------------------------- + | + | Here you may specify which prefix Fortify will assign to all the routes + | that it registers with the application. If necessary, you may change + | subdomain under which all of the Fortify routes will be available. + | + */ + + 'prefix' => '', + + 'domain' => null, + + /* + |-------------------------------------------------------------------------- + | Fortify Routes Middleware + |-------------------------------------------------------------------------- + | + | Here you may specify which middleware Fortify will assign to the routes + | that it registers with the application. If necessary, you may change + | these middleware but typically this provided default is preferred. + | + */ + + 'middleware' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Rate Limiting + |-------------------------------------------------------------------------- + | + | By default, Fortify will throttle logins to five requests per minute for + | every email and IP address combination. However, if you would like to + | specify a custom rate limiter to call then you may specify it here. + | + */ + + 'limiters' => [ + 'login' => 'login', + 'two-factor' => 'two-factor', + ], + + /* + |-------------------------------------------------------------------------- + | Register View Routes + |-------------------------------------------------------------------------- + | + | Here you may specify if the routes returning views should be disabled as + | you may not need them when building your own application. This may be + | especially true if you're writing a custom single-page application. + | + */ + + 'views' => true, + + /* + |-------------------------------------------------------------------------- + | Features + |-------------------------------------------------------------------------- + | + | Some of the Fortify features are optional. You may disable the features + | by removing them from this array. You're free to only remove some of + | these features or you can even remove all of these if you need to. + | + */ + + 'features' => [ + env('ALLOW_REGISTRATION', false) + ? Features::registration() + : null, + env('ALLOW_PASSWORD_CHANGE', true) + ? Features::resetPasswords() + : null, + // Features::emailVerification(), + Features::updateProfileInformation(), + env('ALLOW_PASSWORD_CHANGE', true) + ? Features::updatePasswords() + : null, + env('ALLOW_PASSWORD_CHANGE', true) + ? Features::twoFactorAuthentication([ + 'confirm' => true, + 'confirmPassword' => true, + // 'window' => 0, + ]) + : null, + ], ]; diff --git a/config/scribe.php b/config/scribe.php new file mode 100644 index 0000000..a333ba5 --- /dev/null +++ b/config/scribe.php @@ -0,0 +1,246 @@ + for the generated documentation. + 'title' => config('app.name') . ' API Documentation', + + // A short description of your API. Will be included in the docs webpage, Postman collection and OpenAPI spec. + 'description' => '', + + // Text to place in the "Introduction" section, right after the `description`. Markdown and HTML are supported. + 'intro_text' => <<As you scroll, you'll see code examples for working with the API in different programming languages in the dark area to the right (or as part of the content on mobile). + You can switch the language used with the tabs at the top right (or from the nav menu at the top left on mobile). + INTRO, + + // The base URL displayed in the docs. + // If you're using `laravel` type, you can set this to a dynamic string, like '{{ config("app.tenant_url") }}' to get a dynamic base URL. + 'base_url' => config("app.url"), + + // Routes to include in the docs + 'routes' => [ + [ + 'match' => [ + 'prefixes' => [], + 'domains' => ['*'], + ], + 'include' => [ + 'api.events.discover', + 'api.events.by-policy', + ], + 'exclude' => [], + ], + ], + + // The type of documentation output to generate. + // - "static" will generate a static HTMl page in the /public/docs folder, + // - "laravel" will generate the documentation as a Blade view, so you can add routing and authentication. + // - "external_static" and "external_laravel" do the same as above, but pass the OpenAPI spec as a URL to an external UI template + 'type' => 'static', + + // See https://scribe.knuckles.wtf/laravel/reference/config#theme for supported options + 'theme' => 'default', + + 'static' => [ + // HTML documentation, assets and Postman collection will be generated to this folder. + // Source Markdown will still be in resources/docs. + 'output_path' => 'docs/api', + ], + + 'laravel' => [ + // Whether to automatically create a docs route for you to view your generated docs. You can still set up routing manually. + 'add_routes' => true, + + // URL path to use for the docs endpoint (if `add_routes` is true). + // By default, `/docs` opens the HTML page, `/docs.postman` opens the Postman collection, and `/docs.openapi` the OpenAPI spec. + 'docs_url' => '/docs', + + // Directory within `public` in which to store CSS and JS assets. + // By default, assets are stored in `public/vendor/scribe`. + // If set, assets will be stored in `public/{{assets_directory}}` + 'assets_directory' => null, + + // Middleware to attach to the docs endpoint (if `add_routes` is true). + 'middleware' => [], + ], + + 'external' => [ + 'html_attributes' => [] + ], + + 'try_it_out' => [ + // Add a Try It Out button to your endpoints so consumers can test endpoints right from their browser. + // Don't forget to enable CORS headers for your endpoints. + 'enabled' => false, + + // The base URL to use in the API tester. Leave as null to be the same as the displayed URL (`scribe.base_url`). + 'base_url' => null, + + // [Laravel Sanctum] Fetch a CSRF token before each request, and add it as an X-XSRF-TOKEN header. + 'use_csrf' => false, + + // The URL to fetch the CSRF token from (if `use_csrf` is true). + 'csrf_url' => '/sanctum/csrf-cookie', + ], + + // How is your API authenticated? This information will be used in the displayed docs, generated examples and response calls. + 'auth' => [ + // Set this to true if ANY endpoints in your API use authentication. + 'enabled' => false, + + // Set this to true if your API should be authenticated by default. If so, you must also set `enabled` (above) to true. + // You can then use @unauthenticated or @authenticated on individual endpoints to change their status from the default. + 'default' => false, + + // Where is the auth value meant to be sent in a request? + 'in' => AuthIn::BEARER->value, + + // The name of the auth parameter (e.g. token, key, apiKey) or header (e.g. Authorization, Api-Key). + 'name' => 'key', + + // The value of the parameter to be used by Scribe to authenticate response calls. + // This will NOT be included in the generated documentation. If empty, Scribe will use a random value. + 'use_value' => env('SCRIBE_AUTH_KEY'), + + // Placeholder your users will see for the auth parameter in the example requests. + // Set this to null if you want Scribe to use a random value as placeholder instead. + 'placeholder' => '{YOUR_AUTH_KEY}', + + // Any extra authentication-related info for your users. Markdown and HTML are supported. + 'extra_info' => 'You can retrieve your token by visiting your dashboard and clicking Generate API token.', + ], + + // Example requests for each endpoint will be shown in each of these languages. + // Supported options are: bash, javascript, php, python + // To add a language of your own, see https://scribe.knuckles.wtf/laravel/advanced/example-requests + // Note: does not work for `external` docs types + 'example_languages' => [ + 'bash', + 'javascript', + ], + + // Generate a Postman collection (v2.1.0) in addition to HTML docs. + // For 'static' docs, the collection will be generated to public/docs/collection.json. + // For 'laravel' docs, it will be generated to storage/app/scribe/collection.json. + // Setting `laravel.add_routes` to true (above) will also add a route for the collection. + 'postman' => [ + 'enabled' => true, + + 'overrides' => [ + // 'info.version' => '2.0.0', + ], + ], + + // Generate an OpenAPI spec (v3.0.1) in addition to docs webpage. + // For 'static' docs, the collection will be generated to public/docs/openapi.yaml. + // For 'laravel' docs, it will be generated to storage/app/scribe/openapi.yaml. + // Setting `laravel.add_routes` to true (above) will also add a route for the spec. + 'openapi' => [ + 'enabled' => true, + + 'overrides' => [ + // 'info.version' => '2.0.0', + ], + + // Additional generators to use when generating the OpenAPI spec. + // Should extend `Knuckles\Scribe\Writing\OpenApiSpecGenerators\OpenApiGenerator`. + 'generators' => [], + ], + + 'groups' => [ + // Endpoints which don't have a @group will be placed in this default group. + 'default' => 'Endpoints', + + // By default, Scribe will sort groups alphabetically, and endpoints in the order their routes are defined. + // You can override this by listing the groups, subgroups and endpoints here in the order you want them. + // See https://scribe.knuckles.wtf/blog/laravel-v4#easier-sorting and https://scribe.knuckles.wtf/laravel/reference/config#order for details + // Note: does not work for `external` docs types + 'order' => [], + ], + + // Custom logo path. This will be used as the value of the src attribute for the tag, + // so make sure it points to an accessible URL or path. Set to false to not use a logo. + // For example, if your logo is in public/img: + // - 'logo' => '../img/logo.png' // for `static` type (output folder is public/docs) + // - 'logo' => 'img/logo.png' // for `laravel` type + 'logo' => false, + + // Customize the "Last updated" value displayed in the docs by specifying tokens and formats. + // Examples: + // - {date:F j Y} => March 28, 2022 + // - {git:short} => Short hash of the last Git commit + // Available tokens are `{date:}` and `{git:}`. + // The format you pass to `date` will be passed to PHP's `date()` function. + // The format you pass to `git` can be either "short" or "long". + // Note: does not work for `external` docs types + 'last_updated' => 'Last updated: {date:F j, Y}', + + 'examples' => [ + // Set this to any number to generate the same example values for parameters on each run, + 'faker_seed' => 1234, + + // With API resources and transformers, Scribe tries to generate example models to use in your API responses. + // By default, Scribe will try the model's factory, and if that fails, try fetching the first from the database. + // You can reorder or remove strategies here. + 'models_source' => ['factoryCreate', 'factoryMake', 'databaseFirst'], + ], + + // The strategies Scribe will use to extract information about your routes at each stage. + // Use configureStrategy() to specify settings for a strategy in the list. + // Use removeStrategies() to remove an included strategy. + 'strategies' => [ + 'metadata' => [ + ...Defaults::METADATA_STRATEGIES, + ], + 'headers' => [ + ...Defaults::HEADERS_STRATEGIES, + Strategies\StaticData::withSettings(data: [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ]), + ], + 'urlParameters' => [ + ...Defaults::URL_PARAMETERS_STRATEGIES, + ], + 'queryParameters' => [ + ...Defaults::QUERY_PARAMETERS_STRATEGIES, + ], + 'bodyParameters' => [ + ...Defaults::BODY_PARAMETERS_STRATEGIES, + ], + 'responses' => configureStrategy( + Defaults::RESPONSES_STRATEGIES, + Strategies\Responses\ResponseCalls::withSettings( + only: ['GET *'], + // Recommended: disable debug mode in response calls to avoid error stack traces in responses + config: [ + 'app.debug' => false, + ] + ) + ), + 'responseFields' => [ + ...Defaults::RESPONSE_FIELDS_STRATEGIES, + ] + ], + + // For response calls, API resource responses and transformer responses, + // Scribe will try to start database transactions, so no changes are persisted to your database. + // Tell Scribe which connections should be transacted here. If you only use one db connection, you can leave this as is. + 'database_connections_to_transact' => [ +// config('database.default') + ], + + 'fractal' => [ + // If you are using a custom serializer with league/fractal, you can specify it here. + 'serializer' => null, + ], +]; diff --git a/database/factories/EventFactory.php b/database/factories/EventFactory.php index 41c6c10..9725eff 100644 --- a/database/factories/EventFactory.php +++ b/database/factories/EventFactory.php @@ -2,22 +2,45 @@ namespace Database\Factories; +use App\Models\Event; +use App\Models\Team; +use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Support\Str; +use Illuminate\Support\Carbon; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Event> + * @extends Factory */ class EventFactory extends Factory { - /** - * Define the model's default state. - * - * @return array - */ - public function definition(): array - { - return [ - // - ]; - } + protected $model = Event::class; + + public function definition(): array + { + $start = Carbon::now()->addDays($this->faker->numberBetween(1, 30))->setTime( + $this->faker->numberBetween(9, 18), + $this->faker->randomElement([0, 15, 30, 45]) + ); + + $end = (clone $start)->addHours($this->faker->numberBetween(1, 4)); + + return [ + 'uuid' => (string) Str::uuid(), + 'team_id' => Team::factory(), + 'user_id' => User::factory(), + 'name' => $this->faker->catchPhrase(), + 'nonce_valid_for_minutes' => 15, + 'hodl_asset' => false, + 'start_date_time' => $start, + 'end_date_time' => $end, + 'location' => $this->faker->city(), + 'event_start' => $start->format('H:i'), + 'event_end' => $end->format('H:i'), + 'event_date' => $start->toDateString(), + 'description' => $this->faker->paragraph(3), + 'bg_image_path' => null, + 'profile_photo_path' => null, + ]; + } } diff --git a/database/factories/PolicyFactory.php b/database/factories/PolicyFactory.php new file mode 100644 index 0000000..39df02f --- /dev/null +++ b/database/factories/PolicyFactory.php @@ -0,0 +1,27 @@ + + */ +class PolicyFactory extends Factory +{ + protected $model = Policy::class; + + public function definition(): array + { + return [ + 'hash' => Str::random(56), + 'name' => $this->faker->words(3, true) . ' Policy', + 'team_id' => Team::factory(), + 'user_id' => User::factory(), + ]; + } +} diff --git a/database/factories/TicketFactory.php b/database/factories/TicketFactory.php new file mode 100644 index 0000000..2b550ab --- /dev/null +++ b/database/factories/TicketFactory.php @@ -0,0 +1,29 @@ + + */ +class TicketFactory extends Factory +{ + protected $model = Ticket::class; + + public function definition(): array + { + return [ + 'event_id' => Event::factory(), + 'policy_id' => Policy::factory(), + 'asset_id' => $this->faker->bothify(str_repeat('#', 40)), // adjust length as needed + 'stake_key' => $this->faker->bothify(str_repeat('#', 40)), + 'signature_nonce' => random_bytes(16), // binary(16) + 'ticket_nonce' => random_bytes(16), // binary(16), nullable in schema but we'll fill it + 'signature' => null, + ]; + } +} diff --git a/database/migrations/2025_11_17_205744_add_is_public.php b/database/migrations/2025_11_17_205744_add_is_public.php new file mode 100644 index 0000000..bb7c9e9 --- /dev/null +++ b/database/migrations/2025_11_17_205744_add_is_public.php @@ -0,0 +1,32 @@ +boolean('is_public') + ->default(false) + ->after('profile_photo_path'); + + $table->index('is_public'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('events', function (Blueprint $table) { + $table->dropIndex(['is_public']); + $table->dropColumn('is_public'); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index a9f4519..b1968b3 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -2,21 +2,16 @@ namespace Database\Seeders; -// use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { - /** - * Seed the application's database. - */ - public function run(): void - { - // \App\Models\User::factory(10)->create(); + public function run(): void + { + // Any global seeding goes here, e.g. default roles/permissions/user, etc. - // \App\Models\User::factory()->create([ - // 'name' => 'Test User', - // 'email' => 'test@example.com', - // ]); + if (app()->environment('local')) { + $this->call(DemoEventSeeder::class); } + } } diff --git a/database/seeders/DemoEventSeeder.php b/database/seeders/DemoEventSeeder.php new file mode 100644 index 0000000..a689bfd --- /dev/null +++ b/database/seeders/DemoEventSeeder.php @@ -0,0 +1,274 @@ +first() ?? User::factory()->create([ + 'name' => 'Demo Organizer', + 'email' => 'demo@example.com', + 'password' => Hash::make('password'), // <- demo password + ]); + + // 2) Create a team for that user (or reuse first team) + $team = Team::query()->first() ?? Team::factory()->create([ + 'user_id' => $user->id, + 'name' => 'Demo Event Team', + 'personal_team' => true, + ]); + + // 3) Well-known Cardano policies we want to use in the demo + $knownPolicies = [ + [ + 'name' => 'HOSKY Token', + 'hash' => 'a0028f350aaabe0545fdcb56b039bfb08e4bb4d8c4d7c3c7d481c235', + ], + [ + 'name' => 'Clay Nation (NFT)', + 'hash' => '40fa2aa67258b4ce7b5782f74831d46a84c59a0ff0c28262fab21728', + ], + [ + 'name' => 'SpaceBudz (NFT)', + 'hash' => 'f0ff48bbb7bbe9d59a40f1ce90e9e9d0ff5002ec48f232b49ca0fb9a', + ], + [ + 'name' => 'World Mobile Token (WMT)', + 'hash' => '1d7f33bd23d85e1a25d87d86fac4f199c3197a2f7afeb662a0f34e1e', + ], + [ + 'name' => 'CLAY (Clay Nation FT)', + 'hash' => '38ad9dc3aec6a2f38e220142b9aa6ade63ebe71f65e7cc2b7d8a8535', + ], + ]; + + // 4) Seed (or re-use) those policies for this team/user + $policies = collect($knownPolicies)->map(function (array $p) use ($team, $user) { + return Policy::updateOrCreate( + [ + 'team_id' => $team->id, + 'hash' => $p['hash'], + ], + [ + 'name' => $p['name'], + 'user_id' => $user->id, + ] + ); + }); + + // 4) Create some demo events for this team/user + $eventDefinitions = [ + [ + 'name' => 'Hydra Launch Party', + 'description' => 'An exclusive event showcasing the Hydra-powered ticketing system.', + 'days_from' => 10, + 'hours_long' => 3, + 'location' => 'Las Vegas Convention Center', + 'ticket_window' => 'future', // ticketing not yet started + ], + [ + 'name' => 'Cardano Community Meetup', + 'description' => 'Monthly meetup for Cardano builders and enthusiasts.', + 'days_from' => 3, + 'hours_long' => 2, + 'location' => 'Phoenix Blockchain Hub', + 'ticket_window' => 'current', // ticketing currently open + ], + [ + 'name' => 'VIP Governance Summit', + 'description' => 'Invite-only strategy session for governance and scaling.', + 'days_from' => 21, + 'hours_long' => 4, + 'location' => 'Berlin Innovation Campus', + 'ticket_window' => 'closed', // ticketing already closed + ], + ]; + + $events = collect($eventDefinitions)->map(function (array $spec) use ($team, $user) { + $eventStart = Carbon::now() + ->addDays($spec['days_from']) + ->setTime(18, 0); // 6pm for all demo events + + $eventEnd = (clone $eventStart)->addHours($spec['hours_long']); + + $now = Carbon::now(); + switch ($spec['ticket_window']) { + case 'future': + // Ticketing opens in the future (not yet active) + $ticketStart = $now->copy()->addDays(2)->setTime(9, 0); + $ticketEnd = $now->copy()->addDays(5)->setTime(21, 0); + break; + + case 'closed': + // Ticketing already ended in the past + $ticketStart = $now->copy()->subDays(10)->setTime(9, 0); + $ticketEnd = $now->copy()->subDays(5)->setTime(21, 0); + break; + + case 'current': + default: + // Ticketing currently open (now is between start and end) + $ticketStart = $now->copy()->subDays(1)->setTime(9, 0); + $ticketEnd = $now->copy()->addDays(2)->setTime(21, 0); + break; + } + + return Event::factory() + ->for($team) + ->for($user) + ->create([ + 'name' => $spec['name'], + 'description' => $spec['description'], + 'location' => $spec['location'], + 'start_date_time' => $ticketStart, + 'end_date_time' => $ticketEnd, + 'event_start' => $eventStart->format('H:i'), + 'event_end' => $eventEnd->format('H:i'), + 'event_date' => $eventStart->toDateString(), + 'is_public' => true, + ]); + }); + + // 5) Attach policies to events via pivot + $events->each(function (Event $event) use ($policies) { + $event->policies()->sync( + $policies->random($policies->count() - 1)->pluck('id')->all() + ); + }); + + // 6) Create tickets per event + $events->each(function (Event $event) use ($policies) { + if ($event->policies()->count() === 0) { + $event->policies()->attach( + $policies->random(2)->pluck('id')->all() + ); + $event->refresh(); + } + + $eventPolicies = $event->policies; + + // 10 tickets per event, spread across policies + Ticket::factory() + ->count(10) + ->for($event) + ->state(function () use ($eventPolicies) { + return [ + 'policy_id' => $eventPolicies->random()->id, + ]; + }) + ->create(); + }); + + // 7) Create past events with simulated tickets & check-ins + $pastEventDefinitions = [ + [ + 'name' => 'Hydra Dev Workshop (Past)', + 'description' => 'Hands-on workshop that already happened.', + 'days_ago' => 7, + 'hours_long' => 3, + 'location' => 'Berlin', + ], + [ + 'name' => 'Governance Roundtable (Past)', + 'description' => 'Past governance roundtable with real attendance.', + 'days_ago' => 30, + 'hours_long' => 4, + 'location' => 'Online', + ], + ]; + + $pastEvents = collect($pastEventDefinitions)->map(function (array $spec) use ($team, $user) { + $eventStart = Carbon::now() + ->subDays($spec['days_ago']) + ->setTime(16, 0); + + $eventEnd = (clone $eventStart)->addHours($spec['hours_long']); + + // Ticketing window entirely in the past + $ticketStart = (clone $eventStart)->subDays(2); + $ticketEnd = (clone $eventStart)->subHours(1); + + return Event::factory() + ->for($team) + ->for($user) + ->create([ + 'name' => $spec['name'], + 'description' => $spec['description'], + 'location' => $spec['location'], + 'start_date_time' => $ticketStart, + 'end_date_time' => $ticketEnd, + 'event_start' => $eventStart->format('H:i'), + 'event_end' => $eventEnd->format('H:i'), + 'event_date' => $eventStart->toDateString(), + 'is_public' => true, + ]); + }); + + // Attach policies to past events + $pastEvents->each(function (Event $event) use ($policies) { + $event->policies()->sync( + $policies->random(2)->pluck('id')->all() + ); + }); + + // Create tickets + check-ins/check-outs for past events + $pastEvents->each(function (Event $event) use ($policies, $user) { + $eventPolicies = $event->policies; + + $tickets = Ticket::factory() + ->count(15) + ->for($event) + ->state(function () use ($eventPolicies) { + return [ + 'policy_id' => $eventPolicies->random()->id, + ]; + }) + ->create(); + + // ~70% of ticket holders check in, ~80% of those check out + foreach ($tickets as $ticket) { + if (rand(1, 100) <= 70) { + $checkinTime = $event->event_date + ->copy() + ->setTimeFromTimeString($event->event_start) // "16:00" → time on that date + ->addMinutes(rand(-30, 60)); + + $checkin = Checkin::create([ + 'ticket_id' => $ticket->id, + 'user_id' => $user->id, + 'created_at' => $checkinTime, + 'updated_at' => $checkinTime, + ]); + + if (rand(1, 100) <= 80) { + $checkoutTime = $checkinTime->copy()->addMinutes(rand(30, 180)); + + Checkout::create([ + 'ticket_id' => $ticket->id, + 'user_id' => $user->id, + 'created_at' => $checkoutTime, + 'updated_at' => $checkoutTime, + ]); + } + } + } + }); + + $this->command?->info('Demo events, policies, and tickets seeded.'); + $this->command?->info('Demo login: demo@example.com / password'); + } +} diff --git a/docker-compose.yml b/docker-compose.yml index c58288c..65e7cbb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,6 @@ services: - 'host.docker.internal:host-gateway' ports: - '${APP_PORT:-80}:80' - - '${VITE_PORT:-5173}:${VITE_PORT:-5173}' environment: WWWUSER: '${WWWUSER}' LARAVEL_SAIL: 1 @@ -27,6 +26,23 @@ services: - meilisearch - mailpit - selenium + vite: + image: node:20 + working_dir: /var/www/html + volumes: + - ./:/var/www/html + command: > + sh -c "if [ ! -d node_modules ]; then + echo '>> node_modules not found, running npm install...'; + npm install; + fi && + npm run dev -- --host 0.0.0.0 --port ${VITE_PORT:-5173}" + ports: + - '${VITE_PORT:-5173}:${VITE_PORT:-5173}' + networks: + - sail + depends_on: + - gatekeeper.app mysql: image: 'mysql/mysql-server:8.0' ports: diff --git a/package-lock.json b/package-lock.json index b692392..e5aad36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "GateKeeper-1.5", + "name": "html", "lockfileVersion": 3, "requires": true, "packages": { @@ -1156,6 +1156,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001629", "electron-to-chromium": "^1.4.796", @@ -1249,6 +1250,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.3.tgz", "integrity": "sha512-qK1gkGSRYcJzqrrzdR6a+I0vQ4/R+SoODXyAjscQ/4mzuNzySaMCd+hyVxitSY1+L2fjPD1Gbn+ibNqRmwQeLw==", + "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -2060,6 +2062,7 @@ "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "peer": true, "engines": { "node": "*" } @@ -2244,6 +2247,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", @@ -2787,6 +2791,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", "dev": true, + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -2920,6 +2925,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.1.tgz", "integrity": "sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==", "dev": true, + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.38", @@ -2984,6 +2990,7 @@ "version": "3.4.29", "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.29.tgz", "integrity": "sha512-8QUYfRcYzNlYuzKPfge1UWC6nF9ym0lx7mpGVPJYNhddxEf3DD0+kU07NTL0sXuiT2HuJuKr/iEO8WvXvT0RSQ==", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.4.29", "@vue/compiler-sfc": "3.4.29", diff --git a/package.json b/package.json index a263143..424d166 100644 --- a/package.json +++ b/package.json @@ -7,14 +7,11 @@ }, "devDependencies": { "@inertiajs/vue3": "^1.0.0", - "@tailwindcss/forms": "^0.5.2", - "@tailwindcss/typography": "^0.5.2", "@vitejs/plugin-vue": "^4.5.0", "autoprefixer": "^10.4.7", "axios": "^1.6.4", "laravel-vite-plugin": "^1.0.0", "postcss": "^8.4.14", - "tailwindcss": "^3.1.0", "vite": "^5.0.0", "vue": "^3.2.31" }, diff --git a/resources/js/Components/AppHeader.vue b/resources/js/Components/AppHeader.vue index 593def0..331a435 100644 --- a/resources/js/Components/AppHeader.vue +++ b/resources/js/Components/AppHeader.vue @@ -1,73 +1,103 @@ diff --git a/resources/js/Components/Banner.vue b/resources/js/Components/Banner.vue index de30c9c..f9b641b 100644 --- a/resources/js/Components/Banner.vue +++ b/resources/js/Components/Banner.vue @@ -1,6 +1,6 @@ diff --git a/resources/js/Components/NetworkNotice.vue b/resources/js/Components/NetworkNotice.vue new file mode 100644 index 0000000..941d471 --- /dev/null +++ b/resources/js/Components/NetworkNotice.vue @@ -0,0 +1,19 @@ + + diff --git a/resources/js/Layouts/AppLayout.vue b/resources/js/Layouts/AppLayout.vue index e23ac13..23b62c9 100644 --- a/resources/js/Layouts/AppLayout.vue +++ b/resources/js/Layouts/AppLayout.vue @@ -4,6 +4,7 @@ import {Head, Link, router} from '@inertiajs/vue3'; import ApplicationMark from '@/Components/ApplicationMark.vue'; import {useTheme} from "vuetify"; import Banner from "@/Components/Banner.vue"; +import NetworkNotice from "@/Components/NetworkNotice.vue"; defineProps({ title: String, @@ -66,121 +67,123 @@ footer a {