Commit Graph

17 Commits

Author SHA1 Message Date
e4c99e23e9 fix(app): collapse nested if in useImpersonationStore
WS-3 session 1b-iii follow-up — sonarjs/no-collapsible-if.

useImpersonationStore.ts:103: collapsed nested 'if (state.value)'
into the parent 'else if (data.data.session)' clause. Both legs
are AND-conditions on the same path, so the merge is semantically
identical. Brings the apps/app lint baseline to 0 problems.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 23:05:29 +02:00
b4f5bbe7c2 fix(app): resolve Bucket A/C/D lint items (trivial / style / Vuetify class)
WS-3 session 1b-ii Task 4 (audit Buckets A, C, D — 26 items resolved
this commit; 24 indent items in useTimeSlotDropdown.ts remain — see
deviations).

Bucket A — Trivial fixes (12 items resolved):
- A.1: second-pass eslint --fix on App.vue resolved 4 multi-attribute
  warnings. AppKpiCard / PortalLayout / PublicLayout
  lines-around-comment items were attempted via blank-line addition,
  but that introduced an equal number of vue/block-tag-newline
  errors (the rules conflict at the SFC <script>-tag boundary). The
  blank-line additions were reverted; net-zero, the 3 items remain
  for a 1b-iii .eslintrc.cjs override decision.
