-
Notifications
You must be signed in to change notification settings - Fork 1
Web Push notifications foundation (CIRCLE-38) #107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,4 +31,12 @@ | |
| SlackNotificationJob.perform_later(comment_thread_id: payload[:comment_thread_id]) | ||
| end | ||
| } | ||
|
|
||
| # Web Push (VAPID) keys for browser notifications. For local dev these are | ||
| # checked in; in production they should come from Rails encrypted credentials | ||
| # or your secrets manager. Generate fresh keys with: | ||
| # bundle exec rake coplan:web_push:generate_keys | ||
| config.vapid_public_key = ENV["COPLAN_VAPID_PUBLIC_KEY"] || "BPY5NsdGJ4vEmHHNz3SqK2XsmV93j-iR3-kqN-RMbl4JRd9jnKpzunwdXDwFwlzbRlPErn3x379e6Cz7DfdSS6o=" | ||
| config.vapid_private_key = ENV["COPLAN_VAPID_PRIVATE_KEY"] || "1HoYR1d8QIlf8RYTfugJQFTyLlBat3zd-EFkj5dO9WQ=" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This initializer hardcodes a usable VAPID private key as a default, so any environment that forgets to set Useful? React with 👍 / 👎.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch — fixed in 3f2aa35. The fallback keypair now only applies in |
||
| config.vapid_subject = ENV["COPLAN_VAPID_SUBJECT"] || "mailto:dev@coplan.local" | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # This migration comes from co_plan (originally 20260508000000) | ||
| class CreateCoplanWebPushSubscriptions < ActiveRecord::Migration[8.0] | ||
| def change | ||
| create_table :coplan_web_push_subscriptions, id: :string, limit: 36 do |t| | ||
| t.string :user_id, null: false, limit: 36 | ||
| t.text :endpoint, null: false | ||
| # SHA256 hex digest of endpoint, used for unique constraint since the | ||
| # full endpoint can exceed indexable string limits (FCM endpoints can | ||
| # be 200-400 chars). | ||
| t.string :endpoint_digest, null: false, limit: 64 | ||
| t.string :p256dh_key, null: false, limit: 255 | ||
| t.string :auth_key, null: false, limit: 100 | ||
| t.string :user_agent | ||
| t.datetime :last_seen_at | ||
| t.datetime :last_delivered_at | ||
| t.integer :notifications_delivered_count, null: false, default: 0 | ||
| t.timestamps | ||
| end | ||
|
|
||
| add_index :coplan_web_push_subscriptions, :user_id | ||
| add_index :coplan_web_push_subscriptions, :endpoint_digest, unique: true | ||
| add_foreign_key :coplan_web_push_subscriptions, :coplan_users, column: :user_id | ||
| end | ||
| end |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| module CoPlan | ||
| class ServiceWorkersController < ApplicationController | ||
| # Service worker registration runs from the browser without our session | ||
| # cookie, and the SW URL needs to be public anyway. | ||
| skip_before_action :authenticate_coplan_user! | ||
| skip_before_action :verify_authenticity_token, raise: false | ||
|
|
||
| SW_PATH = CoPlan::Engine.root.join("app/javascript/coplan_service_worker.js") | ||
| SW_BODY = SW_PATH.read.freeze | ||
|
|
||
| def show | ||
| # No Service-Worker-Allowed header on purpose: default scope is the | ||
| # SW's own directory, which is the engine mount point. Push events | ||
| # fire regardless of scope, so this doesn't limit notification reach. | ||
| # | ||
| # Render inline rather than send_file: the JS lives inside the gem, | ||
| # so any reverse proxy that intercepts X-Sendfile (NGINX et al) won't | ||
| # reach it. The file is small and cached in memory at boot. | ||
| response.headers["Cache-Control"] = "no-cache" | ||
| render plain: SW_BODY, content_type: "application/javascript" | ||
| end | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| module CoPlan | ||
| module WebPush | ||
| class SubscriptionsController < ApplicationController | ||
| before_action :require_web_push_configured | ||
|
|
||
| # POST /web_push/subscription | ||
| # Body: { subscription: { endpoint, keys: { p256dh, auth } } } | ||
| def create | ||
| sub_params = subscription_params | ||
| record = WebPushSubscription.upsert_for( | ||
| user: current_user, | ||
| endpoint: sub_params[:endpoint], | ||
| p256dh_key: sub_params.dig(:keys, :p256dh), | ||
| auth_key: sub_params.dig(:keys, :auth), | ||
| user_agent: request.user_agent&.truncate(255) | ||
| ) | ||
| render json: { id: record.id, created_at: record.created_at }, status: :created | ||
| end | ||
|
|
||
| # DELETE /web_push/subscription | ||
| # Body: { subscription: { endpoint } } | ||
| def destroy | ||
| endpoint = subscription_params[:endpoint] | ||
| digest = WebPushSubscription.digest_for(endpoint) | ||
| # Scope to current_user so a leaked endpoint can't unsubscribe | ||
| # someone else. | ||
| deleted = current_user.web_push_subscriptions.where(endpoint_digest: digest).delete_all | ||
| head deleted.positive? ? :no_content : :not_found | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def subscription_params | ||
| params.require(:subscription).permit(:endpoint, keys: [:p256dh, :auth]) | ||
| end | ||
|
|
||
| def require_web_push_configured | ||
| head :service_unavailable unless CoPlan.configuration.web_push_configured? | ||
| end | ||
| end | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lockfile now lists only
arm64-darwin-25for macOS and dropsx86_64-darwin, which makes Bundler treat Intel Macs as unsupported when using a frozen/deployment lockfile. That blocksbundle installfor contributors or CI runners on Intel macOS until the platform is re-added toGemfile.lock.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 3f2aa35 via
bundle lock --add-platform x86_64-darwin arm64-darwin. Both are back in the lockfile so frozenbundle installworks on Intel macOS again. (The original drop was an artifact ofbundle installrunning on this arm64-darwin-25 machine.)