Commit Graph

23 Commits

Author SHA1 Message Date
03ff1cdfce feat(form-builder): apply_deadline_seconds config key (default 5)
Per RFC-WS-6 §Q1 v1.3 addition 4.

Configurable deadline for FormBindingApplicator::apply(). Default 5
seconds catches the long tail of slow applies before they hang the
public flow. Tunable per environment via FORM_BUILDER_APPLY_DEADLINE_SECONDS.

Consumed by ApplyBindingsOnFormSubmit::handle's withDeadline() call
(landed in Phase B).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 02:59:55 +02:00
5980c36ae4 refactor: SentryEventScrubber static + config array notation
The scrubber is fully stateless. Container-resolution per event was
overhead without value, the closure indirection polluted the config
layer with executable logic, and stack traces showed an anonymous
closure frame instead of the class name.

- SentryEventScrubber::scrub() and its private helpers all become
  static methods. No instance fields, so the change is mechanical.
- config/sentry.php before_send switches from a closure that calls
  app() to PHP array-callable notation [Class, method]. Symfony
  OptionsResolver accepts array-callables for static methods.
- PiiScrubbingTest swaps (new SentryEventScrubber)->scrub(...) for
  SentryEventScrubber::scrub(...). Semantics unchanged.

Tests 1537 unchanged. Larastan and Pint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 12:42:25 +02:00
bdb89a2479 feat: sentry-laravel install + scrubber + ignored exceptions
WS-7 PR-2 commit 1. Wires sentry-laravel into the app behind a
config-only no-op when SENTRY_DSN_BACKEND is empty (RFC §3.3).

- composer require sentry/sentry-laravel ^4.15 (resolved 4.25.1)
- config/sentry.php: DSN env mapped to SENTRY_DSN_BACKEND, environment
  falls back to APP_ENV, traces/profiles forced to 0.0 (RFC §2
  amendment B), send_default_pii hard-pinned false, before_send to
  SentryEventScrubber, ignore_exceptions covers ValidationException /
  AuthenticationException / AuthorizationException.
- app/Services/Observability/SentryEventScrubber.php: recursive body /
  header / query-string scrubber + form_values wholesale replacement +
  HttpException sub-500 drop (status filter that ignore_exceptions
  cannot do class-only). Max-depth guard against malicious payloads.
- app/Enums/Observability/ActorType.php: enum + resolver for §3.6
  actor_type tag (consumed by BindSentryContext in commit 2).
- tests/Feature/Observability/PiiScrubbingTest.php: 20 cases.
- api/.env.example: SENTRY_DSN_BACKEND + SENTRY_RELEASE entries.

Larastan: clean. Test count: 1487 to 1507.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 08:55:50 +02:00
0e986f42cb refactor(form-builder): align binding registry with model column reality (WS-6)
Three renames (registry → matches actual Eloquent model column):
  - person.phone_number       → person.phone
  - company.email             → company.contact_email
  - company.phone_number      → company.contact_phone

Six removals (registry attribute does not exist as model column,
intentionally deferred):
  - person.dietary_preferences  (custom_fields JSON path; BACKLOG
    FORM-BINDING-JSON-PATH)
  - artist.email                (Artist model absent + column absent)
  - artist.stage_name           (column absent)
  - artist.tech_rider           (column absent)
  - artist.hospitality_rider    (column absent)
  - artist entity removed entirely (no v1 bindable attributes)

Decisions documented inline in binding_targets.php and tracked
via BACKLOG entries (Task 4 of this session).

Tests touched:
- BindingTypeRegistryTest:
    test_resolve_person_dietary_preferences_returns_collection_array →
      renamed test_resolve_collection_attribute_returns_collection_array,
      uses Config::set to inject a synthetic 'test_entity.tags' collection
      target. v1 has no production collection targets (BACKLOG
      FORM-BINDING-JSON-PATH).
    test_validate_append_strategy_accepts_collection_target — same pattern.
    test_entities_returns_known_entities — drop 'artist' from expected list.
    test_attributes_for_person_includes_email_and_dietary_preferences →
      renamed _includes_email_and_phone (the renamed attribute).
- AppendStrategyRequiresCollectionTargetTest:
    test_passes_with_collection_target — same Config::set synthetic-
    target pattern.
