Commit Graph

4 Commits

Author SHA1 Message Date
central-worker-4
c0225d7691 A merged ticket is read-only on the server, not just on the page (#920)
The Show page announced "This ticket has been merged and is read-only.",
hid the reply composer and withheld the Merge button — and that was the
whole guard. It was presentation, on one page.

The Edit link and the Split button were still rendered, `edit()` still
served the form (with its Status select blank, because `merged` was not
one of the five slugs the form hard-coded), and `update()` took the save
with `errors: {}`: status went back to `open` while `merged_into_id` kept
pointing at the merge target. The row that came out was live and
assignable in the All Open queue AND merged into another ticket, holding
none of its own messages because the merge had moved them — an empty
shell that reads as a real duplicate of the ticket it was folded into.

So the rule now lives on the server, once — `Ticket::isMerged()` and
`refuseIfMerged()` — and every write the page presents as unavailable
calls it: update, edit, split, participants add/remove, and a reply or an
attachment (both hidden by the banner, neither refused by its route).

Two deliberate choices:

  - the guard reads `status === 'merged'`, NOT `merged_into_id !== null`.
    Reading the pointer would look stricter and would freeze every row
    this bug already minted (`status = open, merged_into_id = 56`), so
    the only way to correct one would be SQL.
  - it refuses with a redirect carrying the reason, not `abort(403)`.
    The realistic way to arrive here is a page that was already open when
    somebody else merged the ticket, and that operator is better served
    by the sentence than by an error screen.

The blank Status select was the report's second symptom, and it was the
same defect one layer up: the five hard-coded options meant any ticket
whose status was not among them — `merged`, or any status added on the
Statuses settings tab — rendered with nothing selected, and the first
save silently rewrote the status to whatever happened to be showing. The
options now come from the same table `update()` validates against.
2026-09-03 13:59:24 -07:00
0be06694b2 Read the attachment disk from config, not env (secrets S3)
The three reads were `config('ticketing.storage_disk', env('TICKETING_STORAGE_DISK', 'local'))`,
but this package had no config file at all — so the config() lookup was always
null and env() was the only reader.

Once `config:cache` runs in production, a runtime env() returns NULL. Every
ticket attachment would have silently gone to the 'local' disk, ignoring
whatever the operator had configured — and previously-stored attachments would
404 because show() looked on the wrong disk.

Verified rather than assumed: with TICKETING_STORAGE_DISK=s3proof set,
config() returned 's3proof' both cached and uncached, while env() returned
's3proof' uncached and NULL cached.

Adds config/ticketing.php + mergeConfigFrom so the value is captured at
cache time, and drops the env() fallbacks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 14:00:15 -04:00
Joel Wedemire
652829ab90 fix: bootstrap blocker + 4 security bugs
- Bootstrap (critical): settings/create/index no longer 403 on fresh install.
  Site admins (admin/super_admin) can access settings when 0 groups exist.
  First group creation seeds default priorities (Low/Medium/High/Urgent).
  Index shows friendly first-run splash. Create shows warning + settings link.

- Internal notes leak (high): submitters can no longer receive is_internal
  messages via ticket show, index detail panel, or any Inertia prop.
  filterMessagesForRole() strips internal notes for non-agents.

- Arbitrary assignee (med/high): update() now validates assigned_to against
  actual agent-access users for the ticket's group server-side.

- Cross-group priority/project forgery (medium): store() and update() now
  verify priority_id and project_id belong to the ticket's own group (or
  are global for priorities).

- Foreign message_id on attachment upload (medium): message_id is now
  validated to belong to the current ticket, not just any message row.
2026-04-08 18:31:51 -07:00
Joel Wedemire
391699220f feat: full dashboard-ticketing scaffold with data model, controllers, Vue pages 2026-04-08 17:10:30 -07:00