From 5e2ede14b41fb2cb0148d5bb400dbf6878087e7f Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Mon, 30 Mar 2026 10:32:42 +0200 Subject: [PATCH] fix(admin): index redirect uses auth cookies and Spatie roles - Gate redirect on userData + accessToken; map org roles to events route - Keep legacy admin/client role redirects for compatibility - Rename organizer app HTML title to Event Crew - App - Add Cursor database rules (ULID, JSON, indexes, soft deletes) Made-with: Cursor --- .cursor/rules/103_database.mdc | 283 ++++++++++++++++++ .../src/plugins/1.router/additional-routes.ts | 26 +- apps/app/index.html | 2 +- 3 files changed, 305 insertions(+), 6 deletions(-) create mode 100644 .cursor/rules/103_database.mdc diff --git a/.cursor/rules/103_database.mdc b/.cursor/rules/103_database.mdc new file mode 100644 index 0000000..b98fc65 --- /dev/null +++ b/.cursor/rules/103_database.mdc @@ -0,0 +1,283 @@ +--- +description: Database schema conventions, ULID primary keys, JSON column rules, soft delete strategy, and index requirements for EventCrew +globs: ["api/database/**/*.php", "api/app/Models/**/*.php"] +alwaysApply: true +--- + +# Database & Schema Rules + +## Primary Keys: ULID (NOT UUID v4) + +All business tables use ULID (Universally Unique Lexicographically Sortable Identifier). UUID v4 is forbidden because it is random, causing B-tree index fragmentation in InnoDB. ULID is monotonically increasing (time-ordered). + +### Migration Pattern +```php +$table->ulid('id')->primary(); +``` + +### Model Pattern +```php +use Illuminate\Database\Eloquent\Concerns\HasUlids; + +class Event extends Model +{ + use HasUlids; +} +``` + +### Foreign Keys +```php +$table->foreignUlid('organisation_id')->constrained()->cascadeOnDelete(); +$table->foreignUlid('event_id')->constrained()->cascadeOnDelete(); +$table->foreignUlid('location_id')->nullable()->constrained()->nullOnDelete(); +``` + +### Exception: Pure Pivot Tables +Pure pivot tables (no business logic, only joins) use auto-increment integer PK for join performance: +```php +// organisation_user, event_user_roles, crowd_list_persons, access_zone_days, etc. +$table->id(); // auto-increment integer +``` + +## JSON Column Rules + +### ALLOWED for JSON +- Opaque configuration: `settings`, `blocks`, `fields` +- Toggle-sets and item lists: `items`, `milestone_flags` +- Free-text arrays: `top_feedback` +- Conditional logic definitions: `conditional_logic` + +### NEVER use JSON for +- Dates or periods (use separate columns or pivot tables) +- Status values (use enum columns) +- Foreign keys or references (use FK columns) +- Boolean flags (use bool columns) +- Anything that needs to be filtered, sorted, or aggregated in queries + +### Example: Access Zone Days +```php +// ❌ WRONG: JSON array of dates +$table->json('active_days'); // Can't index or query efficiently + +// ✅ CORRECT: Separate pivot table +Schema::create('access_zone_days', function (Blueprint $table) { + $table->id(); + $table->foreignUlid('access_zone_id')->constrained()->cascadeOnDelete(); + $table->date('day_date'); + $table->unique(['access_zone_id', 'day_date']); + $table->index('day_date'); +}); +``` + +## Soft Delete Strategy + +### Soft Delete ON (recoverable business records) +- `organisations` +- `events` +- `festival_sections` +- `shifts` +- `shift_assignments` +- `persons` +- `artists` +- `companies` +- `production_requests` + +```php +use Illuminate\Database\Eloquent\SoftDeletes; + +class Event extends Model +{ + use SoftDeletes; +} + +// Migration: +$table->softDeletes(); +``` + +### Soft Delete OFF (immutable audit records) +These are historical records that must never be modified or hidden: +- `check_ins` (who checked in, when, by whom) +- `briefing_sends` (delivery tracking) +- `message_replies` (communication audit) +- `shift_waitlist` (position tracking) +- `volunteer_festival_history` (coordinator assessments) +- `show_day_absence_alerts` (operational audit) + +## Required Composite Indexes + +All list queries must target < 50ms. These composite indexes are mandatory: + +### persons +```php +$table->index(['event_id', 'crowd_type_id', 'status']); +$table->index(['email', 'event_id']); +$table->index(['user_id', 'event_id']); +``` + +### shift_assignments +```php +$table->index(['shift_id', 'status']); +$table->index(['person_id', 'status']); +$table->unique(['person_id', 'time_slot_id']); // DB-enforced conflict detection +``` + +### check_ins +```php +$table->index(['event_id', 'person_id', 'scanned_at']); +$table->index(['event_id', 'scanned_at']); +``` + +### briefing_sends +```php +$table->index(['status', 'briefing_id']); +$table->index(['person_id']); +``` + +### shift_waitlist +```php +$table->unique(['shift_id', 'person_id']); +$table->index(['shift_id', 'position']); +``` + +### performances (B2B detection via overlap query) +```php +$table->index(['stage_id', 'date', 'start_time', 'end_time']); +``` + +### events +```php +$table->index(['organisation_id', 'status']); +``` + +### festival_sections +```php +$table->index(['event_id', 'sort_order']); +``` + +### time_slots +```php +$table->index(['event_id', 'person_type', 'date']); +``` + +### shifts +```php +$table->index(['festival_section_id', 'time_slot_id']); +$table->index(['time_slot_id', 'status']); +``` + +## Migration Ordering + +Migrations must be created in dependency order. Foundation tables first, then dependent tables: + +1. `users` (update: add timezone, locale, deleted_at) +2. `organisations` +3. `organisation_user` (pivot) +4. `user_invitations` +5. `events` +6. `event_user_roles` (pivot) +7. `locations` +8. `crowd_types` +9. `companies` +10. `festival_sections` +11. `time_slots` +12. `persons` +13. `shifts` +14. `shift_assignments` +15. `volunteer_availabilities` +16. `shift_waitlist` +17. `accreditation_categories` +18. `accreditation_items` +19. `event_accreditation_items` +20. `accreditation_assignments` +21. `access_zones` + `access_zone_days` + `person_access_zones` +22. `stages` + `stage_days` +23. `artists` +24. `performances` +25. `advance_sections` + `advance_submissions` +26. `briefing_templates` + `briefings` + `briefing_sends` +27. `public_forms` + `form_submissions` +28. `check_ins` +29. `scanners` + +## Migration Template + +```php +ulid('id')->primary(); + + // Foreign keys with constraints + $table->foreignUlid('festival_section_id')->constrained()->cascadeOnDelete(); + $table->foreignUlid('time_slot_id')->constrained()->cascadeOnDelete(); + $table->foreignUlid('location_id')->nullable()->constrained()->nullOnDelete(); + + // Business columns + $table->unsignedSmallInteger('slots_total'); + $table->unsignedSmallInteger('slots_open_for_claiming')->default(0); + $table->string('status')->default('open'); + $table->json('events_during_shift')->nullable(); // opaque config = OK for JSON + + // Timestamps + soft delete + $table->timestamps(); + $table->softDeletes(); + + // Composite indexes (from design document section 3.5) + $table->index(['festival_section_id', 'time_slot_id']); + $table->index(['time_slot_id', 'status']); + }); + } + + public function down(): void + { + Schema::dropIfExists('shifts'); + } +}; +``` + +## Audit Logging + +Use Spatie laravel-activitylog on these models: +- `persons` +- `accreditation_assignments` +- `shift_assignments` +- `check_ins` +- `production_requests` + +```php +use Spatie\Activitylog\Traits\LogsActivity; +use Spatie\Activitylog\LogOptions; + +class Person extends Model +{ + use LogsActivity; + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logOnly(['status', 'is_blacklisted', 'crowd_type_id']) + ->logOnlyDirty(); + } +} +``` + +## Out of Scope (Do NOT create tables for) + +These features are explicitly excluded from v1.x. No placeholders, no empty JSON fields, no migrations: +- Travel management (flights, buses) +- Accommodation management (hotels) +- Travel Party management +- CO2/sustainability reporting +- Native mobile apps +- Ticket sales (own ticketing system) diff --git a/apps/admin/src/plugins/1.router/additional-routes.ts b/apps/admin/src/plugins/1.router/additional-routes.ts index 17ee435..56f5a2e 100644 --- a/apps/admin/src/plugins/1.router/additional-routes.ts +++ b/apps/admin/src/plugins/1.router/additional-routes.ts @@ -10,16 +10,32 @@ export const redirects: RouteRecordRaw[] = [ path: '/', name: 'index', redirect: to => { - // TODO: Get type from backend const userData = useCookie | null | undefined>('userData') - const userRole = userData.value?.role + const accessToken = useCookie('accessToken') + const isLoggedIn = !!(userData.value && accessToken.value) - if (userRole === 'admin') + if (!isLoggedIn) + return { name: 'login', query: to.query } + + // Laravel API + Spatie: `roles` is string[] (e.g. super_admin, org_admin) + const roles = Array.isArray(userData.value?.roles) + ? (userData.value!.roles as string[]) + : [] + const legacyRole = userData.value?.role as string | undefined + + if (legacyRole === 'admin') return { name: 'dashboards-crm' } - if (userRole === 'client') + if (legacyRole === 'client') return { name: 'access-control' } - return { name: 'login', query: to.query } + const isOrgUser = roles.some(r => + ['super_admin', 'org_admin', 'org_member', 'org_readonly'].includes(r), + ) + if (isOrgUser) + return { name: 'events' } + + // Authenticated but unexpected role payload — avoid redirect loop back to login + return { name: 'events' } }, }, { diff --git a/apps/app/index.html b/apps/app/index.html index 0ab0b07..88143aa 100644 --- a/apps/app/index.html +++ b/apps/app/index.html @@ -6,7 +6,7 @@ - Event Crew - Band Portal + Event Crew - App