- MaxOneIdentityKeyPerTargetEntityTest:
    test_passes_with_one_identity_key_each_on_different_entities —
    'company.email' → 'company.contact_email' to match registry rename.

Refs: WS-6 sessie 3a binding-target drift audit

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:14:13 +02:00
6b5111ce43 feat(form-builder): ApplyBindings listener chain with two-transaction pattern (WS-6)
ApplyBindingsOnFormSubmit (sync) wraps the applicator in DB::transaction
and writes apply_status post-commit. On exception: outer catch records
FormSubmissionActionFailure in a separate transaction (survives inner
rollback), marks apply_status=failed, swallows so siblings keep running
(RFC Q3, Q4). When ApplyBindings provisions a Person on a previously
no-subject submission, the listener also writes subject_type/subject_id
back so TriggerPersonIdentityMatchOnFormSubmit (next sync listener) can
find the freshly-provisioned subject.

ApplyBindingsOnFormSectionSubmitted (queued, feature-flagged) ready
for ARTIST_ADVANCE activation per RFC Q10.

Listener chain on FormSubmissionSubmitted explicitly registered in
AppServiceProvider::boot for deterministic ordering (RFC Q1):
ApplyBindings → IdentityMatch → queued siblings.

FormBindingApplicator dropped 'final readonly' to 'class' so listener
tests can subclass it for throw-path coverage; constructor properties
remain readonly individually.

Refs: RFC-WS-6.md §3 (Q1, Q3, Q4, Q10)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 13:18:30 +02:00
16a9265430 feat(form-builder): add PurposeSubjectResolver per purpose (WS-6)
Parallel interface to PurposeGuardProvider for runtime subject
resolution. Seven concrete resolvers, one per v1.0 purpose. Wired
through purposes.php via subject_resolver_class key.

EventRegistration uses PersonProvisioner (may create). Other purposes
resolve from existing context (portal token, production request, auth).
IncidentReport is the only purpose allowed to return null (anonymous-
allowed configurations); the others return concrete model types
(narrowed via PHP covariance) for caller convenience.

Refs: RFC-WS-6.md §3 (Q9)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:57:21 +02:00
e3c9211e3f feat(form-builder): wire PurposeGuardProvider per purpose (WS-6)
Adds PurposeGuardProvider as a parallel interface to PurposeDefinition
(value object stays untouched). Seven concrete providers, one per v1.0
purpose, each declaring its publish-guard list. Registry resolves and
caches providers via guards_class config key.

Universal guards (MaxOneIdentityKeyPerTargetEntity,
AppendStrategyRequiresCollectionTarget, NoAmbiguousTrustLevels,
IdentityKeyBindingsOnlyInFirstSection) wire into every purpose. The
section guard is a cheap no-op when section_level_submit=false.

ArtistAdvanceGuards omits RequiresIdentityKeyBinding because the
artist subject is resolved via portal token, not form data. Same
reasoning for supplier_intake (production_request) and the auth-based
purposes.

Includes a cross-cutting BindingTypeRegistryConsistencyTest that
verifies tasks 5/7/8 do not contradict each other (registry ↔ guards ↔
purpose required_bindings).

Refs: RFC-WS-6.md §3 (Q9, Q13)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 23:01:19 +02:00
0dd991c688 feat(form-builder): add BindingTypeRegistry as single source of truth for target shapes (WS-6)
Config-driven mapping from (target_entity, target_attribute) to storage
shape (scalar/collection/relation), PHP type, and identity-key
eligibility. Replaces any name-suffix matching (e.g. _tags, _skills) —
those are convention-not-contract and reject by design.

Used by publish guards now and (in session 2) by FormBindingApplicator.

