The True-Client-IP header is set by Cloudflare and is only trustworthy
when behind a Cloudflare proxy. In non-Cloudflare deployments, this
header is attacker-controlled and can be used to spoof IP addresses.
Moving the middleware into the saas engine ensures it only loads for our
Cloudflare-fronted production deployment, not for self-hosted OSS
instances.
GHSA-cpch-9qg2-x8fq
* Add JSON response format to void-response controller actions
Add respond_to blocks with JSON format to 14 controllers that
previously only rendered HTML or Turbo Stream responses. JSON
callers get head :no_content (or :created/:ok for push
subscriptions, distinguishing new vs existing).
Covers: board involvements, board/account entropies, column
reordering, card readings, card publishing, user roles, user
avatars, account settings, notification settings, join codes
(with 422 error branch), and push subscriptions.
* Add steps index endpoint with JSON view
Add index action to Cards::StepsController so SDK/CLI callers
can list all steps for a card. Reuses the existing _step.json
partial.
* Add JSON views for paginated card list endpoints
Add show.json.jbuilder for stream, not-now, and closed column
endpoints. No controller changes needed — set_page_and_extract_
portion_from and fresh_when already work format-agnostically.
* Add JSON search endpoint with distinct-card pagination
JSON search paginates distinct Card records (via the existing
Card.mentioning scope with .distinct) rather than Search::Record
objects. This ensures page boundaries, Link headers, and counts
reflect unique cards — no short pages from post-pagination dedup.
ID-match searches return a uniform single-element Card[] array
through the same pagination path.
* Add JSON views for settings and configuration endpoints
Add show.json.jbuilder for account settings (name), join codes
(code, usage_count, usage_limit, url, active), and notification
settings (bundle_email_frequency). Tests for show and update
were added in the void-response commit.
* Add JSON response format to data exports
Create returns 201 with export id, status, and created_at so
callers can poll. Show returns any-status exports for JSON (not
just completed) with a download_url when ready, or 404 for
missing/other-user exports.
* Extend access token JSON API
Add index.json and _access_token.json partial for listing tokens.
Add JSON format to destroy. Include id and created_at in the
create response alongside the existing token/description/permission
fields.
* Fix ambiguous precedence warning in export JSON view
* Assert X-Total-Count pagination header in streams JSON test
* Address review feedback
- Guard steps index against HTML requests (redirect to card)
- Normalize created_at to UTC in access token and export JSON
- Add deterministic ordering (.latest) to search JSON pagination
* Return 406 for HTML requests to steps index instead of redirect
* Add wrap_parameters for flat SDK JSON payload compatibility
Five controllers infer the wrong wrapper key from their class
name, so flat JSON bodies like {"role":"admin"} fail with 400.
Add explicit wrap_parameters declarations:
- Users::RolesController → :user
- Notifications::SettingsController → :user_settings
- Account::JoinCodesController → :account_join_code
- Account::SettingsController → :account
- Boards::EntropiesController → :board
Add integration tests that send flat (unwrapped) JSON payloads
for all seven param-bearing endpoints to prove SDK compatibility.
* Address PR review feedback
- Use 201 Created (not 204) for all create actions: publishes, readings,
left/right positions, push subscriptions
- Simplify push subscription to always return :created (no 200/201 variance)
- Remove superfluous respond_to block from steps#index
- Merge exports#show into single respond_to block
- Move export download_url conditional out of inline args
- Ensure join code active is explicitly boolean
- Remove extra parens from search controller assignment
- Add blank lines between format blocks for readability
* Return JSON bodies on create actions
Create actions for boards, cards, columns, comments, and steps now
render the resource as JSON (via their show views) instead of returning
empty 201 responses with only a Location header. SDKs expect a JSON
body they can deserialize into the created resource.
* Return JSON bodies on reaction create actions
Card and comment reaction creates now render the reaction as JSON
instead of returning empty 201 responses, consistent with the other
resource-creating endpoints.
* Allow access tokens API without account scope
Access tokens are identity-level (not account-scoped), so the
controller needs to work with bearer token auth and no account slug
in the URL. Skip require_account so the bearer token auth path
can proceed.
Also fix involvement test to send params in JSON body (not query
string) to match real SDK usage.
Publishing or unpublishing a board didn't bust the `json.cache! board`
fragment cache because `Board::Publication` didn't touch its parent
board. The board's `updated_at` stayed stale, so subsequent GET requests
continued serving cached JSON without `public_url`.
The public closed column was showing draft cards because it queried
`@board.cards.closed` without a `.published` filter. This adds the
missing `.published` scope to match the pattern used elsewhere in the
public controllers.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add JSON response format to access token creation
Allows programmatic clients (e.g., CLI) to create access tokens
via the API by returning the token, description, and permission
as JSON instead of redirecting to the HTML show page.
* Add JSON response format to access token creation
Allows programmatic clients (e.g., CLI) to create access tokens
via the API by returning the token, description, and permission
as JSON instead of redirecting to the HTML show page.
* Style
---------
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Separate the attachment existence check from the broadcast suppression
override so each file has a single responsibility. The guard now lives
in ActiveStorageAnalyzeJobSkipDetached with its own documentation
explaining the upload-then-delete race condition.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Upload-then-delete races cause AnalyzeJob to hit S3 after PurgeJob has
already removed the object, producing Aws::S3::Errors::NoSuchKey noise
in solid_queue_failed_executions. Since PurgeOnLastAttachment destroys
the attachment row before enqueueing PurgeJob, checking
blob.attachments.exists? catches this — a fast DB query that avoids the
S3 round-trip entirely.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Clear assignee's existing notifications in setup since Notifier now
uses create_or_find_by instead of create, and reload the association
to avoid caching after destroy_all.
Update fixture references after rebase: use `logo_assignment_kevin`
as base notification and `logo_mentioned_david` for mention tests.
Update source to `logo_published` in tests that need generic card events.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add comprehensive integration test covering card assignment, comments,
mentions, email bundling, and edge cases (system user, inactive user)
- Inline push notification test helpers into the only test that uses them
- Remove separate PushNotificationTestHelper module
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The web push payload sends the URL in data.url but the service
worker was looking for data.path, resulting in undefined URLs.
Also fix WebPush::Notification test to use url instead of path
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Each target now implements process directly with its own logic,
rather than using processable?/perform_push hooks. The pushable?
check is done once in Notification#push before iterating targets.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
"Push to target" reads naturally - we push the notification to the
target. "Target processes" also makes sense - the target receives
and handles the notification in its own way.
- Add class method PushTarget.process(notification) that instantiates
and calls the instance method
- Rename instance method from push to process
- Add private push_to helper in Pushable for readable iteration
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace separate WebPushJob and NativePushJob with a single PushJob
that calls notification.push, which iterates over registered targets.
Each target handles its own delivery - Web pushes synchronously via
the pool, Native enqueues device-level jobs via deliver_later_to.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
PushTarget#push reads more naturally than Push#push. The push target
is the thing that pushes notifications to a specific destination.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace NotificationPusher with a cleaner architecture:
- Add Notification::Pushable concern with push target registry
- Add Notification::Push base class with template methods
- Add Notification::Push::Web for web push (OSS)
- Add Notification::Push::Native for native push (SaaS)
- Add Notification::WebPushJob and Notification::NativePushJob
Key design:
- Registry pattern: Notification.register_push_target(:web)
- Template method: push calls should_push? then perform_push
- Subclasses override should_push? (with super) and perform_push
- Each target handles its own job enqueueing
Also:
- Add Notification#pushable? for checking push eligibility
- Add Notification#identity delegation to user
- Reorganize tests to match new class structure
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Tidy up saas engine a bit more
* Fix N+1 queries on identity in UsersController and BoardsController
- Add .includes(:identity) to UsersController#index
- Add .includes(creator: :identity) to BoardsController#index
- Add N+1 regression tests for both controllers
* Simplify controller N+1 tests with query assertions
---------
Co-authored-by: Mike Dalessio <mike@37signals.com>
Include identity.id in the my/identity.json response and return an
empty body from the CREATE signup/completions.json endpoint.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Event notifiers used the `mentionees` DB association to exclude mentioned
users from comment/card notifications. Since mentions are created async
via Mention::CreateJob, a race condition meant the mentionee list could
be empty when the event notification job ran first, causing the user to
receive both a comment and a mention push notification.
Use `scan_mentionees` instead, which scans the rich text body directly
for mentioned users without depending on Mention records existing yet.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rails only applies the last callback when `after_create_commit` and
`after_update_commit` reference the same method name [1]:
> However, if you use the `after_create_commit` and the
`after_update_commit` callback with the same method name, it will only
allow the last callback defined to take effect, as they both internally
alias to `after_commit` which overrides previously defined callbacks
with the same method name.
- Push notifications were never sent when a notification was first
created — only when the source was updated
- Replaced the two callbacks with a single `after_save_commit`, which
fires on both create and update, with the
`source_id_previously_changed?` guard (true for both new records and
source changes)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
[1] https://guides.rubyonrails.org/active_record_callbacks.html#aliases-for-after-commit
Cover the stemmer tokenizing hyphenated input into separate words and
end-to-end search finding cards with hyphenated titles.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ZipKit::FileReader raises MissingEOCD for truncated, corrupted, or
non-zip files. This exception is a direct StandardError subclass, not
a subclass of InvalidStructure or ReadError, so it escaped as an
unhandled job failure instead of being caught and surfaced to the user.
Broaden the rescue in ZipFile::Reader#initialize to catch ReadError
(parent of InvalidStructure), MissingEOCD, and UnsupportedFeature.