Commit Graph

39 Commits

Author SHA1 Message Date
79b7fe0b42 feat: account settings with Vuexy tab pattern and MFA banner fix
Restructures account/profile pages to match Vuexy's account-settings
tab pattern (Account, Security, Notifications) and fixes the MFA
enforcement banner that stayed visible after successful setup.

Backend:
- Add phone column to users table with migration
- Add PUT /me/profile endpoint for profile updates
- Create UpdateProfileRequest form request
- Update MeResource to include phone field

Organizer app:
- Rewrite account-settings as tabbed page (VTabs pill style + VWindow)
- Create AccountTab: avatar, profile form, email change, danger zone
- Create SecurityTab: password change, MFA method cards, backup codes,
  trusted devices, disable MFA danger zone
- Create NotificationsTab: placeholder with disabled toggles
- Fix MFA banner: set authStore.mfaSetupRequired = false on setup complete
- Update router guard to redirect to ?tab=security for MFA enforcement
- Update UserProfile menu links to use tab query params

Portal:
- Restructure profiel.vue with VTabs (Mijn profiel + Beveiliging)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:18:16 +02:00
cd2c775692 fix: eliminate all TypeScript any usage across Vue components
Replace 24 `err: any` error handler types with proper `AxiosError<ApiErrorResponse>`
typing. Fix additional `as any` casts and `Record<string, any>` patterns in registration
field components, event settings, and portal layout. Create shared `ApiErrorResponse`
type for portal app.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:54:01 +02:00
0be2956ea4 feat: MFA frontend with auth page restyling, challenge screen, and setup wizard
- Restyle organizer auth pages: Dutch text, remove placeholder social login
- Restyle portal auth pages to Vuexy v1 centered card pattern with decorative shapes
- MFA challenge card component with VOtpInput, method tabs, backup code input,
  trusted device checkbox, and session countdown timer
- Login pages handle mfa_required response with device fingerprint header
- Security settings page with TOTP setup (QR code), email setup, disable MFA,
  backup codes regeneration, and trusted devices management
- Portal profile page includes MFA security section
- Admin user detail page shows MFA status with reset button
- MFA enforcement route guard redirects to security settings when required
- Device fingerprint utility for trusted device identification
- MFA types, composables with TanStack Query for both apps

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:32:17 +02:00
c64875b6ef feat: align Vuexy primary with demo teal (rgb 13,147,148)
Made-with: Cursor
2026-04-15 19:43:14 +02:00
15be8a09c6 fix: remove duplicate 'Powered by Crewli' footer on registration pages
The portal layout already renders the footer — the inline copies in
[eventSlug].vue and success.vue caused it to appear twice.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 08:52:38 +02:00
821bfc5bcf chore: standardize dev-only debug logging across all three SPAs
Router guards:
- apps/app: added DEV-gated logging matching admin pattern
  (route info, auth decisions, org selection, access granted/denied)
- apps/portal: added DEV-gated logging matching admin pattern
  (route info, auth decisions, backward-compat redirects)
- apps/admin: already had full logging (unchanged)

Ungated console statements fixed:
- admin/main.ts: error handler, plugin registration, mount errors
- admin/pages/login.vue, register.vue: catch block errors
- admin/pages/events/index.vue: fetch error logging
- admin/pages/wizard-examples: demo form submit logging
- admin/pages/faq.vue: catch block error

