Commit Graph

491 Commits

Author SHA1 Message Date
1437829501 chore(backlog): close TECH-DOCS-APPS-PORTAL-PURGE
WS-3 PR-C delivered the per-file DELETE/REWRITE/KEEP_AND_PURGE
matrix on all 9 files referenced in the entry, plus the
out-of-scope post-edit-eslint.sh hook fix. Doc-rot removed:
~80 KB of obsolete bootstrap and prompt-template content.

Single SPA, single cookie, single deploy host. WS-3 complete.
2026-05-06 02:14:46 +02:00
d33c119d75 chore(docs): delete obsolete bootstrap and prompt-template docs
Five files removed, all describing project states that no longer
apply post-WS-TOOLING-001:

- .cursor/instructions.md (8.4 KB): Phase 1-4 roadmap with all
  checkboxes empty; Phase 1 has been done for ~6 months. Broken
  'make portal' target. Content overlaps with CLAUDE.md.
- .cursor/ARCHITECTURE.md (18.9 KB): pre-WS-3 framing (dual SPA,
  dual cookies, dual SANCTUM_STATEFUL_DOMAINS) AND pre-Form-Builder
  schema (volunteer_profiles, public_forms with JSON fields). All
  six sections superseded by SCHEMA.md, AUTH_ARCHITECTURE.md,
  design-document.md, API.md, 102_multi_tenancy.mdc.
- dev-docs/MASTER_PROMPT_CC.md (13 KB): 'paste this above every task'
  workflow superseded by auto-loaded CLAUDE.md and structured
  Claude Chat-authored prompts. Stale dual-SPA + pre-Form-Builder
  assumptions throughout.
- dev-docs/MASTER_PROMPT_CURSOR.md (7.5 KB): same workflow obsoletion;
  Cursor is now IDE-only (Claude Code does all implementation).
  .cursor/rules/ system handles IDE-level guidance.
- dev-docs/dev-guide.md (32 KB): bootstrap-from-scratch document
  containing embedded snapshots of pre-Form-Builder CLAUDE.md,
  pre-Form-Builder SCHEMA.md, pre-Form-Builder API.md as
  copy-paste templates. Section 5 prompts pre-WS-TOOLING-001 era.
  Section 6 (agents) overlaps with CLAUDE_CODE_TOOLING.md.

Total: ~80 KB doc-rot removed.

Cross-reference check found four files outside the deleted set
referencing the deleted paths; all updated in the same commit:

- README.md: Documentation table rebuilt around CLAUDE.md +
  dev-docs/* (also dropped stale resources/design/ row pointing
  at a directory that no longer exists, and corrected docs/*
  paths to dev-docs/*)
- dev-docs/CLAUDE_DESKTOP_SETUP.md: dropped MASTER_PROMPT_CC,
  MASTER_PROMPT_CURSOR, dev-guide entries from the
  bewust-verwijderd exclusion list; updated Gerelateerd pointer
  from dev-guide.md -> SETUP.md
- dev-docs/ARCH-CONSOLIDATION-2026-04.md: updated future-distribution
  pointer from dev-guide.md -> SETUP.md (sprint briefing is
  historical so the change is purely doc-hygiene)
- dev-docs/VIBE_CODING_CHECKLIST.md: removed Dev guide row from
  the bestandspaden table

Remaining references in dev-docs/BACKLOG.md (lines 862-869) live
inside the TECH-DOCS-APPS-PORTAL-PURGE entry that closes in the
next commit.

Canonical replacements: dev-docs/SETUP.md (rewritten this PR),
CLAUDE.md, CLAUDE_CODE_TOOLING.md, and the ARCH-*.md series.
2026-05-06 02:14:10 +02:00
2c4d2257ae chore(hooks): drop apps/portal scope from post-edit-eslint.sh
Three pre-WS-3 references purged: regex (apps/(app|portal)),
grep (apps/(app|portal)), and the now-obsolete "apps/portal/ is
planned but not present" defensive comment. The $spa variable
becomes redundant with only one SPA — collapsed to direct
apps/app/ references.

Net: simpler script, no behavioural change for actual files in
apps/app/ (still runs pnpm eslint --fix). Files outside apps/app/
were already a no-op.
2026-05-06 02:13:01 +02:00
bea66a58e6 chore(docs): purge apps/portal mention from CLAUDE_CODE_TOOLING.md
Single-line fix in the hooks reference table. The post-edit-eslint
hook used to scope to apps/app/ or apps/portal/; post-WS-3 there's
only apps/app/.

Code change in the hook script itself lands in the next commit.
2026-05-06 01:51:37 +02:00
451eab42ac chore(rules): purge apps/portal from 102_multi_tenancy.mdc
Surgical updates reflecting post-WS-3 single-SPA reality. The
OrganisationScope rules, three-level authorization, and invitation
flow are unchanged — they're still the canonical guidance.

Changes:
- globs: drop apps/portal/**/*.{vue,ts}
- Portal Architecture: "two access modes in apps/portal/" ->
  "two access modes under /portal/* routes within apps/app/"
- Token flow URL example: portal.crewli.app -> crewli.app/portal/
  with note about 301 redirect from legacy host
- Login flow URL: portal.crewli.app -> crewli.app
- CORS allowed_origins: drop FRONTEND_PORTAL_URL line
- Production example: collapse dual-host to single-host with
  pointer to AUTH_ARCHITECTURE.md §11 for the legacy env key