- A.3: 6 unused-imports / unused-vars manual deletes:
  * OrganisationSwitcher.vue: removed orphan toggleMenu() function
  * CreateShiftDialog.vue: removed unused 'scenario' from destructure
  * pages/events/[id]/time-slots/index.vue: removed unused 'event'
    slot scope binding (template <#default="{ event }"> → <#default>)
  * pages/organisation/companies.vue: removed unused authStore
    declaration + import
  * pages/platform/activity-log/index.vue: removed unused
    search/searchDebounced pair
  * PersonDetailPanel.vue:77: removed redundant single-statement
    if-braces (curly autofix that the original pass didn't reach)

Bucket C — Style preference (8 items resolved):
- DismissFailureDialog.vue:43: collapsed two consecutive `if cond return false`
  branches into `return !(cond)`
- FormFailureDetail.vue:44: replaced `void clipboard.writeText(...)` with
  `clipboard.writeText(...).catch(() => {})` — fire-and-forget with
  silent rejection (the no-void rule wants the void operator gone;
  .catch() handles it semantically).
- AssignShiftDialog.vue:40-46: hasOverlapWarning collapsed from
  always-false branching to `computed(() => false)` (the early-return
  was dead code; backend enforces the constraint).
- SectionsShiftsPanel.vue:333 + registration-fields.vue:335: rewrote
  `:delay-on-touch-only="true"` to attribute-shorthand `delay-on-touch-only`.
- AssignPersonDialog.vue:120-128: collapsed two `if outer { if inner ... }`
  pairs into single `if (outer && inner)` form (sonarjs/no-collapsible-if).
- useImpersonationStore.ts:99-104: collapsed the same nested-if pattern
  into `if (!data.data.active && state.value)`.

Bucket D — Vuetify utility class rename (5 items, 3 files):
- ml-1 → ms-1 (PersonDetailPanel:271, SectionsShiftsPanel:357,
  AssignPersonDialog:496)
- pl-4 → ps-4 (AssignPersonDialog:457)
- ml-auto → ms-auto (AssignPersonDialog:471)
LTR/RTL-aware Vuetify utilities, matching the Vuexy reference idiom.

Tests + typecheck verified green.

Lint baseline: 62 → 36.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 14:20:34 +02:00
d407cd17de fix(app): resolve Bucket B (type safety) lint items
WS-3 session 1b-ii Task 3 (audit Bucket B — 34 items: 21 absorbed
via ignorePatterns + 14 real fixes; the count of 21 is the actual
non-Tier-3 lint-count drop from the .eslintrc edit, slightly above
the audit's predicted 20 because additional vendored-Vuexy items
beyond the 23 no-explicit-any landed in those paths too).

Config:
- .eslintrc.cjs: add src/@core/** and src/@layouts/** to ignorePatterns.
  Vendored Vuexy code, precedent: src/plugins/iconify/*.js. The
  CLAUDE.md no-any rule remains in force for our own code under src/.

Real type-safety fixes:
- B.1 ref<any> in our code (3 occurrences):
  * blank.vue / default.vue: AppLoadingIndicator template ref now
    typed as InstanceType<typeof AppLoadingIndicator> | null. Picks
    up the defineExpose'd fallbackHandle / resolveHandle methods.
  * NavSearchBar.vue:109: useApi<any>(...) → useApi<SearchResults[]>(...)
    matching the existing searchResult ref type.
- B.2 ShiftDetailPanel.vue: moved the Cancel-dialog ref declarations
  (isCancelDialogOpen, cancellingAssignment) from line 305-307 to
  line 248 — directly above the onCancel handler that uses them.
  Resolves all 7 no-use-before-define items in one move. Same-file,
  no logic change.
- B.3 useImpersonationStore.ts:119: renamed inner 'stored' to
  'storedSnapshot' to resolve shadowing of the outer 'stored' on
  line 18.
- B.4 useFormSchemas.ts:97-99: renamed local mutationFn parameter
  'confirmed_name' to camelCase 'confirmedName'. Wire-format key
  stays snake_case via destructure-alias:
    params: confirmedName ? { confirmed_name: confirmedName } : undefined
  No callers found in apps/app/src — safe rename.

Tests + typecheck verified green.

Lint baseline: 97 → 62.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 14:11:05 +02:00
a7eaf0f948 style(app): apply eslint --fix to Tier 2 (TypeScript plumbing)
WS-3 session 1b-i Tier 2.

Scope: composables, lib, stores, plugins, types, utils, navigation,
main.ts. Mechanical fixes only — predominantly newline-before-return,
arrow-parens, antfu/if-newline, padding-line-between-statements, plus
one unicorn/prefer-includes (.some(p => x === p) → .includes(x))
in router guards.

Excludes (per session prompt):
- apps/app/vite.config.ts (Tier 3)
- apps/app/themeConfig.ts (Tier 3)
- apps/app/vitest.config.ts (Tier 3)
- All .vue files (already in Tier 1)

Hand-reviewed diffs for the three auth/router-critical files before
committing:
- src/lib/axios.ts: reviewed clean. Pure mechanical (quote-props on
  Accept header, curly-strip on single-statement ifs, one blank line
  before impersonationStore.clearState()). No type-import changes,
  no logic touched.
- src/stores/useAuthStore.ts: reviewed clean. curly-strip + padding
  before returns. The initialize()/doInitialize() race-condition guard
  on isInitialized is preserved verbatim.
- src/plugins/1.router/guards.ts: reviewed clean. if-newline reformat
  + one .some() → .includes() rewrite that's behaviorally identical
  for primitive equality on the guestOnlyPaths string array.

Tests + typecheck verified green post-fix:
- apps/app vitest: 49 passed (unchanged)
- apps/app vue-tsc: clean (unchanged)

Lint baseline progression:
- Pre-Tier-2: 422 problems (post-Tier-1)
- Post-Tier-2: 246 problems

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 11:06:46 +02:00
5771a678ef chore: install ts-reset in both portal and app SPAs
Installs @total-typescript/ts-reset 0.6.1 as a dev-dependency in
apps/portal/ and apps/app/. Patches TypeScript's loosest default
types: Array.filter(Boolean) returns non-nullable, JSON.parse
returns unknown, fetch().json() returns unknown, Map.get() strict,
etc.

Configuration: src/reset.d.ts in each SPA imports the reset. Both
tsconfig.json files already include ./src/**/* so the .d.ts is
picked up automatically — no tsconfig edits needed.

Issues surfaced during install:
  - apps/app — 0 pre-install tsc errors in own code; install
    surfaced 2 errors in src/stores/useImpersonationStore.ts
    (both from JSON.parse on sessionStorage content returning
    unknown instead of any). Fixed inline at lines 19 + 123 via
    `as ImpersonationState` casts that make the existing
    trust-in-sessionStorage explicit. Backlog entry
    TECH-TS-IMPERSONATION tracks proper runtime shape validation.
  - apps/portal — 22 pre-existing tsc errors in own code (mostly
    tiptap editor components — tracked as TECH-TS-PORTAL-TSC,
    unrelated to ts-reset). Zero new errors in portal's own code.
    4 additional errors surfaced in tiptap's uncompiled node_modules
    .ts sources (third-party); left as-is.

Neither SPA achieves `tsc --noEmit` clean today — pre-existing
state unrelated to this work package. Build + vitest are the
actual working gates and both remain green:
  - apps/portal: vitest 113/113 passing; production build succeeds
  - apps/app:    (no vitest setup — tracked as TECH-APP-VITEST);
                 production build succeeds

Documentation: /dev-docs/FRONTEND-TOOLING.md added; CLAUDE.md
quality-gates updated.

Backlog: TECH-TS-IMPERSONATION (runtime validation of stored
impersonation state), TECH-TS-PORTAL-TSC (pre-existing portal tsc
errors), TECH-APP-VITEST (Vitest coverage for apps/app).

No production behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 03:58:11 +02:00
4df668b5b8 feat: replace token-based impersonation with enterprise-grade header-based system
Replaces the insecure token-in-localStorage approach with a header-based
impersonation system backed by cache sessions and MFA verification.

Key changes:
- New impersonation_sessions audit table (immutable, ULID PK)
- MFA verification required to start impersonation (TOTP/email/backup)
- X-Impersonate-User header + HandleImpersonation middleware
- Per-request auth context swap (admin session never modified)
- IP pinning, sensitive route blocking, no nesting, sliding 60-min TTL
- Activity log auto-tagged with impersonated_by during sessions
- Frontend: sessionStorage, BroadcastChannel sync, countdown timer
- ImpersonateDialog with reason + MFA verification flow
- 26 comprehensive tests covering core, middleware, audit, lifecycle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 02:42:53 +02:00
c62f377668 fix: MFA setup completion not updating UI state
Root cause: the MFA status endpoint returned `mfa_enabled` as the JSON
key but the TypeScript MfaStatus interface expected `enabled`. At
runtime, `mfaStatus.value?.enabled` was always `undefined`, so
`isEnabled` was always false — the banner never hid and the method
cards never showed "Geconfigureerd".

Additionally, the auth store had no way to re-fetch /auth/me after
initialization, so `mfaSetupRequired` was never properly refreshed
from the backend after MFA setup.

Fixes:
- Rename `mfa_enabled` → `enabled` in the MFA status endpoint response
  to match the TypeScript type (and the /auth/me MeResource which
  already used `enabled`)
- Add `refreshUser()` to the auth store for post-initialization
  re-fetching of /auth/me
- Call `refreshUser()` in onSetupCompleted so the store reflects the
  backend state without a full page reload
- Update backend tests to match the renamed response key

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:30:58 +02:00
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
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
9e7f28420c feat: platform admin frontend — pages, composables, navigation, impersonation
Build the frontend for platform admin in apps/app/:
- TypeScript types (admin.ts) and API composable (useAdmin.ts) with
  TanStack Query for all admin endpoints
- ImpersonationStore (Pinia) + ImpersonationBanner component integrated
  in the main layout, with token-based session management
- Platform navigation section (conditionally shown for super_admin users)
- Route guard blocking /platform/* for non-super_admin users
- 6 pages: dashboard with stats cards, organisations list/detail,
  users list/detail with impersonation, activity log with expandable rows
- All pages implement loading/error/empty states per conventions
- Vite build passes cleanly

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 23:49:36 +02:00
11e379a5b9 feat: add useShiftDetailStore for shift detail panel state
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:04:17 +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
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
37fecf7181 fix: auth race condition on refresh, section edit dialog, time slot duplicate, autocomplete disable
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:16:22 +02:00
10bd55b8ae feat: festival/series model with sub-events, cross-event sections, tab navigation, SectionsShiftsPanel extraction
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:15:19 +02:00
c417a6647a feat(app): auth, orgs/events UI, router guards, and dev tooling
- Add Sanctum auth flow (store, composables, login, axios interceptors)
- Add dashboard, organisation list/detail, events CRUD dialogs
- Wire router guards, navigation, organisation switcher in layout
- Replace Vuexy @db types in NavSearchBar; add @iconify/types; themeConfig title typing
- Vuetify settings.scss + resolve configFile via fileURLToPath; drop dead path aliases
- Root index redirects to dashboard; fix events table route name
- API: DevSeeder + DatabaseSeeder updates; docs TEST_SCENARIO; corporate identity assets

Made-with: Cursor
2026-04-07 21:51:10 +02:00