Refs: RFC-WS-6.md §4 (V1)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:41:25 +02:00
f38c7ece97 chore: install laravel telescope as dev-only debugging dashboard
Installs laravel/telescope ^5.0 (v5.12.5) as a dev-dependency.
Three-layer production safety adapted to Laravel 11 layout (no
Kernel.php; routing/schedule in bootstrap/app.php +
routes/console.php):

  1. composer.json `extra.laravel.dont-discover` lists
     laravel/telescope. After editing, `php artisan package:discover`
     regenerates bootstrap/cache/packages.php — without this step
     the auto-discovery cache still registers the vendor provider.
  2. AppServiceProvider::register() gates registration to local +
     testing environments. Registers BOTH the vendor
     Laravel\Telescope\TelescopeServiceProvider (routes, migrations,
     publishing) AND the project's App\Providers\TelescopeService
     Provider (gate + filter) — they're sibling classes that extend
     ServiceProvider independently, not parent/child, so both must
     register for the dashboard to work. bootstrap/providers.php
     deliberately does NOT list either Telescope provider.
  3. .env TELESCOPE_ENABLED flag (false in .env.example). Runtime
     toggle that disables Telescope even when the providers are
     registered.

Production safety verified via simulated APP_ENV=production check:
confirms no Telescope-* providers are loaded.

Authorization: viewTelescope gate restricts dashboard to users
with the super_admin Spatie Permission role. Even in local
environments, only super_admin can view. Default was an email
allow-list stub — replaced with `$user->hasRole('super_admin')`.