2026-05-06 01:51:18 +02:00
d82cf42728 chore(rules): rewrite 101_vue.mdc as slim principles file
Drops 17 KB of embedded code templates that had drifted from actual
implementations in apps/app/src/ (auth store template still used
localStorage; portal router guards still showed dual-mode logic that
was consolidated to /portal/* routes within apps/app in PR-B1/B2a).

Slim rewrite: principles + file structure + pointers to actual
reference code in apps/app/src/. Globs narrowed to apps/app/**/*
since apps/portal/ no longer exists. Vuexy component selection
deferred to dev-docs/VUEXY_COMPONENTS.md as canonical registry.

Net: ~17 KB -> ~3 KB, less drift surface, points at living code
instead of duplicating it.
2026-05-06 01:50:39 +02:00
5d4132785f chore(docs): rewrite SETUP.md as continue-existing-project guide
Replaces the bootstrap-from-scratch document (Step 2 told readers to
run 'composer create-project laravel/laravel api' on an existing
codebase) with a continue-existing-project guide.

Scope: prerequisites, first-time setup (clone + install + .env + migrate),
daily workflow (three terminals + optional queue worker), env-config
explained, common tasks (test/migrate/route:list/build), documentation
reference linking the dev-docs/ canonical files, troubleshooting.

Drops apps/portal references throughout (single SPA at port 5174).
Drops dual-port SANCTUM_STATEFUL_DOMAINS guidance. Replaces .cursor/
instructions reference with /CLAUDE.md as auto-loaded source of truth.
2026-05-06 01:50:01 +02:00
808ec212eb Merge pull request 'WS-3 PR-B2b: A13-3 + single-cookie + single-host (incl. flatpickr precursor)' (#6) from feat/ws-3-pr-b2b-single-cookie-deploy into main
Reviewed-on: #6
2026-05-06 01:16:05 +02:00
289e735fd6 chore(types): regenerate components.d.ts to sync with PR-B2a additions 2026-05-06 01:09:04 +02:00
eb485573ce chore(types): regenerate auto-imports.d.ts to sync with PR-B2a additions 2026-05-06 01:04:05 +02:00
ad23847050 fix(deps): import flatpickr CSS via JS, add flatpickr direct dep 2026-05-06 01:03:25 +02:00
7a69b03c78 chore(docs): drop apps/portal references from load-bearing files
Three load-bearing files still described the pre-WS-3 dual-SPA
reality. Surgical edits to reflect the single-SPA architecture
shipped in WS-3 PR-B (B1: portal moves; B2a: auth+routing
consolidation; B2b: server-side cookie consolidation).

CLAUDE.md:
- Quality-gates ts-reset bullet (line 27): "both SPAs" → "the SPA"
- Quality-gates Vitest bullet (lines 30-32): rewrite from "apps/portal
  has 113+ tests; apps/app currently has no Vitest setup (TECH-APP-VITEST)"
  to current truth: apps/app has Vitest with 213 tests as of PR-B2a.
  TECH-APP-VITEST is implicitly closed.
- Repository layout (line 44): drop apps/portal/ bullet; rephrase
  apps/app/ as the single workspace
- "Apps and portal architecture" → "App architecture": rewrite for
  single-workspace + two access modes. Login-based covers
  organizers + volunteers + crew + super_admin (context-routed
  in-app via useAuthStore.availableContexts); token-based covers
  artists, suppliers, press
- CORS subsection: collapse two-origin config to single origin
  (localhost:5174 dev, https://crewli.app prod). Preserve the
  existing crewli.nl marketing-only note

WS-TOOLING-001 sections (Larastan, Rector, Telescope tooling
configuration) verified untouched via `git diff CLAUDE.md`.

README.md (line 25): collapse the Applications table from two rows
(Organizer + Portal) to one (SPA). Adjust trailing sentence accordingly.

Makefile:
- .PHONY list: drop `portal`
- help echo: drop "make portal" line
- portal target: removed (the underlying `cd apps/portal && pnpm dev`
  would fail since the directory was removed in PR-B1)

Out of scope (deferred to TECH-DOCS-APPS-PORTAL-PURGE backlog item):
.cursor/ instructions, MASTER_PROMPT_*, dev-docs/SETUP, dev-docs/dev-guide,
dev-docs/CLAUDE_CODE_TOOLING. WS-3-SESSION-1C-AUDIT.md skipped (frozen
historical doc).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 00:30:53 +02:00
812cc17460 docs(auth): reflect single-cookie architecture; close A13-3
dev-docs/AUTH_ARCHITECTURE.md (v1.0 → v2.0):
- Title section updated to single-SPA / single-cookie reality
- Client Applications table collapsed to one row
- Cookie Specification table collapsed to one row (crewli_app_token)
- Token Lifecycle / Validation section: Origin-based resolution
  language removed; middleware described as origin-agnostic
- Cross-app isolation paragraph removed (no second app)
- Configuration Reference table marks FRONTEND_PORTAL_URL as legacy,
  pointing at TECH-FRONTEND-URL-CONSOLIDATE
- New §11 "History" preserves the pre-WS-3 dual-cookie context for
  future readers, mentions PR-B2a + PR-B2b roles in the unwind

dev-docs/BACKLOG.md — three new entries:
- TECH-FRONTEND-URL-CONSOLIDATE: refactor email controllers to drop
  per-app URL map (EmailChangeController, PasswordResetController,
  PersonController) — low priority, code-cleanliness only
- TECH-DOCS-APPS-PORTAL-PURGE: sweep apps/portal references from
  briefing/tooling docs (.cursor/, MASTER_PROMPT_*, SETUP, dev-guide,
  CLAUDE_CODE_TOOLING) — single chore(docs) PR, low priority
- OPS — DNS retirement of portal.crewli.app — operational task,
  deferred until traffic monitoring confirms zero usage

dev-docs/SECURITY_AUDIT.md:
- A13-1 narrative actualised: pre-WS-3 dual-cookie context kept as
  history, status flipped to RESOLVED (the localStorage→httpOnly
  migration shipped earlier in the consolidation arc)
- A13-3: status flipped to RESOLVED by WS-3 PR-B2b; description
  rewritten to reflect the new postLoginRedirect.ts validator and
  the 16 spec coverage
- Priority remediation table item 8 strikes through A13-3

Backend test suite: 1487 passed (unchanged from Commit 2 baseline).
Frontend: 223 passed (unchanged from Commit 1 baseline).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 00:29:26 +02:00
a748c9ee7a chore(deploy): single-host deploy config — drop apps/portal build, retire portal.crewli.app
deploy.sh referenced apps/portal which was deleted in WS-3 PR-B1; the
script has been broken in main since that merge (npm run build
-w apps/portal would fail). Collapse to a single-app build.

Changes:
- deploy.sh: replace dual-build block (build app + portal, verify both
  dist/) with single-app build (build app, verify dist/index.html)
- deploy/nginx/csp-portal.conf: deleted (content was identical to
  csp-spa.conf — verified before removal)
- deploy/README.md: replace "Portal (portal.crewli.app)" server-block
  section with "Legacy portal redirect" — a 301 server block
  template that redirects portal.crewli.app → crewli.app preserving
  the request URI. Notes that DNS retirement is a separate ops task

Out of scope: actually retiring the portal.crewli.app DNS record
(operational, tracked separately).

bash -n deploy.sh: clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 00:24:40 +02:00
2e94a107e4 refactor(auth): consolidate to single cookie post single-SPA
The dual-cookie machinery (crewli_app_token + crewli_portal_token,
Origin-based resolution) was load-bearing only when the second SPA
existed. apps/portal/ was deleted in WS-3 PR-B1; the resolver code
has been carrying dead branches since then. Collapse to one cookie.

Cookie name retained as crewli_app_token — no session breakage on
deploy. crewli_portal_token is fully purged from the server-side.

CookieBearerToken middleware:
- COOKIE_NAMES array → single COOKIE_NAME constant
- resolveCookieName method (Origin/Referer parsing, host+port
  matching against frontend_app_url/frontend_portal_url) → removed
- Body collapses to: skip if Authorization header present; else
  read crewli_app_token cookie and inject Bearer header

SetAuthCookie trait:
- COOKIE_MAP / resolveCookieName / originMatches → removed
- makeAuthCookie / forgetAuthCookie now take only $token; the
  cookie name is the trait's private constant

Five callers updated to drop the resolveCookieName($request) line
and the cookie-name argument: LoginController (3 sites),
MfaVerifyController (1 site), AuthRefreshController (1 site),
LogoutController (1 site), InvitationController (1 site — caller
list in the prompt missed this one but the same pattern applies).

frontend_portal_url config key retained (per Phase A directive Q1):
EmailChangeController, PasswordResetController, PersonController are
non-auth consumers that build per-app URL maps for outbound emails.
The map structure is now functionally redundant (production resolves
all FRONTEND_* env vars to the same host) but stays structurally
intact. Refactor tracked as TECH-FRONTEND-URL-CONSOLIDATE in the
upcoming docs commit.

HttpOnlyCookieAuthTest:
- Removed 4 dual-cookie tests (login_sets_portal_cookie_for_portal_origin,
  app_cookie_does_not_authenticate_portal_requests,
  portal_cookie_does_not_authenticate_app_requests,
  correct_cookie_authenticates_with_matching_origin)
- Renamed login_sets_app_cookie_for_unknown_origin →
  login_sets_app_cookie_regardless_of_origin; expanded to four
  Origin variants (none, app, unknown, foreign) — pins the new
  origin-agnostic contract
- Removed Origin headers from request calls in remaining tests
  (now meaningless)

Backend test count: 1491 → 1487 (-4 deleted, dual-cookie tests
encoding the obsolete contract). Pint clean. Larastan clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 00:24:01 +02:00
96cb1519de feat(security): full A13-3 open-redirect validation in postLoginRedirect
Replaces the WS-3 PR-B2a minimum precaution (`startsWith('/') &&
!startsWith('//')`) with a layered validator that rejects every input
that is not a strict relative path.

isSafeRelativePath rejects:
- Empty / null / undefined input
- Non-`/`-prefixed paths (including leading whitespace)
- Protocol-relative URLs (`//evil.com`)
- Backslash anywhere (browsers normalise `\` → `/` in some contexts;
  `/\evil.com` parses as `//evil.com`)
- ASCII control characters `\x00`–`\x1F` and `\x7F` (NUL, tab, LF, CR,
  DEL, etc. — header-injection vectors)
- Anything the URL constructor parses to a different origin than the
  synthetic invalid origin used as the resolution base

The URL-constructor check is the authoritative guard; the prefix and
character checks are fast pre-filters that short-circuit common
attack shapes without paying the URL allocation.

Test coverage expands from 6 → 16 cases. New cases pin the
backslash, control-character, leading-whitespace, and positive-
character-set contracts. The URL-encoded-slash-in-query case
documents that we don't false-positive on `%2F` in query strings.

Closes A13-3 (open-redirect on post-login).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 00:20:12 +02:00
538072241e docs(backlog): WS-TOOLING-001 done + 3 follow-up tech items 2026-05-06 00:08:09 +02:00
ad36c06208 Merge WS-TOOLING-001: Claude Code deterministic guard-rail layer 2026-05-05 23:58:32 +02:00
9ae3e6a757 chore(claude): ignore worktrees and macOS metadata 2026-05-05 23:58:00 +02:00
b1773ea1f3 docs(claude): document rm -rf rule in hooks reference table 2026-05-05 23:56:21 +02:00
735ba2c8d9 docs: add CLAUDE_CODE_TOOLING.md and cross-reference from CLAUDE.md
Documents the deterministic guard-rail layer: hooks reference,
crewli-reviewer subagent usage, three slash commands, how to test
hooks, how to disable temporarily, and the binding design principle
(settings/hooks deterministic, CLAUDE.md advisory — never duplicate).
2026-05-05 23:25:03 +02:00
05d1a6d31d chore(claude): add sprint-status, review-multitenancy, sync-docs commands
/sprint-status — branch, last package, uncommitted work, next BACKLOG item.
/review-multitenancy <Model> — model+migration+policy+tests checklist.
/sync-docs — runs the dev-docs sync pipeline and reminds to upload .claude-sync/.

Each command's frontmatter declares a least-privilege allowed-tools list.
2026-05-05 23:24:58 +02:00
ff4f9a9dbb chore(claude): add crewli-reviewer subagent
Isolated-context code review against the zero-compromise principles.
Read/Grep/Glob/Bash only — no Edit, so the reviewer cannot patch
code. Outputs MUST FIX / SHOULD FIX / CONSIDER, every finding cited
as path:line.
2026-05-05 23:24:52 +02:00
1e65a65b20 chore(claude): add SessionStart compact context injector
inject-sprint-context.sh fires on SessionStart with matcher=compact
and emits branch, last 10 commits, and the top of BACKLOG.md so
Claude resumes with sprint context after auto-compaction. Output
capped at ~600 tokens.
2026-05-05 23:24:46 +02:00
f7ef26d450 chore(claude): add pint and eslint PostToolUse hooks
post-edit-pint.sh runs vendor/bin/pint --dirty from api/ after any
.php edit. post-edit-eslint.sh runs pnpm eslint --fix inside the
matching SPA dir for .vue/.ts/.tsx/.js files under apps/app/ or
apps/portal/. Both exit 0 unconditionally — formatting failures must
not block the agent.
2026-05-05 23:24:41 +02:00
da42dbb2dd chore(claude): add protect-files and block-dangerous-bash PreToolUse hooks
protect-files.sh blocks Edit/Write to secrets, lock files, default
Laravel migrations, the deleted apps/admin/ tree, .claude/ itself,
and dev-docs/SCHEMA.md.

block-dangerous-bash.sh blocks destructive git operations, blanket
dependency updates, and database wipes that aren't scoped to the
testing environment.

Both signal block via exit 2 with a reason on stderr; both stay well
under 500ms per invocation.
2026-05-05 23:24:32 +02:00
18fb035c23 chore(claude): add settings.json with hook registry
Registers PreToolUse, PostToolUse, and SessionStart hooks for the
deterministic guard-rail layer. settings.local.json stays gitignored
for per-user overrides.
2026-05-05 23:24:08 +02:00
68f1e6f80c Merge pull request 'WS-3 PR-B2a: auth + routing consolidation (single SPA, dual axios, context-aware guards)' (#5) from feat/ws-3-pr-b2a-auth-routing-consolidation into main
Reviewed-on: #5
2026-05-05 22:43:52 +02:00
145d0cbdad docs(backlog): add ARCH-API-RESPONSE-VALIDATION workstream entry
Workstream-sized item geborgt voor uniforme typed + runtime-validated
contracts op de API-grens (backend PHP Enums, frontend Zod schemas,
codegen TS types). Scope, sequentie (post-PR-C/WS-7, pre-RFC-FORM-BUILDER-UI),
en open beslissingen vastgelegd. Verwijst naar dev-docs/ARCH-API-VALIDATION.md
skeleton voor architectuur-detail.

Voorkomt dat S3b technische schuld stapelt — landt vóór RFC-FORM-BUILDER-UI
zodat nieuwe composables vanaf dag één het gevalideerde patroon consumeren.
2026-05-05 22:32:05 +02:00
b3fb617985 chore(sync): track ARCH-API-VALIDATION.md in .claude-sync.conf 2026-05-05 22:17:45 +02:00
babbbd97cb docs(arch): add ARCH-API-VALIDATION.md skeleton — uniform API response validation workstream 2026-05-05 22:17:27 +02:00
b191fbe917 refactor(auth): migrate MfaChallengeCard to useAuthStore.verifyMfa
The card consumed the API directly via useVerifyMfa() (TanStack Query
mutation). Per Decision F's intent (store owns business logic, the
component consumes typed results), the card now calls
useAuthStore.verifyMfa() and pattern-matches on the MfaVerifyResult
discriminated union.

Changes:
- MfaChallengeCard: drop useVerifyMfa import; call authStore.verifyMfa
  with camelCase args (sessionToken, trustDevice, deviceFingerprint,
  deviceName); local isVerifying ref replaces verifyMutation.isPending.
  On result.kind === 'authenticated' emit `verified` (no payload —
  the store has already refreshed user state); on 'failed' surface
  result.reason with a generic fallback.
- emit signature: `verified: [data: unknown]` → `verified: []`.
- login.vue: onMfaVerified no longer calls authStore.refreshUser —
  authStore.verifyMfa() refreshes internally. Page just routes to
  resolvePostLoginTarget().

Adds 4 vitest specs in components/auth/__tests__/MfaChallengeCard.spec.ts
covering: success path emits `verified` with camelCase args, failure
path shows reason and suppresses emit, trustDevice toggle honours
fingerprint + device name, fallback message when reason is empty.

Test count 209 → 213. Lint + typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:01:32 +02:00
eb7f3eb057 fix(portal): consume portal events from useAuthStore instead of duplicate /auth/me fetch
The auth-store merge made portal_events available on the unified
/auth/me response (held in useAuthStore.portalEvents). usePortalStore
now sources userEvents from the auth store, eliminating the duplicate
fetch that the legacy slim usePortalAuthStore had compensated for.

Changes:
- types/auth.ts: add portal_events?: PortalEvent[] to MeResponse
- useAuthStore: add portalEvents ref, populated in setUser from
  me.portal_events, cleared in clearState
- usePortalStore: replace loadUserEventsFromApiAndStorage (which
  fetched /auth/me) with syncEventsFromAuthStore (which reads
  authStore.portalEvents). A reactive watch keeps userEvents in sync
  whenever the auth store updates (login, refresh, logout). The
  sessionStorage merge stays as offline cache + post-registration
  bridge.
- types/portal.ts: drop the now-unused AuthMeUser type — MeResponse
  is the canonical shape post-merge.

Boundaries: usePortalStore (stores-portal) statically imports
useAuthStore (stores) — already allowed by the matrix
(stores-portal allow includes stores).

Adds 4 vitest specs covering: userEvents reflects auth.portalEvents,
no apiClient.get('/auth/me') call from the portal store,
sessionStorage fallback when auth has not hydrated, reactive update
on auth.portalEvents change.

Test count 205 → 209. Lint + typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:57:40 +02:00
3019095a2a fix(security): A13-8 — migrate portal store to sessionStorage with explicit reset
usePortalStore now persists state in sessionStorage instead of
localStorage. Tab-close clears the session implicitly; explicit logout
+ 401 paths invoke reset() which iterates the `crewli:portal:` prefix
and removes every key (forward-compatible for future portal-namespaced
state).

Storage keys are renamed under the canonical prefix:
- crewli_portal_user_events_v1 → crewli:portal:events
- crewli_portal_active_event_id_v1 → crewli:portal:activeEventId

The single new prefix-clear function (clearStoragePrefix) replaces the
hand-listed key removals, so future portal-namespaced state additions
need no reset() change.

useAuthStore.handleUnauthorized() (the 401 interceptor target) is now
async and invokes clearAll() — the canonical session-cleanup hub —
restoring the portal-storage cleanup that the deleted
usePortalAuthStore.handleUnauthorized previously owned. The merge in
Phase E left this gap; this commit closes it.

Adds 7 vitest specs in stores/portal/__tests__/usePortalStore.spec.ts
covering: sessionStorage persistence, reset() prefix-iteration,
non-prefixed-key isolation, reactive state reset, useAuthStore.clearAll
+ handleUnauthorized integration.

Test count 198 → 205. Lint + typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:43:40 +02:00
38a94c78e9 feat(auth): post-login landing route resolution per context
login.vue is rewritten to consume useAuthStore.login()'s discriminated
union — no more direct apiClient calls or branching on raw API response
shapes. The page maps result.kind to UI/routing decisions only:

- mfa-required → swap to MfaChallengeCard with the typed payload
- authenticated → resolvePostLoginTarget() (?to= relative, else
  auth.resolveLandingRoute())
- must-set-password → forward-compatible placeholder route
- failed → field-level errors + rate_limit message branch

resolveLandingRoute() now returns a string path instead of
RouteLocationRaw — the typed router accepts string-paths cleanly,
removes the cast at every call site, and lets useAuthStore.spec.ts +
guards.spec.ts assert the resolved path directly.

A13-3 minimum precaution lives in a new utility:
src/utils/postLoginRedirect.ts. The relative-only check
(`startsWith('/') && !startsWith('//')`) rejects absolute, protocol-
relative, javascript:, and data: schemes. Full domain validation lands
in WS-3 PR-B2b.

6 vitest specs in utils/__tests__/postLoginRedirect.spec.ts cover the
six rejection / passthrough scenarios.

Test count 192 → 198. Lint + typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:40:32 +02:00
209e0ef682 feat(layout): context-switcher for multi-role users
Adds components/shared/ContextSwitcher.vue — a Vuetify menu-button
that renders only when useAuthStore.showContextSwitcher is true (i.e.
the user has both portal and organizer contexts available). Click
calls useAuthStore.setLastContext + resolveLandingRoute and pushes
the new route.

Wired into both layouts:
- PortalLayout.vue: navbar right section, before UserAvatarMenu
- DefaultLayoutWithVerticalNav.vue (organizer navbar host): before
  NavbarThemeSwitcher (OrganizerLayout.vue itself is a 10-line
  wrapper around DefaultLayoutWithVerticalNav, so the component
  wires into the actual navbar host).

Boundaries matrix update: components-shared now allows `stores` so
canonical shared chrome (ContextSwitcher, future global indicators)
can read useAuthStore directly without re-homing to
components/layout/. stores-portal stays disallowed for components-
shared by design — portal-specific state has no place in shared
chrome.

Adds 3 vitest specs covering: visibility gated by
showContextSwitcher, click invokes setLastContext + router.push.

Test count 189 → 192. Frontend lint + typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:35:32 +02:00
473b22ac9e feat(router): context-aware guards with meta-driven role/context resolution
Rewrites plugins/1.router/guards.ts per ARCH-CONSOLIDATION §4.3. The
B1 portal-context carve-out is removed; portal/organizer routing is
now declarative via meta.context, role gates via meta.requiresRole.

Guard pipeline:
1. Initialize auth store on first navigation
2. Public routes pass through (authenticated user on guest-only path
   is bounced to resolveLandingRoute)
3. Auth required → /login?to=<path>
4. MFA setup gate → /account-settings?tab=security
5. requiresRole declarative check (replaces hardcoded /platform path
   prefix + isSuperAdmin)
6. Context routing — portal returns early, organizer falls through
   and sets lastContext
7. Org-selection check (organizer routes only)

Page meta updates (mechanical, idempotent):
- 4 portal pages: removed `requiresAuth: true` (auth is implicit)
- 4 pages: replaced `requiresAuth: false` with `meta.public: true`
  (registreren, wachtwoord-instellen, advance/[token],
  invitations/[token])
- 22 organizer pages: added `context: 'organizer'`
  (account-settings, events/**, organisation/form-failures/**,
  select-organisation, dashboard, events/index, members,
  organisation/{index,companies,settings})
- 8 platform pages: added `context: 'organizer'` +
  `requiresRole: 'super_admin'`
- 6 organizer pages had no definePage block — one was added with
  `context: 'organizer'`

Adds plugins/1.router/__tests__/guards.spec.ts (11 tests) covering
public passthrough, unauthenticated redirect, portal/organizer
context branching, declarative requiresRole, org-selection
redirect, MFA gate.

Test count 178 → 189 (11 new). Lint + typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:32:54 +02:00
f2b08ecb21 refactor(auth): merge usePortalAuthStore into useAuthStore with context-aware getters
usePortalAuthStore is deleted — its 114 lines were a slim wrapper over
the same /auth/me endpoint useAuthStore already consumes. The merged
store gains the full set of additions Bert specified for B2a:

State:
- availableContexts / defaultContext (from /auth/me contexts block)
- lastContext (localStorage-persisted)
- portalToken (in-memory only, for the bearer-axios flavour)

Getters: isPortalUser, isOrganizerUser, isPlatformAdmin (alias of
isSuperAdmin), showContextSwitcher, hasRole(), hasAnyRole().

Actions: login(), verifyMfa() — both return typed discriminated
unions so login.vue (Phase H) consumes results without branching on
raw API response shapes. setLastContext, setPortalToken,
resolveLandingRoute, clearAll. clearAll dynamically imports
usePortalStore.reset() to clear portal sessionStorage on session-end —
this is the canonical session-cleanup hub now that the merge has
happened.

5 source files migrated from usePortalAuthStore → useAuthStore. The
PortalLayout.spec.ts mock follows. The boundaries matrix gains a
single new edge (`stores → stores-portal`) replacing the deleted
stores-portal/usePortalAuthStore which previously owned that
cross-zone call.

Adds 16 vitest specs in src/stores/__tests__/useAuthStore.spec.ts
covering setUser context hydration, hasRole/hasAnyRole, lastContext
localStorage persistence, resolveLandingRoute precedence
(portal/organizer/super_admin/multi-role/forceContext/forbidden
fallback), portalToken state, and clearAll cleanup.

Test count 162 → 178 (16 new). Frontend lint + typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:25:24 +02:00
13d7b18257 refactor(axios): split lib/axios.ts into factory + default + portal-token instances
The single axios.ts file becomes a directory with:
- factory.ts — createApiClient + the registerDefaultInterceptors /
  registerPortalTokenInterceptors seam (preserves the
  TECH-AXIOS-STORE-COUPLING decoupling — no store imports inside)
- default.ts — cookie-authenticated client (organizer + cookie-auth
  portal flows; existing 45 call sites resolve unchanged)
- portal-token.ts — Bearer-auth client for the artist-advance /
  supplier-intake flows (forward-compatible groundwork; no active
  consumers today)
- index.ts — re-exports apiClient + portalApiClient + the register* /
  createApiClient surface; the existing `import { apiClient } from
  '@/lib/axios'` continues to work directory-resolved.

The bindings plugin (plugins/3.axios-bindings.ts) now wires both
clients with a shared deps base + flavour-specific overrides. The
`getPortalToken` callback returns null until Phase E surfaces
`portalToken` on useAuthStore — no current consumers exercise the
Bearer path, so the null-return is intentional placeholder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:18:55 +02:00
a2760ffd64 feat(auth): add contexts + platform.is_super_admin to /auth/me, factory role-category states
Additive enrichment to MeResource — existing fields untouched, MeTest stays green.
New fields:
- contexts.available: list<'portal'|'organizer'> derived from Person + Organisation memberships
- contexts.default: precedence super_admin > organizer > portal > fallback portal
- platform.is_super_admin: bool promoted from app_roles
- organisations[].roles: 1-element array form alongside the legacy scalar role,
  forward-compatible for the multi-role pivot work tracked in TECH-PIVOT-ROLES-MULTI

UserFactory gains volunteer(), orgAdmin(), volunteerAndOrganizer(), superAdmin()
state methods — codified role categories for reuse across future workstreams.

Adds forbidden.vue placeholder (PublicLayout) for the context-failure landing in
the upcoming guard rewrite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:15:10 +02:00
b5a2140517 fix(sync): track ARCH-BINDINGS.md in .claude-sync.conf 2026-05-05 20:43:20 +02:00
d1503ceadf docs(vuexy): update VUEXY_COMPONENTS.md for post-PR-B1 single-SPA reality
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:33:33 +02:00
a4281df021 docs(arch): add ARCH-BINDINGS.md — canonical reference for FormBindingApplicator pipeline (WS-8a) 2026-05-05 20:22:11 +02:00
06b3a637b2 Merge pull request 'WS-3 PR-B1: Portal moves + routing wiring' (#4) from feat/ws-3-pr-b1-portal-moves into main
Reviewed-on: #4
2026-05-05 20:21:01 +02:00
0dceb437f3 refactor(register): drop auth-store dependency from success.vue, rely on query param
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:52:13 +02:00
4a4bd6c51e chore(monorepo): remove apps/portal — single SPA from this commit forward
Completes WS-3 PR-B1 charter §4.2: portal is fully consumed by
apps/app under /portal/** (authenticated portal routes) and
/register/** (public token-based form-fill). All portal source has
moved or been merged in earlier commits in this PR.

Adaptations from the original prompt's Phase F:
  - pnpm-workspace.yaml does not exist at the repo root (the monorepo
    isn't a pnpm workspace; each app has its own package.json /
    node_modules / scripts). No edit needed.
  - Root package.json has no `dev:portal` / `build:portal` scripts.
    No cleanup needed.
  - Skipped `pnpm -w build` — apps/app builds via its own scripts.

Deletes 384 portal files (build configs, layouts, plugins, vendored
@layouts, public/, dev/prod Dockerfiles, nginx.conf, env.d.ts,
themeConfig, tsconfig, package.json, lockfile, etc.). All authentic
portal logic is preserved in apps/app/src — verified by:
  - Vitest 23 / 162 passing
  - vue-tsc --noEmit clean
  - eslint clean (zero new errors / warnings)

NOT verified at this point: `pnpm build`. The build fails on a
pre-existing missing `flatpickr` stylesheet import in
src/@core/components/app-form-elements/AppDateTimePicker.vue —
present on main pre-PR, unrelated to this work, and tracked
separately. Reproduced on plain `main` without any of these changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:32:37 +02:00
a84742a01f chore(eslint): activate boundary sub-zones (TECH-WS3-BOUNDARIES-SUBZONES)
Adds the WS-3 §4.2 sub-zone classification to the apps/app
boundaries matrix:

- components-{shared,portal,organizer} alongside the legacy
  components type. components/{auth,settings} are folded into
  components-shared as the legacy cross-context home for MFA dialogs
  + PasswordRequirements (used by both organizer reset-password and
  portal wachtwoord-instellen / profiel).
- composables-forms (src/composables/forms/**) — pure form-runtime
  helpers reusable from organizer Form Builder later.
- stores-portal (src/stores/portal/**) — keeps the portal auth +
  portal store walled off from the organizer auth surface.
- pages-{register,portal,platform,organizer} alongside the legacy
  pages type — register pages cannot reach into stores or
  components-portal/-organizer; portal pages cannot reach
  components-organizer; organizer + platform pages cannot reach
  stores-portal or components-portal.

Cross-context edges are forbidden (organizer ↛ portal,
shared ↛ portal/organizer). Two pragmatic exceptions are documented
inline:
  - components-shared accepts the legacy auth/ + settings/ paths
    until PR-B2 cleanup re-homes them under shared/{auth,settings}/.
  - pages-register may read stores-portal because success.vue
    optionally enriches with the portal user when authenticated.
    PR-B2 may move success.vue into pages-portal so this drops.

Lint: 0 errors / 0 new warnings (only the pre-existing
boundaries v5→v6 deprecation warnings, which apply to all 19 rules
now). Tests: 23 / 162 pass. Typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:29:32 +02:00
5c689f42a0 feat(router): wire portal/register pages, portal-context guard carve-out, lint cleanup
Routing wiring (Phase D of WS-3 PR-B1):

- apps/app/src/plugins/1.router/guards.ts: add a single early-return
  carve-out before the org-selection redirect — `if (to.meta.context
  === 'portal') return`. Per ARCH-CONSOLIDATION-2026-04 §4.3,
  meta.context is the canonical contract; PR-B2 evolves the guards
  from this key to full context-aware logic (post-login landing,
  context-switcher, role checks).
- apps/app/env.d.ts: extend RouteMeta with the new layout names
  ('OrganizerLayout' | 'PortalLayout' | 'PublicLayout'), context,
  requiresAuth, requiresToken, navMode, navTitle.
- apps/app/typed-router.d.ts: regenerated by unplugin-vue-router to
  pick up portal/* and register/* route names.
- Page meta finalisation: portal pages have layout: 'PortalLayout',
  context: 'portal', preserving original requiresAuth + nav fields;
  register pages have layout: 'PublicLayout' + public: true (the
  apps/app guard convention for public routes, since meta.public is
  what the existing guard recognises).

Form-types restructure (boundaries cleanup):

- apps/app/src/composables/forms/types/formBuilder.ts → src/types/forms/
- apps/app/src/composables/forms/utils/{formValidation,validators}.ts
  → src/utils/forms/
- All `@/composables/forms/{types,utils}/*` imports rewritten across
  pages, components, composables, tests.
- This avoids a `types → composables` boundaries violation at
  src/types/formSchema.ts which re-exports primitives from the
  inlined form-schema. types/formSchema.ts now imports from
  @/types/forms/formBuilder which is in the same boundaries zone.

Lint cleanup for moved portal sources (apps/portal had no
.eslintrc.cjs; the migrated code now has to pass apps/app's stricter
config):

- axios.isAxiosError → named import { isAxiosError }
  (ClaimenTab, RoosterTab, profiel.vue)
- void schemaQuery.refetch() → schemaQuery.refetch()
  (register/[public_token].vue)
- if-then-else collapsed to single boolean return (formatFieldValue)
- :delay-on-touch-only="true" → delay-on-touch-only shorthand
  (FieldSectionPriority)
- ml-2 class → ms-2 (FieldAvailabilityPicker)
- multi-statement-per-line splits in profiel.vue + spec files
- unused emailConfigured ref removed (profiel.vue)
- one-component-per-file disabled with TODO TECH-WS3-PORTAL-LINT-CLEANUP
  ref (FieldOptionsLocale.spec.ts — multi-Wrapper test pattern)
- restored `import Draggable from 'vuedraggable'` after lint:fix
  removed it (template-only usage; the import IS needed)
- camelcase param renamed in FieldOptionsLocale harness factory
- typecheck nudge: spec state.data typed via PublicFormSectionOption[] /
  PublicFormTimeSlot[] aliases instead of Record<string, unknown>
- PortalLayout.vue: explicit `import { useRoute, useRouter }` so the
  vitest mock can intercept (the trimmed AutoImport set doesn't pull
  vue-router's auto-imports)

Vitest: 23 / 162 passing. Lint: 0 errors / 0 new warnings (only the
pre-existing boundaries v5→v6 deprecation warnings remain). Typecheck:
clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:26:46 +02:00
e3452312d1 refactor(layouts): merge portal navbar/drawer into PortalLayout.vue
Migrates the navbar (event/platform two-mode toggle), mobile drawer
with avatar header + logout, RouterView Suspense wrapper, and footer
from apps/portal/src/layouts/portal.vue into the PortalLayout.vue
skeleton from PR-A. The skeleton's structure (VApp / VAppBar / VMain
/ VFooter) is preserved as the outer shell.

Notable adaptations:
  - useAuthStore → usePortalAuthStore (renamed in C.3)
  - usePortalStore import path → @/stores/portal/usePortalStore
  - mobile nav links now point at /portal/evenementen and /portal/profiel
    (the new sub-zone paths) instead of /evenementen and /profiel
  - explicit `import { useRoute, useRouter }` from vue-router so the
    vitest mock can intercept (auto-import not configured for these in
    the trimmed test config)

Updated PortalLayout.spec.ts to mock the two pinia stores plus
useSkins, vue-router, UserAvatarMenu, and AppLoadingIndicator. Tests
now assert the auth-conditional rendering: header + drawer hidden
when unauthenticated, main + footer always present.

Also pulls in the @form-schema → @/composables/forms/* import
rewrites in the C.4-moved composables that the previous commit's
rename-only diff left unstaged.

Vitest: 23 files / 162 tests, no errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:11:58 +02:00
7282861a7e refactor(portal): move composables, types, schemas; drop duplicates
Composables (apps/portal/src/composables → apps/app/src/composables/):
  - useFormDraft, publicFormInjection → composables/ (root, used by
    shared/public-form components)
  - api/usePublicForm, api/usePublicFormSections,
    api/usePublicFormTimeSlots → composables/api/ (no collisions)
  - api/usePortalShifts, api/usePortalProfile, api/useVolunteerRegistration
    → composables/api/portal/ (subfolder per WS-3 PR-B1 charter to
    leave room for organizer-side namesakes without clashes)
  - api/useMfa → DELETED (apps/app version is a strict superset
    with extra invalidateQueries calls and the admin-reset mutation)

Types (apps/portal/src/types → apps/app/src/types/):
  - api, portal-shift, portal, registration → moved
  - mfa → DELETED (byte-identical to apps/app/src/types/mfa.ts)

Schemas:
  - apps/portal/src/schemas/registrationSchema.ts → apps/app/src/schemas/

Utils:
  - deviceFingerprint, paginationMeta → DELETED (byte-identical
    duplicates already in apps/app/src/utils/)

Lib:
  - apps/portal/src/lib/{axios,query-client}.ts → DELETED. apps/app's
    callback-bound axios (post-PR-A) and query-client are the
    canonical versions. Portal pages currently importing
    `@/lib/axios#apiClient` resolve to apps/app's apiClient with no
    behavioral change for cookie-based requests.

Tests: 4 composable specs (useFormDraft x2, usePublicFormSections,
usePublicFormTimeSlots) moved into __tests__/ subdirs alongside
their composables.

@form-schema imports inside the moved files rewritten to
@/composables/forms/*.

Vitest now: 23 files / 162 tests passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:08:53 +02:00