All console statements in Crewli-authored code are now gated behind
import.meta.env.DEV — zero console output in production builds.
Vuexy template demo files (views/demos/*) left as-is.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:43:31 +02:00
8ace0480ae fix: handle 401 gracefully in auth initialization after httpOnly migration
Race condition: the axios 401 interceptor uses a dynamic import, so
handleUnauthorized() fires AFTER doInitialize() sets isInitialized=true.
handleUnauthorized() then reset isInitialized to false, leaving the app
stuck on a loading spinner with no way to recover.

Fix: remove isInitialized=false from handleUnauthorized() in all three
apps. When handleUnauthorized() redirects via window.location.href, all
JS state resets naturally. When it skips the redirect (already on a
public page like /login), the app should render normally in an
unauthenticated state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:24:58 +02:00
513ca519b2 security: migrate auth tokens to httpOnly cookies (hybrid bearer token approach)
Backend:
- CookieBearerToken middleware reads httpOnly cookie and injects Authorization
  header before Sanctum validates (prepended to API middleware group)
- SetAuthCookie trait provides cookie creation/expiry helpers with per-app
  cookie names (crewli_admin_token, crewli_app_token, crewli_portal_token)
- LoginController sets token via Set-Cookie, removes it from JSON body
- LogoutController expires the auth cookie on logout
- AuthRefreshController (POST /auth/refresh) rotates tokens with new cookie
- InvitationController accept also sets token via cookie, not JSON body
- All cookies: httpOnly, SameSite=Strict, Secure (in production)

Frontend (all three SPAs):
- Removed all localStorage token storage (apps/app, apps/portal)
- Removed all JS-readable cookie token storage (apps/admin)
- Removed Authorization: Bearer header interceptors from axios
- Auth stores now rely on GET /auth/me to validate httpOnly cookie
- Admin app: new Pinia auth store replaces useCookie-based auth pattern
- withCredentials: true ensures browser sends cookies automatically

Fixes security findings A13-1 (localStorage tokens) and A13-2 (admin
cookie flags). Tokens are now invisible to JavaScript.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:06:44 +02:00
836cffa232 feat: password reset, email change with verification, and password change
Password reset: multi-app support with custom notification linking to correct
frontend (app/portal/admin). Email change: self-service with password
confirmation and admin-initiated, both sending verification to new address
with 24h expiry. Confirmation sent to old email on completion. Password
change: authenticated endpoint revoking other sessions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:38:54 +02:00
53100d4f6d feat: portal cross-event my-shifts endpoint and dashboard page
GET /portal/my-shifts aggregates shift assignments across all events
the logged-in user is linked to via Person records. Groups by event
then date, showing only active assignments (approved/pending_approval)
for approved/pending persons.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:07:08 +02:00
d4004c798c feat: show identity match hint on registration success page
When a pending identity match is detected after volunteer registration,
the API now returns has_existing_account in the response. The success
page shows a login suggestion card so the volunteer can link their
registration to their existing account.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 08:56:25 +02:00
b8286d6a84 security: round 4 — frontend hardening (deps, XSS, cookie security)
Vulnerable dependencies upgraded:
- Backend: league/commonmark >=2.8.2 (HTML injection bypass),
  phpunit/phpunit >=11.5.50, laravel/tinker (psysh LPE)
- Frontend: axios 1.13→1.15 (SSRF + metadata exfiltration),
  @casl/ability updated (prototype pollution)
- Removed swiper from all 3 apps (prototype pollution CVE,
  only used in Vuexy demo pages)

XSS vectors removed:
- Deleted Vuexy demo pages with v-html rendering API data:
  help-center/article, academy/course-details
- Deleted all front-pages (landing, pricing, checkout, payment) —
  Vuexy marketing template, not Crewli business logic
- Deleted swiper demo components and views
- Fixed admin main.ts: replaced innerHTML with template literal
  with safe DOM construction using textContent

Cookie security:
- Added SameSite=Strict and Secure flags to admin cookie defaults

Cleanup:
- Removed swiper SCSS from all 3 apps
- Removed swiper custom element config from all 3 vite configs
- Portal localStorage cleanup verified: reset() clears all keys,
  called on both explicit logout and 401 interceptor

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 07:15:00 +02:00
1028498705 security: round 1 — quick wins (rate limiting, headers, mass assignment, logging)
- Add throttle middleware to login (5/min), portal/token-auth (10/min),
  volunteer-register (5/min), and invitation routes (10/min)
- Set Sanctum token expiration to 7 days
- Remove billing_status from UpdateOrganisationRequest (super_admin only)
- Revoke all Sanctum tokens on password reset
- Strengthen password rules: min 8 chars, mixed case, numbers
- Create SecurityHeaders middleware (X-Content-Type-Options, X-Frame-Options,
  HSTS, Referrer-Policy, Permissions-Policy)
- Fix open redirect on all 3 login pages (validate ?to= starts with /)
- Set APP_DEBUG=false in .env.example
- Log failed login attempts with email, IP, user-agent
- Log authorization failures (403) with user, IP, path, method
- Harden mass assignment: remove user_id from Person, audit fields from
  ShiftAssignment, system fields from UserInvitation $fillable
- Replace real DB records with factory make() in mail preview routes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 01:34:51 +02:00
ef195a6777 feat(mail): center-align action button in email template
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 00:49:41 +02:00
f9faeb7ea0 feat(portal): restructure into three-screen architecture with event tabs
Replace scattered dashboard pages with a three-screen volunteer portal:

1. Mijn evenementen (/evenementen) - landing page with visual event cards
   in a responsive grid, sorted upcoming-first
2. Event-pagina (/evenementen/:eventId) - single page with hash-based tabs
   (Overzicht, Mijn rooster, Diensten claimen, Informatie) replacing the
   old separate dashboard/my-shifts/claim-shifts pages
3. Mijn profiel (/profiel) - unchanged, platform-level settings

Key changes:
- Extract page content into tab components (RoosterTab, ClaimenTab,
  OverzichtTab, InformatieTab) that receive eventId as prop
- Dual-mode navbar: platform mode (Crewli logo) vs event mode (org name
  + event name + back link)
- StatusCard now emits switchTab events instead of route navigation
- Smart login redirect: 1 event → direct to event, 2+ → overview
- Backward-compat redirects for /dashboard/* → /evenementen
- Delete EventSwitcher (replaced by events overview page)
- Update UserAvatarMenu with "Mijn evenementen" link

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:30:20 +02:00
2d7464e05b feat(portal): horizontal navbar layout with avatar menu and profile restructuring
Replace the simple inline header with a proper Vuexy-style horizontal
navbar featuring left (logo + event switcher), center (conditional menu
items based on approval status), and right (avatar dropdown with profile
link and logout) sections. Move profile page from /profile to /profiel
as a platform-level page with "Mijn evenementen" section, removing the
event-scoped status card and remarks field. Registration and success
pages now use the portal layout with hideEventMenu meta so they get the
navbar when logged in but no event menu items.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:44:21 +02:00
59ad09fad2 feat(portal): auth persistence, shift visibility, profile page, and UI polish
- Fix session persistence: add loading state to App.vue, hydrate portal store
  in router guards so page refresh preserves auth + event context
- Fix shift visibility for festivals: query child event time slots so shifts
  on sub-events appear in the portal
- Add profile page with editable personal info and password change
- Add backend endpoints: PUT /portal/profile and PUT /portal/password
- Fix registration form: make first_name/last_name editable for logged-in users
- Restyle login page: remove Vuexy illustration, center form with Crewli branding
- Improve dashboard StatusCard with action cards, icons, and upcoming shift count
- Enhance shift cards with status border colors and availability progress bars

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:19:14 +02:00
8f1b53130b fix(portal): separate login from navigation error handling
Move router.replace() outside the login try/catch so navigation
failures (e.g. stale Vite HMR dynamic imports) don't mask a
successful login. Falls back to window.location on nav error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 08:55:38 +02:00
5173f7297f feat(portal): shift claiming and my-shifts for volunteer portal
Backend: PortalShiftController with 4 endpoints (available-shifts,
my-shifts, claim, cancel) delegating to ShiftAssignmentService.
24 PHPUnit tests covering happy paths, auth, conflicts, and edge cases.

Frontend: claim-shifts and my-shifts pages with TanStack Query
composable, conflict detection, confirmation dialogs, and cancel flow.
Navigation and dashboard cards wired up for approved volunteers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 08:47:12 +02:00
0d5523dbfe fix(portal): access ref value in login form submission
form is a ref() — in <script setup>, .value is required to access
the inner object. form.email was undefined at runtime, causing a
TypeError that was caught and shown as the generic login error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 08:30:51 +02:00
34eb790b3e feat(portal): login, dashboard, event switcher, password reset flow
Made-with: Cursor
2026-04-13 00:52:23 +02:00
de8ebf724b feat(portal): adaptive email check and passwords on volunteer registration
Made-with: Cursor
2026-04-13 00:42:33 +02:00
4df82d8358 feat(portal): registration API fields, field_values, conditional steps
Wire registration_fields and event toggles into the existing wizard;
replace hardcoded step content with dynamic controls; submit
field_values and festival_section_id section_preferences; map 422
field_values.* to inline errors.

Made-with: Cursor
2026-04-13 00:17:58 +02:00
63e2e5bed7 Revert "feat(portal): dynamic volunteer registration fields and conditional steps"
This reverts commit e986e4c7eb.
2026-04-13 00:15:12 +02:00
e986e4c7eb feat(portal): dynamic volunteer registration fields and conditional steps
Render registration_fields from the API grouped by section, submit
field_values and festival_section_id-based section_preferences.
Respect event toggles for section preferences and availability; polish
layout (header, welcome v-html, section cards, navigation).

Made-with: Cursor
2026-04-12 23:59:16 +02:00
2102a35688 fix(portal): remove unnecessary scrollbars on registration availability step
Vuetify VList defaults to overflow:auto; override to visible for the
availability list. Coerce duration_hours to number when summing hours
so API decimal strings do not string-concatenate.

Made-with: Cursor
2026-04-12 19:56:37 +02:00
d06cb01726 refactor: remove access_requirements (toegangsbehoeften) from application
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:53:28 +02:00
6dccf87234 feat: add date_of_birth field to persons across all layers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 09:06:29 +02:00
d2f282eb4c feat: split name into first_name + last_name across users, persons, and companies
Cross-cutting migration affecting the entire stack:
- Database: 3 migrations splitting name columns with data migration
- Models: first_name/last_name on User, Person; contact_first_name/contact_last_name on Company; backward-compatible name accessors
- API: all resources return first_name, last_name, full_name; assignablePersons endpoint updated
- Requests: validation rules updated for all person/user/company forms
- Services: VolunteerRegistrationService, ShiftAssignmentService, InvitationService updated
- Frontend: TypeScript types, Zod schemas, all forms split into Voornaam/Achternaam fields
- Display: all person/user name references use full_name; initials use first_name[0]+last_name[0]
- Tests: all 371 tests passing
- Docs: SCHEMA.md and API.md updated

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 23:04:55 +02:00
2764d3476f fix(portal): widen registration wizard sidebar from md-3 to md-4
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 22:07:29 +02:00
9e4e0c3d4b fix(portal): match wizard sidebar exactly to Vuexy vertical wizard spec
- Remove "AANMELDEN" header text from sidebar
- Increase number squares to 50x50px with borderRadius 10px (not circles)
- Show subtitles on ALL steps (not just active), with dimmed color per state
- Active step: bold title, default color; completed: medium-weight, 0.5 opacity;
  future: medium-weight, 0.4 opacity
- Increase check icon to size 22, number font to 18px bold
- Update sidebar padding to pa-5 pt-8
- Add h-100 to right card, increase content padding to pa-8
- Guard goToStep click handler with index check

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 21:57:33 +02:00
dfe7a63ad3 fix(portal): match registration wizard sidebar to Vuexy vertical wizard pattern
- Replace bg-primary block on active step with Vuexy number-square pattern:
  40x40 rounded-lg squares with primary/tonal/grey backgrounds per state
- Remove outer VCard wrapper so sidebar sits on page background
- Wrap right content column in its own VCard for clear visual separation
- Use text-overline for "AANMELDEN" label, 28px gap between steps
- Active step: primary square + primary text + subtitle visible
- Completed steps: tonal primary square with checkmark + muted text
- Future steps: grey square + muted text, no subtitle
- Mobile chips wrapped in their own card for consistency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 21:45:26 +02:00
0d741550a8 feat: event registration branding with vertical wizard layout
- Add registration_banner_url, registration_welcome_text, registration_logo_url
  columns to events table with migration
- Add uploadImage endpoint (POST .../upload-image) with form request validation
  for banner and logo images (jpg/png/webp, max 5MB)
- Include branding fields in EventResource and PublicRegistrationDataController
- Build registration settings UI in organizer event settings page with
  banner/logo upload and welcome text editor
- Redesign portal registration page: hero banner with gradient overlay,
  welcome text card, vertical step navigation (desktop) / horizontal chips
  (mobile), two-column form fields with density="comfortable"
- Update success page with event banner and consistent branding
- Seed welcome text for Echt Feesten 2026
- Add 9 PHPUnit tests covering image upload, branding fields in API responses

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 21:09:49 +02:00
212db0d3cb feat(portal): redesign registration wizard, header and login with Vuexy patterns
- Registration page: switch to blank layout with split-screen design (event branding
  illustration on left, AppStepper wizard on right), replacing generic chip stepper
- Portal header: refined to flat surface bar with proper branding and icon-based nav
- Login page: upgrade to Vuexy V2 auth pattern with split-screen illustration
- Success page: blank layout with centered card, avatar icon, and footer mask

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 20:47:24 +02:00
c21bc085e9 feat: registration section preferences with show_in_registration filtering and deduplication
Add show_in_registration and registration_description columns to festival_sections.
Registration form now shows deduplicated sections by name (across sub-events),
filtered by show_in_registration=true, grouped by category with card-based UI.
Section preferences use section_name instead of section_id.
Add GET/PUT registration-settings endpoints for festival-level bulk management.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 20:03:54 +02:00
3400e4cc7e feat(portal): multi-step volunteer registration form with public event endpoint
- Add GET /api/v1/public/events/{slug}/registration-data endpoint for fetching
  event sections and time slots without auth
- Create 5-step registration form: personal info, details, motivation, section
  preferences, availability
- VeeValidate + Zod validation per step with Dutch error messages
- Auth-aware: pre-fills name/email for authenticated users
- Mobile responsive with custom chip-based step indicator
- Success page with contextual actions (dashboard vs login)
- Types, composable (TanStack Query), and Zod schemas

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:41:20 +02:00
87f0bcce6e feat(portal): strip Vuexy demo content and create clean portal shell
Remove all demo pages, dialogs, sidebar navigation, and layout components.
Create minimal top-bar portal layout with auth-aware navigation, placeholder
pages for volunteer registration, dashboard, shifts, profile, artist advance,
and login. Add Pinia auth store, axios with Sanctum support, and router guards.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:38:55 +02:00
0d24506c89 feat: consolidate frontend API layer, add query-client, and harden backend Fase 1
Frontend:
- Consolidate duplicate API layers into single src/lib/axios.ts per app
- Remove src/lib/api-client.ts and src/utils/api.ts (admin)
- Add src/lib/query-client.ts with TanStack Query config per app
- Update all imports and auto-import config

Backend:
- Fix organisations.billing_status default to 'trial'
- Fix user_invitations.invited_by_user_id to nullOnDelete
- Add MeResource with separated app_roles and pivot-based org roles
- Add cross-org check to EventPolicy view() and update()
- Restrict EventPolicy create/update to org_admin/event_manager (not org_member)
- Attach creator as org_admin on organisation store
- Add query scopes to Event and UserInvitation models
- Improve factories with Dutch test data
- Expand test suite from 29 to 41 tests (90 assertions)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 17:35:34 +02:00
1cb7674d52 refactor: align codebase with EventCrew domain and trim legacy band stack
- Update API: events, users, policies, routes, resources, migrations
- Remove deprecated models/resources (customers, setlists, invitations, etc.)
- Refresh admin app and docs; remove apps/band

Made-with: Cursor
2026-03-29 23:19:06 +02:00