Pruning: Schedule::command('telescope:prune --hours=48') added in
routes/console.php (Laravel 11's schedule location), environment-
gated to local + testing only.

Documentation: /dev-docs/TELESCOPE.md added; CLAUDE.md gets a
Development-tooling section. The doc explicitly calls out the
dual-provider registration (vendor + app) which differs from the
single-provider pattern in older Laravel versions.

Migrations applied: telescope_entries, telescope_entries_tags,
telescope_monitoring tables. Route registration verified in local
(42 telescope.* routes).

Tests: 1208/1208 passing — Telescope loads in the testing
environment as well, so the suite exercised it without issues.

Deployment note (flag for separate docs): a production operator
who runs `php artisan migrate` manually will still apply the
Telescope migrations — but because the providers never register
in production, the tables stay empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 04:03:31 +02:00
61719bf8bf refactor(form-builder): pre-publish check reads form_field_bindings; drop binding JSON columns 2026-04-24 20:09:27 +02:00
ab67ed46ca refactor(form-builder): consolidate subject-type allow-list into purpose registry
Q6 of ARCH-CONSOLIDATION-ADDENDUM-2026-04-24: the allowed
`form_submissions.subject_type` values are now derived from
`PurposeRegistry::allSubjectTypes()` instead of the parallel
`config/form_subjects.php` file.

- CreateFormSubmissionRequest validates `subject_type` against the
  registry via constructor-injected PurposeRegistry.
- FormSubmissionController and FormValueService resolve the subject
  FQCN through `Relation::getMorphedModel()` — the morph-map is the
  single source of truth for alias → model mapping.
- `config/form_subjects.php` is deleted. `MorphMapAlignmentTest` keeps
  the registry and morph-map aligned going forward.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:35:48 +02:00
e93207765b feat(form-builder): introduce purpose registry and definition value object
Lands the v1.0 purpose registry (WS-2 of the consolidation sprint) as a
first-class concept: a `PurposeDefinition` value object, a
`PurposeRegistry` service keyed by slug, and a declarative
`config/form_builder/purposes.php` registry with exactly the seven
purposes from ARCH-CONSOLIDATION §6.4.

Also rebuilds the morph-map in `AppServiceProvider::boot` into three
labelled blocks: (1) domain subject types derived from
`PurposeRegistry::allSubjectTypes()`, (2) non-purpose domain types
hardcoded with comments (form_schemas owner_types, activity-log
subjects), (3) framework types (spatie/activitylog; Sanctum stays
absent per addendum Q4).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:35:18 +02:00
25de407e14 feat(forms): add form_binding, form_subjects, form_filter_registry, form_builder configs
Groundwork for S2+ services. Entity Column Registry whitelists valid
Pattern A/C binding targets; subject-type registry enforces morph-map;
filter registry separates filterable columns from bindable ones; builder
config holds limits, webhook policy, captcha, retention, feature flags.
Adds dedicated 'webhooks' Redis queue connection (retry_after 120s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 11:54:11 +02:00
28727f246b chore: remove admin SPA and update to two-app production setup
Remove apps/admin/ entirely — platform admin functionality now lives
in apps/app/ under /platform/* routes for super_admin users.

Production URL scheme changed:
- Organizer app: crewli.app (was app.crewli.app)
- Portal: portal.crewli.app (unchanged)
- API: api.crewli.app (unchanged)
- admin.crewli.app and app.crewli.app retired

Backend:
- Removed FRONTEND_ADMIN_URL config and admin cookie (crewli_admin_token)
  from SetAuthCookie, CookieBearerToken, cors.php, app.php
- Updated .env and .env.example (two origins, no port 5173)
- Updated cookie test: admin origin test → unknown origin fallback test

Infrastructure:
- Makefile: removed admin target
- deploy/nginx: updated CSP comment, removed admin vhost
- Updated README.md, CLAUDE.md, and all dev-docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 08:44:10 +02:00
940297f214 security: implement CSP headers (API middleware + Nginx configs + dev meta tags)
API middleware:
- SecurityHeaders now sets Content-Security-Policy from config/security.php
- Default API policy: "default-src 'none'; frame-ancestors 'none'"
- Supports report-only mode via CSP_REPORT_ONLY env var
- Policy value configurable via CSP_POLICY env var

Nginx deployment configs (deploy/nginx/):
- security-headers.conf: shared headers for all server blocks
- csp-api.conf: restrictive JSON-only policy for api.crewli.app
- csp-spa.conf: SPA policy for app/admin (self + unsafe-inline styles)
- csp-portal.conf: portal policy matching SPA

Development:
- CSP meta tags added to all three index.html files
- Includes 'unsafe-inline' + 'unsafe-eval' for Vite HMR/loader script
- Each app allows its own ws:// port for HMR websocket

Resolves security finding A13-9.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:14:37 +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
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
ec4ba8733d feat(api): organisation email branding and shared mail layout
- Add email branding columns to organisations table (logo, color, reply-to, sender name, footer)
- Create MailBrandingService for resolving per-org branding with defaults
- Create CrewliMailable abstract base class with branded from/reply-to
- Create shared Blade layout (mail.layouts.crewli) with inline CSS
- Refactor Registration*Mail and InvitationMail to extend CrewliMailable
- Add config/crewli.php for platform-wide defaults (portal_url, app_url, logo)
- Add dev-only /mail-preview/{type} route for browser email previewing
- Update Organisation model, resource, and form requests with branding fields

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 00:44:34 +02:00
8435e74fd3 feat(api): registration auth, account creation, check-email & email notifications
- Add POST /public/check-email endpoint with rate limiting (10/min)
- Create user accounts during volunteer registration (new or returning)
- Returning volunteers authenticate with existing password
- Add password validation to VolunteerRegistrationRequest
- Normalize emails to lowercase throughout registration flow
- Handle race condition on duplicate accounts gracefully
- Create RegistrationConfirmationMail, RegistrationApprovedMail, RegistrationRejectedMail
- Wire approval/rejection emails into PersonController
- Add POST persons/{person}/reject endpoint
- Trigger TagSyncService on registration and approval
- Add CheckEmailTest, PersonApprovalEmailTest, extend VolunteerRegistrationTest

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 00:37:04 +02:00
9acb27af3a feat: fase 2 backend — crowd types, persons, sections, shifts, invite flow
- Crowd Types + Persons CRUD (73 tests)
- Festival Sections + Time Slots + Shifts CRUD met assign/claim flow (84 tests)
- Invite Flow + Member Management met InvitationService (109 tests)
- Schema v1.6 migraties volledig uitgevoerd
- DevSeeder bijgewerkt met crowd types voor testorganisatie
2026-04-08 01:34:46 +02:00
fda161ee09 chore: align migrations, docs, and frontends with crewli.app setup
- Replace dated migrations with ordered 2026_04_07_* chain; fold users update into base migration
- Update OrganisationScope, AppServiceProvider, seeders, api routes, and .env.example
- Refresh Cursor rules, CLAUDE.md, Makefile, README, and docs (API, SCHEMA, SETUP)
- Adjust admin/app/portal HTML, packages, api-client, events types, and theme config
- Update docker-compose and VS Code settings; remove stray Office lock files from resources

Made-with: Cursor
2026-04-07 10:45: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
34e12e00b3 feat: initial commit - Band Management application 2026-01-06 03:11:46 +01:00