feat: autogenerate vapid keys for push notifications (#3128)

* feat: Autogenerate push notification keys
* add vapid service class and remove pushkey model
* add spec for vapid service
* unset vapid env keys
* Unset VAPID_PRIVATE_KEY env variable

Co-authored-by: Sojan Jose <sojan@chatwoot.com>
Co-authored-by: Vishnu Narayanan <vishnu@chatwoot.com>
This commit is contained in:
Santhosh C 2021-11-09 21:36:32 +05:30 committed by GitHub
parent 0c9b682329
commit 6cfd7d3836
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 86 additions and 8 deletions

View file

@ -28,6 +28,7 @@ class DashboardController < ActionController::Base
'ANALYTICS_HOST' 'ANALYTICS_HOST'
).merge( ).merge(
APP_VERSION: Chatwoot.config[:version], APP_VERSION: Chatwoot.config[:version],
VAPID_PUBLIC_KEY: VapidService.public_key,
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false')
) )
end end

View file

@ -34,5 +34,4 @@ describe('campaignMixin', () => {
const wrapper = shallowMount(Component); const wrapper = shallowMount(Component);
expect(wrapper.vm.isOnOffType).toBe(true); expect(wrapper.vm.isOnOffType).toBe(true);
}); });
}); });

View file

@ -2,7 +2,7 @@ import events from 'widget/api/events';
const state = { const state = {
isOpen: false, isOpen: false,
} };
const actions = { const actions = {
create: async (_, { name }) => { create: async (_, { name }) => {
@ -17,7 +17,7 @@ const actions = {
const mutations = { const mutations = {
toggleOpen($state) { toggleOpen($state) {
$state.isOpen = !$state.isOpen; $state.isOpen = !$state.isOpen;
} },
}; };
export default { export default {

View file

@ -43,7 +43,7 @@ class Notification::PushNotificationService
end end
def send_browser_push?(subscription) def send_browser_push?(subscription)
ENV['VAPID_PUBLIC_KEY'] && subscription.browser_push? VapidService.public_key && subscription.browser_push?
end end
def send_browser_push(subscription) def send_browser_push(subscription)
@ -56,8 +56,8 @@ class Notification::PushNotificationService
auth: subscription.subscription_attributes['auth'], auth: subscription.subscription_attributes['auth'],
vapid: { vapid: {
subject: push_url, subject: push_url,
public_key: ENV['VAPID_PUBLIC_KEY'], public_key: VapidService.public_key,
private_key: ENV['VAPID_PRIVATE_KEY'] private_key: VapidService.private_key
}, },
ssl_timeout: 5, ssl_timeout: 5,
open_timeout: 5, open_timeout: 5,

View file

@ -35,8 +35,8 @@
hostURL: '<%= ENV.fetch('FRONTEND_URL', '') %>', hostURL: '<%= ENV.fetch('FRONTEND_URL', '') %>',
fbAppId: '<%= ENV.fetch('FB_APP_ID', nil) %>', fbAppId: '<%= ENV.fetch('FB_APP_ID', nil) %>',
signupEnabled: '<%= @global_config['ENABLE_ACCOUNT_SIGNUP'] %>', signupEnabled: '<%= @global_config['ENABLE_ACCOUNT_SIGNUP'] %>',
<% if ENV['VAPID_PUBLIC_KEY'] %> <% if @global_config['VAPID_PUBLIC_KEY'] %>
vapidPublicKey: new Uint8Array(<%= Base64.urlsafe_decode64(ENV['VAPID_PUBLIC_KEY']).bytes %>), vapidPublicKey: new Uint8Array(<%= Base64.urlsafe_decode64(@global_config['VAPID_PUBLIC_KEY']).bytes %>),
<% end %> <% end %>
enabledLanguages: <%= available_locales_with_name.to_json.html_safe %>, enabledLanguages: <%= available_locales_with_name.to_json.html_safe %>,
selectedLocale: '<%= I18n.locale %>' selectedLocale: '<%= I18n.locale %>'

25
lib/vapid_service.rb Normal file
View file

@ -0,0 +1,25 @@
class VapidService
def self.public_key
vapid_keys['public_key']
end
def self.private_key
vapid_keys['private_key']
end
def self.vapid_keys
config = GlobalConfig.get('VAPID_KEYS')
return config['VAPID_KEYS'] if config['VAPID_KEYS'].present?
# keys don't exist in the database. so let's generate and save them
keys = Webpush.generate_key
# TODO: remove the logic on environment variables when we completely deprecate
public_key = ENV['VAPID_PUBLIC_KEY'] || keys.public_key
private_key = ENV['VAPID_PRIVATE_KEY'] || keys.private_key
i = InstallationConfig.where(name: 'VAPID_KEYS').first_or_create(value: { public_key: public_key, private_key: private_key })
i.value
end
private_class_method :vapid_keys
end

View file

@ -0,0 +1,53 @@
require 'rails_helper'
describe VapidService do
subject(:trigger) { described_class }
describe 'execute' do
context 'when called with default options' do
before do
GlobalConfig.clear_cache
end
it 'hit DB for the first call' do
expect(InstallationConfig).to receive(:find_by)
GlobalConfig.get('VAPID_KEYS')
end
it 'get public key from env' do
# this gets public key
ENV['VAPID_PUBLIC_KEY'] = 'test'
described_class.public_key
# subsequent calls should not hit DB
expect(InstallationConfig).not_to receive(:find_by)
described_class.public_key
ENV['VAPID_PUBLIC_KEY'] = nil
end
it 'get private key from env' do
# this gets private key
ENV['VAPID_PRIVATE_KEY'] = 'test'
described_class.private_key
# subsequent calls should not hit DB
expect(InstallationConfig).not_to receive(:find_by)
described_class.private_key
ENV['VAPID_PRIVATE_KEY'] = nil
ENV['VAPID_PRIVATE_KEY'] = nil
end
it 'clears cache and fetch from DB next time, when clear_cache is called' do
# this loads from DB and is cached
GlobalConfig.get('VAPID_KEYS')
# clears the cache
GlobalConfig.clear_cache
# should be loaded from DB
expect(InstallationConfig).to receive(:find_by).with({ name: 'VAPID_KEYS' }).and_return(nil)
GlobalConfig.get('VAPID_KEYS')
end
end
end
end