From 78c7316f2a8d5cb1f575b5c5c31343115059e888 Mon Sep 17 00:00:00 2001 From: Matthew Rider Date: Mon, 9 Sep 2024 09:42:13 +0200 Subject: [PATCH 01/23] Test drive creation of organizations by admin --- Gemfile.lock | 2 +- app/dashboards/organization_dashboard.rb | 16 +++++++++ config/routes.rb | 2 +- spec/requests/admin/organization_spec.rb | 41 ++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 spec/requests/admin/organization_spec.rb diff --git a/Gemfile.lock b/Gemfile.lock index 3744b362d..b4ce0b21b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -438,7 +438,7 @@ GEM PLATFORMS ruby - x86_64-linux + x86_64-linux-musl DEPENDENCIES active_model_otp diff --git a/app/dashboards/organization_dashboard.rb b/app/dashboards/organization_dashboard.rb index 35d5d626b..750ad530a 100644 --- a/app/dashboards/organization_dashboard.rb +++ b/app/dashboards/organization_dashboard.rb @@ -53,6 +53,22 @@ class OrganizationDashboard < Administrate::BaseDashboard whats_app_more_info_message ].freeze + FORM_ATTRIBUTES_NEW = %i[ + name + contact_person + business_plan + upgrade_discount + whats_app_profile_about + onboarding_data_protection_link + onboarding_data_processing_consent_additional_info + onboarding_imprint_link + onboarding_ask_for_additional_consent + onboarding_additional_consent_heading + onboarding_additional_consent_text + onboarding_allowed + channel_image + ].freeze + FORM_ATTRIBUTES_EDIT = %i[ name contact_person diff --git a/config/routes.rb b/config/routes.rb index a1ddacd82..6171ae2a6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -122,7 +122,7 @@ resources :messages, only: %i[index show destroy] resources :delayed_jobs, only: %i[index show destroy] resources :business_plans, only: %i[index show edit update] - resources :organizations, only: %i[index show edit update] + resources :organizations, only: %i[index show edit update new create] resources :stats, only: [:index] end end diff --git a/spec/requests/admin/organization_spec.rb b/spec/requests/admin/organization_spec.rb new file mode 100644 index 000000000..6341ef4b8 --- /dev/null +++ b/spec/requests/admin/organization_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Organization management' do + context 'POST /admin/organizations' do + subject { -> { post admin_organizations_path(as: user), params: params } } + + let(:params) { { organization: { business_plan_id: create(:business_plan).id } } } + context 'unauthenticated' do + let(:user) { nil } + + it 'renders not found ' do + subject.call + expect(response).to be_not_found + end + end + + context 'unauthorized' do + let(:user) { create(:user, admin: false) } + + it 'renders not found ' do + subject.call + expect(response).to be_not_found + end + end + + context 'authenticated and authorized' do + let(:user) { create(:user, admin: true) } + + it 'creates the organization' do + expect { subject.call }.to change(Organization, :count).from(0).to(1) + end + + it "redirect to organization's show page" do + subject.call + expect(response).to redirect_to(admin_organization_path(Organization.last)) + end + end + end +end From d710f09c874a25e0a442d5585f04ee65a170c491 Mon Sep 17 00:00:00 2001 From: Matthew Rider Date: Mon, 9 Sep 2024 10:57:38 +0200 Subject: [PATCH 02/23] Test drive adding email from address --- app/dashboards/organization_dashboard.rb | 3 +++ spec/requests/admin/organization_spec.rb | 28 ++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/app/dashboards/organization_dashboard.rb b/app/dashboards/organization_dashboard.rb index 750ad530a..3f49c465a 100644 --- a/app/dashboards/organization_dashboard.rb +++ b/app/dashboards/organization_dashboard.rb @@ -32,6 +32,7 @@ class OrganizationDashboard < Administrate::BaseDashboard whats_app_profile_about: Field::Text, signal_complete_onboarding_link: Field::Url, whats_app_quick_reply_button_text: Field::JSONB + email_from_address: Field::Email, }.freeze COLLECTION_ATTRIBUTES = %i[ @@ -51,6 +52,7 @@ class OrganizationDashboard < Administrate::BaseDashboard updated_at upgraded_business_plan_at whats_app_more_info_message + email_from_address ].freeze FORM_ATTRIBUTES_NEW = %i[ @@ -67,6 +69,7 @@ class OrganizationDashboard < Administrate::BaseDashboard onboarding_additional_consent_text onboarding_allowed channel_image + email_from_address ].freeze FORM_ATTRIBUTES_EDIT = %i[ diff --git a/spec/requests/admin/organization_spec.rb b/spec/requests/admin/organization_spec.rb index 6341ef4b8..e2c1eb608 100644 --- a/spec/requests/admin/organization_spec.rb +++ b/spec/requests/admin/organization_spec.rb @@ -6,7 +6,10 @@ context 'POST /admin/organizations' do subject { -> { post admin_organizations_path(as: user), params: params } } - let(:params) { { organization: { business_plan_id: create(:business_plan).id } } } + let(:business_plan) { create(:business_plan) } + let(:required_params) { { organization: { name: 'Find by my name', business_plan_id: business_plan.id } } } + let(:params) { required_params } + context 'unauthenticated' do let(:user) { nil } @@ -32,9 +35,30 @@ expect { subject.call }.to change(Organization, :count).from(0).to(1) end + it 'assigns the business plan to the organization' do + subject.call + organization = Organization.find_by(name: 'Find by my name') + expect(organization.business_plan).to eq(business_plan) + end + it "redirect to organization's show page" do subject.call - expect(response).to redirect_to(admin_organization_path(Organization.last)) + organization = Organization.find_by(name: 'Find by my name') + expect(response).to redirect_to(admin_organization_path(organization)) + end + + context 'Email' do + let(:params) do + required_params.deep_merge({ organization: { name: 'I have an email', + email_from_address: 'redaktion@100ey.es' } }) + end + + it 'allows configuring email_from_address' do + subject.call + follow_redirect! + expect(page).to have_content('I have an email') + expect(page).to have_content('redaktion@100ey.es') + end end end end From b818bd4a6750e23f66c8f9d6ef45cbbcd9a71427 Mon Sep 17 00:00:00 2001 From: Matthew Rider Date: Mon, 9 Sep 2024 11:20:59 +0200 Subject: [PATCH 03/23] Test drive configuring telegram when creating an org --- app/dashboards/organization_dashboard.rb | 5 +++++ spec/requests/admin/organization_spec.rb | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/app/dashboards/organization_dashboard.rb b/app/dashboards/organization_dashboard.rb index 3f49c465a..b16af20d4 100644 --- a/app/dashboards/organization_dashboard.rb +++ b/app/dashboards/organization_dashboard.rb @@ -33,6 +33,8 @@ class OrganizationDashboard < Administrate::BaseDashboard signal_complete_onboarding_link: Field::Url, whats_app_quick_reply_button_text: Field::JSONB email_from_address: Field::Email, + telegram_bot_username: Field::Text, + telegram_bot_api_key: Field::Text }.freeze COLLECTION_ATTRIBUTES = %i[ @@ -53,6 +55,7 @@ class OrganizationDashboard < Administrate::BaseDashboard upgraded_business_plan_at whats_app_more_info_message email_from_address + telegram_bot_username ].freeze FORM_ATTRIBUTES_NEW = %i[ @@ -70,6 +73,8 @@ class OrganizationDashboard < Administrate::BaseDashboard onboarding_allowed channel_image email_from_address + telegram_bot_username + telegram_bot_api_key ].freeze FORM_ATTRIBUTES_EDIT = %i[ diff --git a/spec/requests/admin/organization_spec.rb b/spec/requests/admin/organization_spec.rb index e2c1eb608..a70b7ebed 100644 --- a/spec/requests/admin/organization_spec.rb +++ b/spec/requests/admin/organization_spec.rb @@ -60,6 +60,22 @@ expect(page).to have_content('redaktion@100ey.es') end end + + context 'Telegram' do + let(:params) do + required_params.deep_merge({ organization: { telegram_bot_api_key: 'valid_api_key', + telegram_bot_username: 'unique_username_bot' } }) + end + + it 'allows configuring Telegram' do + subject.call + follow_redirect! + expect(page).to have_content('unique_username_bot') + expect(page).not_to have_content('valid_api_key') + organization = Organization.find_by(telegram_bot_username: 'unique_username_bot') + expect(organization.telegram_bot_api_key).to eq('valid_api_key') + end + end end end end From 588beaefcbdbb1a1ce2e351bd44373b6dea50303 Mon Sep 17 00:00:00 2001 From: Matthew Rider Date: Mon, 9 Sep 2024 11:32:01 +0200 Subject: [PATCH 04/23] Test drive configuring Threema at org creation --- app/dashboards/organization_dashboard.rb | 9 ++++++++- spec/requests/admin/organization_spec.rb | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/app/dashboards/organization_dashboard.rb b/app/dashboards/organization_dashboard.rb index b16af20d4..18246afd8 100644 --- a/app/dashboards/organization_dashboard.rb +++ b/app/dashboards/organization_dashboard.rb @@ -34,7 +34,10 @@ class OrganizationDashboard < Administrate::BaseDashboard whats_app_quick_reply_button_text: Field::JSONB email_from_address: Field::Email, telegram_bot_username: Field::Text, - telegram_bot_api_key: Field::Text + telegram_bot_api_key: Field::Text, + threemarb_api_identity: Field::Text, + threemarb_api_secret: Field::Password, + threemarb_private: Field::Password, }.freeze COLLECTION_ATTRIBUTES = %i[ @@ -56,6 +59,7 @@ class OrganizationDashboard < Administrate::BaseDashboard whats_app_more_info_message email_from_address telegram_bot_username + threemarb_api_identity ].freeze FORM_ATTRIBUTES_NEW = %i[ @@ -75,6 +79,9 @@ class OrganizationDashboard < Administrate::BaseDashboard email_from_address telegram_bot_username telegram_bot_api_key + threemarb_api_identity + threemarb_api_secret + threemarb_private ].freeze FORM_ATTRIBUTES_EDIT = %i[ diff --git a/spec/requests/admin/organization_spec.rb b/spec/requests/admin/organization_spec.rb index a70b7ebed..e66a4a1c4 100644 --- a/spec/requests/admin/organization_spec.rb +++ b/spec/requests/admin/organization_spec.rb @@ -76,6 +76,25 @@ expect(organization.telegram_bot_api_key).to eq('valid_api_key') end end + + context 'Threema' do + let(:params) do + required_params.deep_merge({ organization: { threemarb_api_identity: '*APIIDENT', + threemarb_api_secret: 'valid_secret', + threemarb_private: 'valid_private_key' } }) + end + + it 'allows configuring Threema' do + subject.call + follow_redirect! + expect(page).to have_content('*APIIDENT') + expect(page).not_to have_content('valid_secret') + expect(page).not_to have_content('valid_private_key') + + organization = Organization.find_by(threemarb_api_identity: '*APIIDENT') + expect(organization).to have_attributes(threemarb_api_secret: 'valid_secret', threemarb_private: 'valid_private_key') + end + end end end end From 4ff2168adb16615fd5b6b849329f3accc437a3c4 Mon Sep 17 00:00:00 2001 From: Matthew Rider Date: Mon, 9 Sep 2024 11:33:01 +0200 Subject: [PATCH 05/23] Lint, improve dashboard field types --- app/dashboards/organization_dashboard.rb | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/app/dashboards/organization_dashboard.rb b/app/dashboards/organization_dashboard.rb index 18246afd8..729ee3278 100644 --- a/app/dashboards/organization_dashboard.rb +++ b/app/dashboards/organization_dashboard.rb @@ -14,12 +14,6 @@ class OrganizationDashboard < Administrate::BaseDashboard created_at: Field::DateTime, updated_at: Field::DateTime, upgraded_business_plan_at: Field::DateTime, - threemarb_api_identity: Field::String, - threemarb_api_secret: Field::String, - threemarb_private: Field::String, - twilio_account_sid: Field::String, - twilio_api_key_sid: Field::String, - twilio_api_key_secret: Field::String, onboarding_allowed: Field::JSONB, onboarding_data_protection_link: Field::Url, onboarding_data_processing_consent_additional_info: Field::Text, @@ -33,9 +27,9 @@ class OrganizationDashboard < Administrate::BaseDashboard signal_complete_onboarding_link: Field::Url, whats_app_quick_reply_button_text: Field::JSONB email_from_address: Field::Email, - telegram_bot_username: Field::Text, - telegram_bot_api_key: Field::Text, - threemarb_api_identity: Field::Text, + telegram_bot_username: Field::String, + telegram_bot_api_key: Field::Password, + threemarb_api_identity: Field::String, threemarb_api_secret: Field::Password, threemarb_private: Field::Password, }.freeze From 9b7887bfc5721ec40ef5656cb23a21d0aa661552 Mon Sep 17 00:00:00 2001 From: Matthew Rider Date: Mon, 9 Sep 2024 11:43:10 +0200 Subject: [PATCH 06/23] Test drive Twilio WhatsApp configuration --- app/dashboards/organization_dashboard.rb | 7 ++++++ spec/requests/admin/organization_spec.rb | 27 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/app/dashboards/organization_dashboard.rb b/app/dashboards/organization_dashboard.rb index 729ee3278..a5b3a24ed 100644 --- a/app/dashboards/organization_dashboard.rb +++ b/app/dashboards/organization_dashboard.rb @@ -54,6 +54,9 @@ class OrganizationDashboard < Administrate::BaseDashboard email_from_address telegram_bot_username threemarb_api_identity + whats_app_server_phone_number + twilio_account_sid + twilio_api_key_sid ].freeze FORM_ATTRIBUTES_NEW = %i[ @@ -76,6 +79,10 @@ class OrganizationDashboard < Administrate::BaseDashboard threemarb_api_identity threemarb_api_secret threemarb_private + whats_app_server_phone_number + twilio_account_sid + twilio_api_key_sid + twilio_api_key_secret ].freeze FORM_ATTRIBUTES_EDIT = %i[ diff --git a/spec/requests/admin/organization_spec.rb b/spec/requests/admin/organization_spec.rb index e66a4a1c4..1ec20d0f1 100644 --- a/spec/requests/admin/organization_spec.rb +++ b/spec/requests/admin/organization_spec.rb @@ -95,6 +95,33 @@ expect(organization).to have_attributes(threemarb_api_secret: 'valid_secret', threemarb_private: 'valid_private_key') end end + + context 'WhatsApp Twilio' do + let(:whats_app_params) do + { + whats_app_server_phone_number: '+4912345678', + twilio_account_sid: 'valid_account_sid', + twilio_api_key_sid: 'valid_api_key_sid', + twilio_api_key_secret: 'valid_secret' + } + end + let(:params) do + required_params.deep_merge({ organization: whats_app_params }) + end + + it 'allows configuring Twilio WhatsApp' do + subject.call + follow_redirect! + + expect(page).to have_content('+4912345678') + expect(page).to have_content('valid_account_sid') + expect(page).to have_content('valid_api_key_sid') + expect(page).not_to have_content('valid_secret') + + organization = Organization.find_by(whats_app_server_phone_number: '+4912345678') + expect(organization).to have_attributes(whats_app_params) + end + end end end end From 2c0604fc636ec9e4b07971b475b590eba774d6d2 Mon Sep 17 00:00:00 2001 From: Matthew Rider Date: Mon, 9 Sep 2024 11:45:00 +0200 Subject: [PATCH 07/23] Improve descriptions of specs --- spec/requests/admin/organization_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/requests/admin/organization_spec.rb b/spec/requests/admin/organization_spec.rb index 1ec20d0f1..7a9ff5167 100644 --- a/spec/requests/admin/organization_spec.rb +++ b/spec/requests/admin/organization_spec.rb @@ -53,7 +53,7 @@ email_from_address: 'redaktion@100ey.es' } }) end - it 'allows configuring email_from_address' do + it 'allows configuration of Email specific attrs' do subject.call follow_redirect! expect(page).to have_content('I have an email') @@ -67,7 +67,7 @@ telegram_bot_username: 'unique_username_bot' } }) end - it 'allows configuring Telegram' do + it 'allows configuration of Telegram specific attrs' do subject.call follow_redirect! expect(page).to have_content('unique_username_bot') @@ -84,7 +84,7 @@ threemarb_private: 'valid_private_key' } }) end - it 'allows configuring Threema' do + it 'allows configuration of Threema specific attrs' do subject.call follow_redirect! expect(page).to have_content('*APIIDENT') @@ -109,7 +109,7 @@ required_params.deep_merge({ organization: whats_app_params }) end - it 'allows configuring Twilio WhatsApp' do + it 'allows configuration of Twilio specific attrs' do subject.call follow_redirect! From b551f68c9600bd6e8cc90a9e7e0d9ec6d05fc731 Mon Sep 17 00:00:00 2001 From: Matthew Rider Date: Tue, 10 Sep 2024 20:34:13 +0200 Subject: [PATCH 08/23] Add steps to add and register signal phone number at the organization level --- .../signal_controller.rb | 8 +- app/jobs/signal_adapter/set_trust_mode_job.rb | 38 ++++ .../signal/captcha_form.html.erb | 31 +++ app/views/organizations/signal/edit.html.erb | 15 ++ .../organizations/signal/verify_form.html.erb | 16 ++ config/routes.rb | 11 + spec/requests/organizations/signal_spec.rb | 206 ++++++++++++++++++ 7 files changed, 321 insertions(+), 4 deletions(-) rename app/controllers/{settings => organizations}/signal_controller.rb (90%) create mode 100644 app/jobs/signal_adapter/set_trust_mode_job.rb create mode 100644 app/views/organizations/signal/captcha_form.html.erb create mode 100644 app/views/organizations/signal/edit.html.erb create mode 100644 app/views/organizations/signal/verify_form.html.erb create mode 100644 spec/requests/organizations/signal_spec.rb diff --git a/app/controllers/settings/signal_controller.rb b/app/controllers/organizations/signal_controller.rb similarity index 90% rename from app/controllers/settings/signal_controller.rb rename to app/controllers/organizations/signal_controller.rb index 61b29827c..45861fc56 100644 --- a/app/controllers/settings/signal_controller.rb +++ b/app/controllers/organizations/signal_controller.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -module Settings +module Organizations class SignalController < ApplicationController def edit; end def update if @organization.update(update_params) - redirect_to settings_register_path + redirect_to organization_signal_register_path(@organization) else render :edit, status: :unprocessable_entity end @@ -26,7 +26,7 @@ def register end case response when Net::HTTPSuccess - redirect_to settings_verify_path + redirect_to organization_signal_verify_path else handle_error_response(response) end @@ -46,7 +46,7 @@ def verify end case response when Net::HTTPSuccess - Signal::SetTrustModeJob.perform_later(signal_server_phone_number: signal_server_phone_number) + SignalAdapter::SetTrustModeJob.perform_later(signal_server_phone_number: signal_server_phone_number) else handle_error_response(response) end diff --git a/app/jobs/signal_adapter/set_trust_mode_job.rb b/app/jobs/signal_adapter/set_trust_mode_job.rb new file mode 100644 index 000000000..70aa9193c --- /dev/null +++ b/app/jobs/signal_adapter/set_trust_mode_job.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'net/http' + +module SignalAdapter + class SetTrustModeJob < ApplicationJob + def perform(signal_server_phone_number:) + uri = URI.parse("#{ENV.fetch('SIGNAL_CLI_REST_API_ENDPOINT', 'http://localhost:8080')}/v1/configuration/#{signal_server_phone_number}/settings") + request = Net::HTTP::Post.new(uri, { + Accept: 'application/json', + 'Content-Type': 'application/json' + }) + request.body = { trust_mode: 'always' }.to_json + response = Net::HTTP.start(uri.host, uri.port) do |http| + http.request(request) + end + case response + when Net::HTTPSuccess + Rails.logger.debug 'Updated config' + else + handle_error(JSON.parse(response.body)['error']) + end + end + + private + + def handle_error(error_message) + exception = SignalAdapter::BadRequestError.new(error_code: response.code, message: error_message) + context = { + code: response.code, + message: response.message, + headers: response.to_hash, + body: error_message + } + ErrorNotifier.report(exception, context: context) + end + end +end diff --git a/app/views/organizations/signal/captcha_form.html.erb b/app/views/organizations/signal/captcha_form.html.erb new file mode 100644 index 000000000..ccb1f0a1f --- /dev/null +++ b/app/views/organizations/signal/captcha_form.html.erb @@ -0,0 +1,31 @@ +<%= c 'main' do %> + <%= c 'page_header', styles: [:wide, :inheritBackgroundColor, :xlargePaddingTop, :spaceBetween] do %> + <%= c 'heading' do %> + <%= "Register Signal Number" %> + <% end %> + <% end %> + + <%= c 'section', styles: [:wide, :largeBottomMargin] do %> +
+ <%= c 'heading', tag: :h2 do %> + Generate a captcha + <% end %> +

To get the token, go to https://signalcaptchas.org/registration/generate.html

+ +

If the token from that page doesn't work, you can try https://signalcaptchas.org/challenge/generate.html

+ +

After filling the captcha, the site doesn't immediately show the token but tries to redirect to a signalcaptcha:// url that contains the token.

+

+ After a short moment, a link will appear underneath the captcha called "Open Signal". + Get the token by right clicking on it and clicking: copy url. You can paste that directly into: +

+
+ <%= c 'form', model: @organization, url: organization_signal_register_path(@organization), method: :post do %> + <%= c 'base_field', id: 'organization[signal][captcha]', label: 'Captcha' do |field| %> + <% c 'input', id: 'organization[signal][captcha]', value: nil %> + <% end %> + + <%= c 'submit_button', label: t('save') %> + <% end %> + <% end %> +<% end %> diff --git a/app/views/organizations/signal/edit.html.erb b/app/views/organizations/signal/edit.html.erb new file mode 100644 index 000000000..d3b1f41c0 --- /dev/null +++ b/app/views/organizations/signal/edit.html.erb @@ -0,0 +1,15 @@ +<%= c 'main' do %> + <%= c 'page_header', styles: [:wide, :inheritBackgroundColor, :xlargePaddingTop, :spaceBetween] do %> + <%= c 'heading' do %> + <%= "Update organization with Signal Number" %> + <% end %> + <% end %> + <%= c 'section', styles: [:wide, :largeBottomMargin] do %> + <%= c 'form', model: @organization, url: organization_signal_add_path(@organization) do %> + <%= c 'field', object: @organization, attr: :signal_server_phone_number do |field| %> + <% c 'input', field.input_defaults.merge(required: true) %> + <% end %> + <%= c 'submit_button', label: t('save') %> + <% end %> + <% end %> +<% end %> diff --git a/app/views/organizations/signal/verify_form.html.erb b/app/views/organizations/signal/verify_form.html.erb new file mode 100644 index 000000000..c4381bc6f --- /dev/null +++ b/app/views/organizations/signal/verify_form.html.erb @@ -0,0 +1,16 @@ + +<%= c 'main' do %> + <%= c 'page_header', styles: [:wide, :inheritBackgroundColor, :xlargePaddingTop, :spaceBetween] do %> + <%= c 'heading' do %> + <%= "Verify Signal Number" %> + <% end %> + <% end %> + <%= c 'section', styles: [:wide, :largeBottomMargin] do %> + <%= c 'form', model: @organization, url: organization_signal_verify_path(@organization), method: :post do %> + <%= c 'base_field', id: 'organization[signal][token]', label: 'Captcha' do |field| %> + <% c 'input', id: 'organization[signal][token]' %> + <% end %> + <%= c 'submit_button', label: t('save') %> + <% end %> + <% end %> +<% end %> diff --git a/config/routes.rb b/config/routes.rb index 6171ae2a6..7237289a1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -92,6 +92,17 @@ put '/profile/upgrade_business_plan', to: 'profile#upgrade_business_plan' get '/about', to: 'about#index' + + constraints Clearance::Constraints::SignedIn.new(&:admin?) do + scope module: 'organizations' do + get '/signal/add', to: 'signal#edit' + patch '/signal/add', to: 'signal#update' + get '/signal/register', to: 'signal#captcha_form' + post '/signal/register', to: 'signal#register' + get '/signal/verify', to: 'signal#verify_form' + post '/signal/verify', to: 'signal#verify' + end + end end get '/health', to: 'health#index' diff --git a/spec/requests/organizations/signal_spec.rb b/spec/requests/organizations/signal_spec.rb new file mode 100644 index 000000000..b9b6dba3d --- /dev/null +++ b/spec/requests/organizations/signal_spec.rb @@ -0,0 +1,206 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'webmock/rspec' + +RSpec.describe 'Signal' do + let(:organization) { create(:organization, signal_server_phone_number: nil) } + + describe 'GET /:organization_id/add-signal' do + subject { -> { get organization_signal_add_path(organization, as: user) } } + + context 'unauthenticated' do + let(:user) { nil } + + it 'renders not found ' do + subject.call + expect(response).to be_not_found + end + end + + context 'unauthorized' do + let(:user) { create(:user, admin: false, organizations: [organization]) } + + it 'renders not found ' do + subject.call + expect(response).to be_not_found + end + end + + context 'authenticated and authorized' do + let(:user) { create(:user, admin: true) } + + it 'should be successful' do + subject.call + expect(response).to be_successful + end + + it 'renders a form to add signal_server_phone_number' do + subject.call + expect(page).to have_css("form[action='/#{organization.id}/signal/add']") do |form| + expect(form).to have_css('input[id="organization[signal_server_phone_number]"]') + end + end + end + end + + describe 'PATCH /:organization_id/add-signal' do + subject { -> { patch organization_signal_add_path(organization, as: user), params: params } } + + let(:params) { { organization: { signal_server_phone_number: '+4912345678' } } } + + context 'unauthenticated' do + let(:user) { nil } + + it 'renders not found ' do + subject.call + expect(response).to be_not_found + end + end + + context 'unauthorized' do + let(:user) { create(:user, admin: false, organizations: [organization]) } + + it 'renders not found ' do + subject.call + expect(response).to be_not_found + end + end + + context 'authenticated and authorized' do + let(:user) { create(:user, admin: true) } + + it "updates the organization's signal_server_phone_number" do + expect { subject.call }.to (change { organization.reload.signal_server_phone_number }).from(nil).to('+4912345678') + end + + it 'redirects to /signal/register' do + subject.call + expect(response).to redirect_to(organization_signal_register_path(organization)) + end + + it 'renders a form to register' do + subject.call + follow_redirect! + expect(page).to have_css("form[action='/#{organization.id}/signal/register']") do |form| + expect(form).to have_css('input[id="organization[signal][captcha]"]') + end + end + end + end + + describe 'POST /:organization_id/signal/register' do + subject { -> { post organization_signal_register_path(organization, as: user), params: params } } + + let(:params) { { organization: { signal: { captcha: 'signalcaptcha://signal-hcaptcha.valid-captcha' } } } } + let(:uri) { URI.parse('http://signal:8080/v1/register/+4912345678') } + + before do + organization.update!(signal_server_phone_number: '+4912345678') + stub_request(:post, uri).to_return(status: 201) + end + + context 'unauthenticated' do + let(:user) { nil } + + it 'renders not found ' do + subject.call + expect(response).to be_not_found + end + end + + context 'unauthorized' do + let(:user) { create(:user, admin: false, organizations: [organization]) } + + it 'renders not found ' do + subject.call + expect(response).to be_not_found + end + end + + context 'authenticated and authorized' do + let(:user) { create(:user, admin: true) } + + it 'redirects to /signal/verify' do + subject.call + expect(response).to redirect_to(organization_signal_verify_path(organization)) + end + + it 'renders a form to verify' do + subject.call + follow_redirect! + expect(page).to have_css("form[action='/#{organization.id}/signal/verify']") do |form| + expect(form).to have_css('input[id="organization[signal][token]"]') + end + end + + context 'given the register is unsucessful' do + let(:error_message) { 'Invalid captcha' } + + before do + allow(Sentry).to receive(:capture_exception) + stub_request(:post, uri).to_return(status: 400, body: { error: error_message }.to_json) + end + + it 'reports the error' do + expect(Sentry).to receive(:capture_exception).with(SignalAdapter::BadRequestError.new(error_code: 400, message: error_message)) + + subject.call + end + end + end + end + + describe 'POST /:organization_id/signal/verify' do + subject { -> { post organization_signal_verify_path(organization, as: user), params: params } } + + let(:params) { { organization: { signal: { token: '123456' } } } } + let(:uri) { URI.parse('http://signal:8080/v1/register/+4912345678/verify/123456') } + + before do + organization.update!(signal_server_phone_number: '+4912345678') + stub_request(:post, uri).to_return(status: 201) + end + + context 'unauthenticated' do + let(:user) { nil } + + it 'renders not found ' do + subject.call + expect(response).to be_not_found + end + end + + context 'unauthorized' do + let(:user) { create(:user, admin: false, organizations: [organization]) } + + it 'renders not found ' do + subject.call + expect(response).to be_not_found + end + end + + context 'authenticated and authorized' do + let(:user) { create(:user, admin: true) } + + it 'schedules a job to set trust mode to always' do + expect { subject.call }.to have_enqueued_job(SignalAdapter::SetTrustModeJob).with(signal_server_phone_number: '+4912345678') + end + + context 'given the verify is unsucessful' do + let(:error_message) { 'Verify error: StatusCode: 400' } + + before do + allow(Sentry).to receive(:capture_exception) + stub_request(:post, uri).to_return(status: 400, body: { error: error_message }.to_json) + end + + it 'reports the error' do + expect(Sentry).to receive(:capture_exception).with(SignalAdapter::BadRequestError.new(error_code: 400, message: error_message)) + + subject.call + end + end + end + end +end From 9167777417131648e6aaf38a2def95c1a90e89e4 Mon Sep 17 00:00:00 2001 From: Matthew Rider Date: Wed, 11 Sep 2024 09:31:13 +0200 Subject: [PATCH 09/23] Fix specs to work with both docker and local --- spec/requests/organizations/signal_spec.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/spec/requests/organizations/signal_spec.rb b/spec/requests/organizations/signal_spec.rb index b9b6dba3d..7d0f71781 100644 --- a/spec/requests/organizations/signal_spec.rb +++ b/spec/requests/organizations/signal_spec.rb @@ -93,10 +93,12 @@ subject { -> { post organization_signal_register_path(organization, as: user), params: params } } let(:params) { { organization: { signal: { captcha: 'signalcaptcha://signal-hcaptcha.valid-captcha' } } } } - let(:uri) { URI.parse('http://signal:8080/v1/register/+4912345678') } + let(:uri) { URI.parse('http://localhost:8080/v1/register/+4912345678') } before do organization.update!(signal_server_phone_number: '+4912345678') + + allow(ENV).to receive(:fetch).with('SIGNAL_CLI_REST_API_ENDPOINT', 'http://localhost:8080').and_return('http://localhost:8080') stub_request(:post, uri).to_return(status: 201) end @@ -155,10 +157,12 @@ subject { -> { post organization_signal_verify_path(organization, as: user), params: params } } let(:params) { { organization: { signal: { token: '123456' } } } } - let(:uri) { URI.parse('http://signal:8080/v1/register/+4912345678/verify/123456') } + let(:uri) { URI.parse('http://localhost:8080/v1/register/+4912345678/verify/123456') } before do organization.update!(signal_server_phone_number: '+4912345678') + + allow(ENV).to receive(:fetch).with('SIGNAL_CLI_REST_API_ENDPOINT', 'http://localhost:8080').and_return('http://localhost:8080') stub_request(:post, uri).to_return(status: 201) end From 433c8b300c8278ae270d2ce79a2b4e033dbac3d0 Mon Sep 17 00:00:00 2001 From: Matthew Rider Date: Tue, 15 Oct 2024 12:00:50 +0200 Subject: [PATCH 10/23] Remove Twilio for new orgs, update field types --- app/dashboards/organization_dashboard.rb | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/app/dashboards/organization_dashboard.rb b/app/dashboards/organization_dashboard.rb index a5b3a24ed..b45d1bcd6 100644 --- a/app/dashboards/organization_dashboard.rb +++ b/app/dashboards/organization_dashboard.rb @@ -28,10 +28,10 @@ class OrganizationDashboard < Administrate::BaseDashboard whats_app_quick_reply_button_text: Field::JSONB email_from_address: Field::Email, telegram_bot_username: Field::String, - telegram_bot_api_key: Field::Password, + telegram_bot_api_key: Field::String, threemarb_api_identity: Field::String, - threemarb_api_secret: Field::Password, - threemarb_private: Field::Password, + threemarb_api_secret: Field::String, + threemarb_private: Field::String }.freeze COLLECTION_ATTRIBUTES = %i[ @@ -54,9 +54,6 @@ class OrganizationDashboard < Administrate::BaseDashboard email_from_address telegram_bot_username threemarb_api_identity - whats_app_server_phone_number - twilio_account_sid - twilio_api_key_sid ].freeze FORM_ATTRIBUTES_NEW = %i[ @@ -79,10 +76,6 @@ class OrganizationDashboard < Administrate::BaseDashboard threemarb_api_identity threemarb_api_secret threemarb_private - whats_app_server_phone_number - twilio_account_sid - twilio_api_key_sid - twilio_api_key_secret ].freeze FORM_ATTRIBUTES_EDIT = %i[ From e203ca0e55d598de50f4cbee29c4eb5fad067a12 Mon Sep 17 00:00:00 2001 From: Matthew Rider Date: Tue, 15 Oct 2024 12:02:31 +0200 Subject: [PATCH 11/23] Allow org to be created without Telegram as a channel --- app/models/organization.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/organization.rb b/app/models/organization.rb index 44b6adbac..a3e53baf1 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -23,7 +23,7 @@ class Organization < ApplicationRecord before_update :notify_admin after_update_commit :notify_admin_of_welcome_message_change - validates :telegram_bot_username, uniqueness: true + validates :telegram_bot_username, uniqueness: true, allow_nil: true def channels_onboarding_allowed { From ad2e0d0c51e30b1727d6342dbd74d140970c8419 Mon Sep 17 00:00:00 2001 From: Matthew Rider Date: Tue, 15 Oct 2024 12:54:13 +0200 Subject: [PATCH 12/23] Set telegram webhook url on org creation - when a it is created with telegram config --- app/jobs/telegram_adapter/set_webhook_url_job.rb | 13 +++++++++++++ app/models/organization.rb | 7 +++++++ 2 files changed, 20 insertions(+) create mode 100644 app/jobs/telegram_adapter/set_webhook_url_job.rb diff --git a/app/jobs/telegram_adapter/set_webhook_url_job.rb b/app/jobs/telegram_adapter/set_webhook_url_job.rb new file mode 100644 index 000000000..033e42ce5 --- /dev/null +++ b/app/jobs/telegram_adapter/set_webhook_url_job.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module TelegramAdapter + class SetWebhookUrlJob < ApplicationJob + def perform(organization_id:) + organization = Organization.find(organization_id) + + bot = Telegram::Bot::Client.new(organization.telegram_bot_api_key) + path = "telegram/#{Telegram::Bot::RoutesHelper.token_hash(organization.telegram_bot_api_key)}" + bot.set_webhook(url: "https://#{ENV.fetch('APPLICATION_HOSTNAME', 'localhost:3000')}/#{path}") + end + end +end diff --git a/app/models/organization.rb b/app/models/organization.rb index a3e53baf1..b66358b26 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -21,6 +21,7 @@ class Organization < ApplicationRecord has_one_attached :channel_image before_update :notify_admin + after_create_commit :set_telegram_webhook after_update_commit :notify_admin_of_welcome_message_change validates :telegram_bot_username, uniqueness: true, allow_nil: true @@ -116,6 +117,12 @@ def contributors_tags_with_count private + def set_telegram_webhook + return unless saved_change_to_telegram_bot_username? + + TelegramAdapter::SetWebhookUrlJob.perform_later(organization_id: id) + end + def notify_admin return unless business_plan_id_changed? && upgraded_business_plan_at.present? From 041cdd5cd4824cd912d413205741446b0784e59e Mon Sep 17 00:00:00 2001 From: Matthew Rider Date: Wed, 16 Oct 2024 11:04:34 +0200 Subject: [PATCH 13/23] Move adding signal phone number to admin portal --- .../organizations/signal_controller.rb | 10 ---------- app/dashboards/organization_dashboard.rb | 11 ++++++++--- app/models/organization.rb | 3 +++ app/views/settings/signal/edit.html.erb | 17 ----------------- config/routes.rb | 2 -- 5 files changed, 11 insertions(+), 32 deletions(-) delete mode 100644 app/views/settings/signal/edit.html.erb diff --git a/app/controllers/organizations/signal_controller.rb b/app/controllers/organizations/signal_controller.rb index 45861fc56..9073f025b 100644 --- a/app/controllers/organizations/signal_controller.rb +++ b/app/controllers/organizations/signal_controller.rb @@ -2,16 +2,6 @@ module Organizations class SignalController < ApplicationController - def edit; end - - def update - if @organization.update(update_params) - redirect_to organization_signal_register_path(@organization) - else - render :edit, status: :unprocessable_entity - end - end - def captcha_form; end def register diff --git a/app/dashboards/organization_dashboard.rb b/app/dashboards/organization_dashboard.rb index b45d1bcd6..bff4cedd6 100644 --- a/app/dashboards/organization_dashboard.rb +++ b/app/dashboards/organization_dashboard.rb @@ -31,14 +31,17 @@ class OrganizationDashboard < Administrate::BaseDashboard telegram_bot_api_key: Field::String, threemarb_api_identity: Field::String, threemarb_api_secret: Field::String, - threemarb_private: Field::String + threemarb_private: Field::String, + signal_server_phone_number: Field::String }.freeze COLLECTION_ATTRIBUTES = %i[ name contact_person - contributors - users + email_from_address + telegram_bot_username + threemarb_api_identity + signal_server_phone_number ].freeze SHOW_PAGE_ATTRIBUTES = %i[ @@ -54,6 +57,7 @@ class OrganizationDashboard < Administrate::BaseDashboard email_from_address telegram_bot_username threemarb_api_identity + signal_server_phone_number ].freeze FORM_ATTRIBUTES_NEW = %i[ @@ -76,6 +80,7 @@ class OrganizationDashboard < Administrate::BaseDashboard threemarb_api_identity threemarb_api_secret threemarb_private + signal_server_phone_number ].freeze FORM_ATTRIBUTES_EDIT = %i[ diff --git a/app/models/organization.rb b/app/models/organization.rb index b66358b26..d7bb2eb4a 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -24,7 +24,10 @@ class Organization < ApplicationRecord after_create_commit :set_telegram_webhook after_update_commit :notify_admin_of_welcome_message_change + phony_normalize :signal_server_phone_number, default_country_code: 'DE' + validates :telegram_bot_username, uniqueness: true, allow_nil: true + validates :signal_server_phone_number, phony_plausible: true def channels_onboarding_allowed { diff --git a/app/views/settings/signal/edit.html.erb b/app/views/settings/signal/edit.html.erb deleted file mode 100644 index 2e9950892..000000000 --- a/app/views/settings/signal/edit.html.erb +++ /dev/null @@ -1,17 +0,0 @@ -<%= c 'main' do %> - <%= c 'page_header', styles: [:wide, :inheritBackgroundColor, :xlargePaddingTop, :spaceBetween] do %> - <%= c 'heading' do %> - <%= "Update organization with Signal Number" %> - <% end %> - <% end %> - - <%= c 'section', styles: [:wide, :largeBottomMargin] do %> - <%= c 'form', model: @organization, url: settings_signal_server_phone_number_path do %> - <%= c 'field', object: @organization, attr: :signal_server_phone_number do |field| %> - <% c 'input', field.input_defaults.merge(required: true) %> - <% end %> - - <%= c 'submit_button', label: t('save') %> - <% end %> - <% end %> -<% end %> diff --git a/config/routes.rb b/config/routes.rb index 7237289a1..71189f0c7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -95,8 +95,6 @@ constraints Clearance::Constraints::SignedIn.new(&:admin?) do scope module: 'organizations' do - get '/signal/add', to: 'signal#edit' - patch '/signal/add', to: 'signal#update' get '/signal/register', to: 'signal#captcha_form' post '/signal/register', to: 'signal#register' get '/signal/verify', to: 'signal#verify_form' From 0b9817a10b64f2b3cacfe41984e3c3de8c553b5d Mon Sep 17 00:00:00 2001 From: Matthew Rider Date: Wed, 16 Oct 2024 11:06:14 +0200 Subject: [PATCH 14/23] Update admin root to organizations#index --- config/routes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 71189f0c7..002a2ef1d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -121,7 +121,7 @@ namespace :admin do constraints Clearance::Constraints::SignedIn.new(&:admin?) do - root to: 'users#index' + root to: 'organizations#index' resources :users resources :contributors, only: %i[index show edit update destroy] do From 740e3a937f5f4ab309ff50d0a8e931df029d59f1 Mon Sep 17 00:00:00 2001 From: Matthew Rider Date: Wed, 16 Oct 2024 11:37:03 +0200 Subject: [PATCH 15/23] Add link to register signal --- app/dashboards/organization_dashboard.rb | 2 +- app/fields/setup_signal_link_field.rb | 13 +++++++++++++ .../fields/setup_signal_link_field/_form.html.erb | 6 ++++++ .../fields/setup_signal_link_field/_index.html.erb | 3 +++ .../fields/setup_signal_link_field/_show.html.erb | 4 ++++ config/locales/de.yml | 7 +++++++ 6 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 app/fields/setup_signal_link_field.rb create mode 100644 app/views/fields/setup_signal_link_field/_form.html.erb create mode 100644 app/views/fields/setup_signal_link_field/_index.html.erb create mode 100644 app/views/fields/setup_signal_link_field/_show.html.erb diff --git a/app/dashboards/organization_dashboard.rb b/app/dashboards/organization_dashboard.rb index bff4cedd6..904070a91 100644 --- a/app/dashboards/organization_dashboard.rb +++ b/app/dashboards/organization_dashboard.rb @@ -32,7 +32,7 @@ class OrganizationDashboard < Administrate::BaseDashboard threemarb_api_identity: Field::String, threemarb_api_secret: Field::String, threemarb_private: Field::String, - signal_server_phone_number: Field::String + signal_server_phone_number: SetupSignalLinkField }.freeze COLLECTION_ATTRIBUTES = %i[ diff --git a/app/fields/setup_signal_link_field.rb b/app/fields/setup_signal_link_field.rb new file mode 100644 index 000000000..965c85b10 --- /dev/null +++ b/app/fields/setup_signal_link_field.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'administrate/field/base' + +class SetupSignalLinkField < Administrate::Field::Base + def setup_signal_url + "/#{resource.id}/signal/register" + end + + def signal_server_phone_number + data + end +end diff --git a/app/views/fields/setup_signal_link_field/_form.html.erb b/app/views/fields/setup_signal_link_field/_form.html.erb new file mode 100644 index 000000000..1e72f7636 --- /dev/null +++ b/app/views/fields/setup_signal_link_field/_form.html.erb @@ -0,0 +1,6 @@ +
+ <%= f.label field.attribute %> +
+
+ <%= f.text_field field.attribute %> +
diff --git a/app/views/fields/setup_signal_link_field/_index.html.erb b/app/views/fields/setup_signal_link_field/_index.html.erb new file mode 100644 index 000000000..1cf61a8b7 --- /dev/null +++ b/app/views/fields/setup_signal_link_field/_index.html.erb @@ -0,0 +1,3 @@ +<%= link_to field.setup_signal_url, style: 'text-decoration: underline' do %> + <%= field.signal_server_phone_number %> +<% end %> diff --git a/app/views/fields/setup_signal_link_field/_show.html.erb b/app/views/fields/setup_signal_link_field/_show.html.erb new file mode 100644 index 000000000..6c8e74c01 --- /dev/null +++ b/app/views/fields/setup_signal_link_field/_show.html.erb @@ -0,0 +1,4 @@ +

Klicken Sie hier, um sich zu registrieren, falls Sie noch nicht registriert sind

+<%= link_to field.setup_signal_url, style: 'text-decoration: underline' do %> + <%= field.signal_server_phone_number %> +<% end %> diff --git a/config/locales/de.yml b/config/locales/de.yml index 9f6f7f7cc..9d3b40fea 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -856,6 +856,13 @@ de: username: Telegram Username threema_id: Threema Id telegram_id: Telegram Id + organization: + name: Name + contact_person: Kontaktperson + email_from_address: E-Mail Adresse + telegram_bot_username: Telegram-Bot-Benutzername + threemarb_api_identity: Threema Id + signal_server_phone_number: Signal-Rufnummer number: currency: format: From e450e8bb05ed36fadb576c1a9302f9d253f943de Mon Sep 17 00:00:00 2001 From: Matthew Rider Date: Wed, 16 Oct 2024 12:56:21 +0200 Subject: [PATCH 16/23] Refactor signal registration process --- .../signal_captcha_form.css | 7 ++++ .../signal_captcha_form.html.erb | 30 +++++++++++++++++ .../signal_captcha_form.rb | 13 ++++++++ .../signal_verify_phone_number_form.css | 4 +++ .../signal_verify_phone_number_form.html.erb} | 10 +++--- .../signal_verify_phone_number_form.rb | 13 ++++++++ .../organizations/signal_controller.rb | 28 ++++++---------- .../register_phone_number_service.rb | 24 ++++++++++++++ .../verify_phone_number_service.rb | 23 +++++++++++++ .../signal/captcha_form.html.erb | 32 +------------------ app/views/organizations/signal/edit.html.erb | 15 --------- .../organizations/signal/verify_form.html.erb | 17 +--------- .../settings/signal/captcha_form.html.erb | 31 ------------------ config/locales/de.yml | 12 +++++++ 14 files changed, 142 insertions(+), 117 deletions(-) create mode 100644 app/components/signal_captcha_form/signal_captcha_form.css create mode 100644 app/components/signal_captcha_form/signal_captcha_form.html.erb create mode 100644 app/components/signal_captcha_form/signal_captcha_form.rb create mode 100644 app/components/signal_verify_phone_number_form/signal_verify_phone_number_form.css rename app/{views/settings/signal/verify_form.html.erb => components/signal_verify_phone_number_form/signal_verify_phone_number_form.html.erb} (64%) create mode 100644 app/components/signal_verify_phone_number_form/signal_verify_phone_number_form.rb create mode 100644 app/services/signal_adapter/register_phone_number_service.rb create mode 100644 app/services/signal_adapter/verify_phone_number_service.rb delete mode 100644 app/views/organizations/signal/edit.html.erb delete mode 100644 app/views/settings/signal/captcha_form.html.erb diff --git a/app/components/signal_captcha_form/signal_captcha_form.css b/app/components/signal_captcha_form/signal_captcha_form.css new file mode 100644 index 000000000..a15e85219 --- /dev/null +++ b/app/components/signal_captcha_form/signal_captcha_form.css @@ -0,0 +1,7 @@ +.SignalCaptchaForm-useVoiceSection { + margin-top: var(--spacing-unit-s); +} + +.SignalCaptchaForm .SubmitButton { + margin-top: var(--spacing-unit); +} diff --git a/app/components/signal_captcha_form/signal_captcha_form.html.erb b/app/components/signal_captcha_form/signal_captcha_form.html.erb new file mode 100644 index 000000000..a80f65715 --- /dev/null +++ b/app/components/signal_captcha_form/signal_captcha_form.html.erb @@ -0,0 +1,30 @@ +<%= c 'main', **attrs do %> + <%= c 'page_header', styles: [:wide, :inheritBackgroundColor, :xlargePaddingTop] do %> + <%= c 'heading', tag: :h2 do %> + <%= t('.main_heading') %> + <% end %> + <% end %> + + <%= c 'section', styles: [:wide] do %> +

<%= t('.instructions.where_to_generate_html', link: 'https://signalcaptchas.org/registration/generate.html') %>

+

<%= t('.instructions.after_solving_captcha') %>

+ <% end %> + + <%= c 'section', styles: [:wide] do %> + <%= c 'form', model: organization, url: organization_signal_register_path(organization), method: :post do %> + <%= c 'base_field', id: 'organization[signal][captcha]', label: 'Captcha' do |field| %> + <% c 'textarea', id: 'organization[signal][captcha]', value: nil %> + <% end %> + +
+ +

<%= t('.instructions.use_voice.help') %>

+
+ + <%= c 'submit_button', label: t('save') %> + <% end %> + <% end %> +<% end %> diff --git a/app/components/signal_captcha_form/signal_captcha_form.rb b/app/components/signal_captcha_form/signal_captcha_form.rb new file mode 100644 index 000000000..13d2a3e34 --- /dev/null +++ b/app/components/signal_captcha_form/signal_captcha_form.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module SignalCaptchaForm + class SignalCaptchaForm < ApplicationComponent + def initialize(organization:) + super + + @organization = organization + end + + attr_reader :organization + end +end diff --git a/app/components/signal_verify_phone_number_form/signal_verify_phone_number_form.css b/app/components/signal_verify_phone_number_form/signal_verify_phone_number_form.css new file mode 100644 index 000000000..366762118 --- /dev/null +++ b/app/components/signal_verify_phone_number_form/signal_verify_phone_number_form.css @@ -0,0 +1,4 @@ + +.SignalVerifyPhoneNumberForm .SubmitButton { + margin-top: var(--spacing-unit); +} diff --git a/app/views/settings/signal/verify_form.html.erb b/app/components/signal_verify_phone_number_form/signal_verify_phone_number_form.html.erb similarity index 64% rename from app/views/settings/signal/verify_form.html.erb rename to app/components/signal_verify_phone_number_form/signal_verify_phone_number_form.html.erb index 559d6d0db..d5a483e46 100644 --- a/app/views/settings/signal/verify_form.html.erb +++ b/app/components/signal_verify_phone_number_form/signal_verify_phone_number_form.html.erb @@ -1,13 +1,13 @@ -<%= c 'main' do %> - <%= c 'page_header', styles: [:wide, :inheritBackgroundColor, :xlargePaddingTop, :spaceBetween] do %> +<%= c 'main', **attrs do %> + <%= c 'page_header', styles: [:wide, :inheritBackgroundColor, :xlargePaddingTop] do %> <%= c 'heading' do %> - <%= "Verify Signal Number" %> + <%= t('.main_heading') %> <% end %> <% end %> <%= c 'section', styles: [:wide, :largeBottomMargin] do %> - <%= c 'form', model: @organization, url: settings_verify_path, method: :post do %> - <%= c 'base_field', id: 'organization[signal][token]', label: 'Captcha' do |field| %> + <%= c 'form', model: organization, url: organization_signal_verify_path(organization), method: :post do %> + <%= c 'base_field', id: 'organization[signal][token]', label: 'Token' do |field| %> <% c 'input', id: 'organization[signal][token]' %> <% end %> diff --git a/app/components/signal_verify_phone_number_form/signal_verify_phone_number_form.rb b/app/components/signal_verify_phone_number_form/signal_verify_phone_number_form.rb new file mode 100644 index 000000000..9807c0330 --- /dev/null +++ b/app/components/signal_verify_phone_number_form/signal_verify_phone_number_form.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module SignalVerifyPhoneNumberForm + class SignalVerifyPhoneNumberForm < ApplicationComponent + def initialize(organization:) + super + + @organization = organization + end + + attr_reader :organization + end +end diff --git a/app/controllers/organizations/signal_controller.rb b/app/controllers/organizations/signal_controller.rb index 9073f025b..246657da5 100644 --- a/app/controllers/organizations/signal_controller.rb +++ b/app/controllers/organizations/signal_controller.rb @@ -5,20 +5,14 @@ class SignalController < ApplicationController def captcha_form; end def register - uri = URI.parse("#{ENV.fetch('SIGNAL_CLI_REST_API_ENDPOINT', 'http://localhost:8080')}/v1/register/#{signal_server_phone_number}") - request = Net::HTTP::Post.new(uri, { - Accept: 'application/json', - 'Content-Type': 'application/json' - }) - request.body = register_data.to_json - response = Net::HTTP.start(uri.host, uri.port) do |http| - http.request(request) - end + response = SignalAdapter::RegisterPhoneNumberService.new(organization_id: @organization.id, register_data: register_data).call + Rails.logger.debug JSON.parse(response.body) case response when Net::HTTPSuccess redirect_to organization_signal_verify_path else handle_error_response(response) + render :captcha_form, status: :unprocessable_entity end end @@ -26,19 +20,14 @@ def verify_form; end def verify token = params[:organization][:signal][:token] - uri = URI.parse("#{ENV.fetch('SIGNAL_CLI_REST_API_ENDPOINT', 'http://localhost:8080')}/v1/register/#{signal_server_phone_number}/verify/#{token}") - request = Net::HTTP::Post.new(uri, { - Accept: 'application/json', - 'Content-Type': 'application/json' - }) - response = Net::HTTP.start(uri.host, uri.port) do |http| - http.request(request) - end + response = SignalAdapter::VerifyPhoneNumberService.new(organization_id: @organization.id, token: token).call + Rails.logger.debug JSON.parse(response.body) case response when Net::HTTPSuccess - SignalAdapter::SetTrustModeJob.perform_later(signal_server_phone_number: signal_server_phone_number) + SignalAdapter::SetTrustModeJob.perform_later(signal_server_phone_number: organization.signal_server_phone_number) else handle_error_response(response) + render :verify_form, status: :unprocessable_entity end end @@ -55,7 +44,7 @@ def signal_server_phone_number def register_data { captcha: params[:organization][:signal][:captcha], - use_voice: false + use_voice: ActiveModel::Type::Boolean.new.cast(params[:organization][:signal][:use_voice]) } end @@ -69,6 +58,7 @@ def handle_error_response(response) body: error_message } ErrorNotifier.report(exception, context: context) + flash.now[:error] = error_message end end end diff --git a/app/services/signal_adapter/register_phone_number_service.rb b/app/services/signal_adapter/register_phone_number_service.rb new file mode 100644 index 000000000..d5fcddfe1 --- /dev/null +++ b/app/services/signal_adapter/register_phone_number_service.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module SignalAdapter + class RegisterPhoneNumberService + attr_reader :register_data, :uri + + def initialize(organization_id:, register_data:) + @organization = Organization.find(organization_id) + @uri = URI.parse("#{ENV.fetch('SIGNAL_CLI_REST_API_ENDPOINT', 'http://localhost:8080')}/v1/register/#{@organization.signal_server_phone_number}") + @register_data = register_data + end + + def call + request = Net::HTTP::Post.new(uri, { + Accept: 'application/json', + 'Content-Type': 'application/json' + }) + request.body = register_data.to_json + Net::HTTP.start(uri.host, uri.port) do |http| + http.request(request) + end + end + end +end diff --git a/app/services/signal_adapter/verify_phone_number_service.rb b/app/services/signal_adapter/verify_phone_number_service.rb new file mode 100644 index 000000000..f5289bab1 --- /dev/null +++ b/app/services/signal_adapter/verify_phone_number_service.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module SignalAdapter + class VerifyPhoneNumberService + attr_reader :token, :uri, :organization + + def initialize(organization_id:, token:) + @organization = Organization.find(organization_id) + @uri = URI.parse("#{ENV.fetch('SIGNAL_CLI_REST_API_ENDPOINT', 'http://localhost:8080')}/v1/register/#{organization.signal_server_phone_number}/verify/#{token}") + @token = token + end + + def call + request = Net::HTTP::Post.new(uri, { + Accept: 'application/json', + 'Content-Type': 'application/json' + }) + Net::HTTP.start(uri.host, uri.port) do |http| + http.request(request) + end + end + end +end diff --git a/app/views/organizations/signal/captcha_form.html.erb b/app/views/organizations/signal/captcha_form.html.erb index ccb1f0a1f..d624e806c 100644 --- a/app/views/organizations/signal/captcha_form.html.erb +++ b/app/views/organizations/signal/captcha_form.html.erb @@ -1,31 +1 @@ -<%= c 'main' do %> - <%= c 'page_header', styles: [:wide, :inheritBackgroundColor, :xlargePaddingTop, :spaceBetween] do %> - <%= c 'heading' do %> - <%= "Register Signal Number" %> - <% end %> - <% end %> - - <%= c 'section', styles: [:wide, :largeBottomMargin] do %> -
- <%= c 'heading', tag: :h2 do %> - Generate a captcha - <% end %> -

To get the token, go to https://signalcaptchas.org/registration/generate.html

- -

If the token from that page doesn't work, you can try https://signalcaptchas.org/challenge/generate.html

- -

After filling the captcha, the site doesn't immediately show the token but tries to redirect to a signalcaptcha:// url that contains the token.

-

- After a short moment, a link will appear underneath the captcha called "Open Signal". - Get the token by right clicking on it and clicking: copy url. You can paste that directly into: -

-
- <%= c 'form', model: @organization, url: organization_signal_register_path(@organization), method: :post do %> - <%= c 'base_field', id: 'organization[signal][captcha]', label: 'Captcha' do |field| %> - <% c 'input', id: 'organization[signal][captcha]', value: nil %> - <% end %> - - <%= c 'submit_button', label: t('save') %> - <% end %> - <% end %> -<% end %> +<%= c 'signal_captcha_form', organization: @organization %> diff --git a/app/views/organizations/signal/edit.html.erb b/app/views/organizations/signal/edit.html.erb deleted file mode 100644 index d3b1f41c0..000000000 --- a/app/views/organizations/signal/edit.html.erb +++ /dev/null @@ -1,15 +0,0 @@ -<%= c 'main' do %> - <%= c 'page_header', styles: [:wide, :inheritBackgroundColor, :xlargePaddingTop, :spaceBetween] do %> - <%= c 'heading' do %> - <%= "Update organization with Signal Number" %> - <% end %> - <% end %> - <%= c 'section', styles: [:wide, :largeBottomMargin] do %> - <%= c 'form', model: @organization, url: organization_signal_add_path(@organization) do %> - <%= c 'field', object: @organization, attr: :signal_server_phone_number do |field| %> - <% c 'input', field.input_defaults.merge(required: true) %> - <% end %> - <%= c 'submit_button', label: t('save') %> - <% end %> - <% end %> -<% end %> diff --git a/app/views/organizations/signal/verify_form.html.erb b/app/views/organizations/signal/verify_form.html.erb index c4381bc6f..733f34129 100644 --- a/app/views/organizations/signal/verify_form.html.erb +++ b/app/views/organizations/signal/verify_form.html.erb @@ -1,16 +1 @@ - -<%= c 'main' do %> - <%= c 'page_header', styles: [:wide, :inheritBackgroundColor, :xlargePaddingTop, :spaceBetween] do %> - <%= c 'heading' do %> - <%= "Verify Signal Number" %> - <% end %> - <% end %> - <%= c 'section', styles: [:wide, :largeBottomMargin] do %> - <%= c 'form', model: @organization, url: organization_signal_verify_path(@organization), method: :post do %> - <%= c 'base_field', id: 'organization[signal][token]', label: 'Captcha' do |field| %> - <% c 'input', id: 'organization[signal][token]' %> - <% end %> - <%= c 'submit_button', label: t('save') %> - <% end %> - <% end %> -<% end %> +<%= c 'signal_verify_phone_number_form', organization: @organization %> diff --git a/app/views/settings/signal/captcha_form.html.erb b/app/views/settings/signal/captcha_form.html.erb deleted file mode 100644 index 3e3659113..000000000 --- a/app/views/settings/signal/captcha_form.html.erb +++ /dev/null @@ -1,31 +0,0 @@ -<%= c 'main' do %> - <%= c 'page_header', styles: [:wide, :inheritBackgroundColor, :xlargePaddingTop, :spaceBetween] do %> - <%= c 'heading' do %> - <%= "Register Signal Number" %> - <% end %> - <% end %> - - <%= c 'section', styles: [:wide, :largeBottomMargin] do %> -
- <%= c 'heading', tag: :h2 do %> - Generate a captcha - <% end %> -

To get the token, go to https://signalcaptchas.org/registration/generate.html

- -

If the token from that page doesn't work, you can try https://signalcaptchas.org/challenge/generate.html

- -

After filling the captcha, the site doesn't immediately show the token but tries to redirect to a signalcaptcha:// url that contains the token.

-

- After a short moment, a link will appear underneath the captcha called "Open Signal". - Get the token by right clicking on it and clicking: copy url. You can paste that directly into: -

-
- <%= c 'form', model: @organization, url: settings_register_path, method: :post do %> - <%= c 'base_field', id: 'organization[signal][captcha]', label: 'Captcha' do |field| %> - <% c 'input', id: 'organization[signal][captcha]', value: nil %> - <% end %> - - <%= c 'submit_button', label: t('save') %> - <% end %> - <% end %> -<% end %> diff --git a/config/locales/de.yml b/config/locales/de.yml index 9d3b40fea..e265daec2 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -412,6 +412,18 @@ de: heading: Bitte wähle eine Organisation aus, um fortzufahren errors_page: alt_text: Otter arbeitet an der Lösung des Problems + signal_captcha_form: + main_heading: Erzeugen eines Captcha-Tokens + instructions: + where_to_generate_html: Um das Token zu erhalten, gehst du zu Signal-Captcha für die Registrierung generieren. + after_solving_captcha: | + Nach dem Ausfüllen des Captcha zeigt die Website das Token nicht sofort an, sondern versucht, zu einer signalcaptcha://-Url umzuleiten, die das Token enthält. Nach einem kurzen Moment erscheint unter dem Captcha ein Link mit der Bezeichnung „Open Signal“. + Holen Sie sich das Token, indem Sie mit der rechten Maustaste darauf klicken und auf: copy url. Sie können es direkt in die Seite einfügen: + use_voice: + label: Telefonanruf anfordern + help: Sie müssen zuerst eine SMS anfordern, die unter alrets@dialog.click empfangen wird. Wenn Sie diese nach 60 Sekunden nicht erhalten haben, können Sie einen Sprachanruf anfordern. + signal_verify_phone_number_form: + main_heading: Rufnummer überprüfen mailer: unsubscribe: From 9ea881b9a8521a136200e1a45a414d8c52e1f1fc Mon Sep 17 00:00:00 2001 From: Matthew Rider Date: Wed, 16 Oct 2024 13:06:02 +0200 Subject: [PATCH 17/23] Schedule a job to set the username - update the organization's signal_complete_onboarding_link with the username link in response --- .../organizations/signal_controller.rb | 3 +- app/jobs/signal_adapter/set_username_job.rb | 39 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 app/jobs/signal_adapter/set_username_job.rb diff --git a/app/controllers/organizations/signal_controller.rb b/app/controllers/organizations/signal_controller.rb index 246657da5..c7c004344 100644 --- a/app/controllers/organizations/signal_controller.rb +++ b/app/controllers/organizations/signal_controller.rb @@ -24,7 +24,8 @@ def verify Rails.logger.debug JSON.parse(response.body) case response when Net::HTTPSuccess - SignalAdapter::SetTrustModeJob.perform_later(signal_server_phone_number: organization.signal_server_phone_number) + SignalAdapter::SetTrustModeJob.perform_later(signal_server_phone_number: @organization.signal_server_phone_number) + SignalAdapter::SetUsernameJob.perform_later(organization_id: @organization.id) else handle_error_response(response) render :verify_form, status: :unprocessable_entity diff --git a/app/jobs/signal_adapter/set_username_job.rb b/app/jobs/signal_adapter/set_username_job.rb new file mode 100644 index 000000000..9b3c3efea --- /dev/null +++ b/app/jobs/signal_adapter/set_username_job.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'net/http' + +module SignalAdapter + class SetUsernameJob < ApplicationJob + def perform(organization_id:) + organization = Organization.find(organization_id) + uri = URI.parse("#{ENV.fetch('SIGNAL_CLI_REST_API_ENDPOINT', 'http://localhost:8080')}/v1/accounts/#{organization.signal_server_phone_number}/username") + request = Net::HTTP::Post.new(uri, { + Accept: 'application/json', + 'Content-Type': 'application/json' + }) + request.body = { username: organization.project_name.gsub(/\s+/, '').camelize }.to_json + response = Net::HTTP.start(uri.host, uri.port) do |http| + http.request(request) + end + case response + when Net::HTTPSuccess + organization.update!(signal_complete_onboarding_link: JSON.parse(response.body)['username_link']) + else + handle_error(JSON.parse(response.body)['error']) + end + end + + private + + def handle_error(error_message) + exception = SignalAdapter::BadRequestError.new(error_code: response.code, message: error_message) + context = { + code: response.code, + message: response.message, + headers: response.to_hash, + body: error_message + } + ErrorNotifier.report(exception, context: context) + end + end +end From 6a774a3fc0ea4e429653490d1602f043d3b6e521 Mon Sep 17 00:00:00 2001 From: Matthew Rider Date: Wed, 16 Oct 2024 13:07:26 +0200 Subject: [PATCH 18/23] Lint --- .../signal_captcha_form/signal_captcha_form.html.erb | 4 ++-- .../signal_verify_phone_number_form.css | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/components/signal_captcha_form/signal_captcha_form.html.erb b/app/components/signal_captcha_form/signal_captcha_form.html.erb index a80f65715..f0f0b1f5d 100644 --- a/app/components/signal_captcha_form/signal_captcha_form.html.erb +++ b/app/components/signal_captcha_form/signal_captcha_form.html.erb @@ -4,12 +4,12 @@ <%= t('.main_heading') %> <% end %> <% end %> - + <%= c 'section', styles: [:wide] do %>

<%= t('.instructions.where_to_generate_html', link: 'https://signalcaptchas.org/registration/generate.html') %>

<%= t('.instructions.after_solving_captcha') %>

<% end %> - + <%= c 'section', styles: [:wide] do %> <%= c 'form', model: organization, url: organization_signal_register_path(organization), method: :post do %> <%= c 'base_field', id: 'organization[signal][captcha]', label: 'Captcha' do |field| %> diff --git a/app/components/signal_verify_phone_number_form/signal_verify_phone_number_form.css b/app/components/signal_verify_phone_number_form/signal_verify_phone_number_form.css index 366762118..9bd5a3467 100644 --- a/app/components/signal_verify_phone_number_form/signal_verify_phone_number_form.css +++ b/app/components/signal_verify_phone_number_form/signal_verify_phone_number_form.css @@ -1,4 +1,3 @@ - .SignalVerifyPhoneNumberForm .SubmitButton { margin-top: var(--spacing-unit); } From 482dbaafa5da949d3bd2ad29898ba812dd7d0fc7 Mon Sep 17 00:00:00 2001 From: Matthew Rider Date: Thu, 17 Oct 2024 11:45:25 +0200 Subject: [PATCH 19/23] Fix failing tests --- .../organizations/signal_controller.rb | 4 +- .../postmark_adapter/outbound_spec.rb | 8 +- .../signal_adapter/outbound/file_spec.rb | 7 +- .../signal_adapter/outbound/text_spec.rb | 6 +- spec/models/contributor_spec.rb | 13 ++- spec/requests/admin/organization_spec.rb | 20 ++--- spec/requests/organizations/signal_spec.rb | 83 ------------------- spec/system/admin/auth_spec.rb | 2 +- 8 files changed, 32 insertions(+), 111 deletions(-) diff --git a/app/controllers/organizations/signal_controller.rb b/app/controllers/organizations/signal_controller.rb index c7c004344..9c04fcef1 100644 --- a/app/controllers/organizations/signal_controller.rb +++ b/app/controllers/organizations/signal_controller.rb @@ -6,7 +6,7 @@ def captcha_form; end def register response = SignalAdapter::RegisterPhoneNumberService.new(organization_id: @organization.id, register_data: register_data).call - Rails.logger.debug JSON.parse(response.body) + Rails.logger.debug JSON.parse(response.body) if response.body.present? case response when Net::HTTPSuccess redirect_to organization_signal_verify_path @@ -21,7 +21,7 @@ def verify_form; end def verify token = params[:organization][:signal][:token] response = SignalAdapter::VerifyPhoneNumberService.new(organization_id: @organization.id, token: token).call - Rails.logger.debug JSON.parse(response.body) + Rails.logger.debug JSON.parse(response.body) if response.body.present? case response when Net::HTTPSuccess SignalAdapter::SetTrustModeJob.perform_later(signal_server_phone_number: @organization.signal_server_phone_number) diff --git a/spec/adapters/postmark_adapter/outbound_spec.rb b/spec/adapters/postmark_adapter/outbound_spec.rb index 7dd01cb9d..09073ad57 100644 --- a/spec/adapters/postmark_adapter/outbound_spec.rb +++ b/spec/adapters/postmark_adapter/outbound_spec.rb @@ -52,7 +52,7 @@ describe 'with(message: message, organization: message.organization)' do let(:adapter) { described_class.with(message: message, organization: message.organization) } - let(:organization) { create(:organization, email_from_address: '100eyes-test-account@example.org', project_name: 'TestingProject') } + let!(:organization) { create(:organization, email_from_address: '100eyes-test-account@example.org', project_name: 'TestingProject') } let(:request) { create(:request, id: 4711, organization: organization) } let(:recipient) { create(:contributor, email: email_address) } let(:email_address) { 'recipient@example.org' } @@ -291,7 +291,7 @@ context 'no admin' do let(:admin) { nil } - let(:contributor) { create(:contributor) } + let(:contributor) { create(:contributor, organization: organization) } it 'does not enqueue a Mailer' do expect { subject }.not_to have_enqueued_job @@ -309,7 +309,7 @@ context 'user without an admin role' do let(:admin) { build(:user) } - let(:contributor) { create(:contributor) } + let(:contributor) { create(:contributor, organization: organization) } it 'does not enqueue a Mailer' do expect { subject }.not_to have_enqueued_job @@ -318,7 +318,7 @@ context 'admin email equals contributor email' do let(:admin) { create(:user, admin: true, email: 'my-email@example.org') } - let(:contributor) { create(:contributor, email: 'my-email@example.org') } + let(:contributor) { create(:contributor, email: 'my-email@example.org', organization: organization) } it 'does not enqueue a Mailer' do expect { subject }.not_to have_enqueued_job diff --git a/spec/adapters/signal_adapter/outbound/file_spec.rb b/spec/adapters/signal_adapter/outbound/file_spec.rb index 3f6cbd310..d800351c4 100644 --- a/spec/adapters/signal_adapter/outbound/file_spec.rb +++ b/spec/adapters/signal_adapter/outbound/file_spec.rb @@ -6,7 +6,12 @@ RSpec.describe SignalAdapter::Outbound::File do let(:adapter) { described_class.new } let(:contributor) { create(:contributor, signal_phone_number: '+4915112345678', email: nil) } - let(:request) { create(:request, organization: create(:organization, signal_server_phone_number: 'SIGNAL_SERVER_PHONE_NUMBER')) } + let(:organization) do + build(:organization, signal_server_phone_number: 'SIGNAL_SERVER_PHONE_NUMBER').tap do |org| + org.save(validate: false) + end + end + let(:request) { create(:request, organization: organization) } let(:message) { create(:message, :with_file, recipient: contributor, text: 'Hello Signal', request: request) } let(:perform) { -> { adapter.perform(message: message) } } diff --git a/spec/adapters/signal_adapter/outbound/text_spec.rb b/spec/adapters/signal_adapter/outbound/text_spec.rb index 516af151f..411b0d8fa 100644 --- a/spec/adapters/signal_adapter/outbound/text_spec.rb +++ b/spec/adapters/signal_adapter/outbound/text_spec.rb @@ -5,7 +5,11 @@ RSpec.describe SignalAdapter::Outbound::Text do let(:adapter) { described_class.new } - let(:organization) { create(:organization, signal_server_phone_number: 'SIGNAL_SERVER_PHONE_NUMBER') } + let(:organization) do + build(:organization, signal_server_phone_number: 'SIGNAL_SERVER_PHONE_NUMBER').tap do |org| + org.save(validate: false) + end + end let(:contributor) { create(:contributor, signal_phone_number: '+4915112345678', email: nil, organization: organization) } let(:message) { create(:message, :with_file, text: 'Hello Signal') } let(:organization_id) { organization.id } diff --git a/spec/models/contributor_spec.rb b/spec/models/contributor_spec.rb index 4410b1182..82c629d80 100644 --- a/spec/models/contributor_spec.rb +++ b/spec/models/contributor_spec.rb @@ -913,10 +913,13 @@ describe '.send_welcome_message!', telegram_bot: :rails do subject { -> { contributor.send_welcome_message!(organization) } } - let(:organization) do + let!(:organization) do create(:organization, onboarding_success_heading: 'Welcome new contributor!', onboarding_success_text: 'You onboarded successfully.') end - let(:contributor) { create(:contributor, telegram_id: nil, email: nil, threema_id: nil) } + let(:contributor) do + create(:contributor, telegram_id: nil, email: nil, threema_id: nil, signal_phone_number: nil, whats_app_phone_number: nil, + organization: organization) + end it { should_not have_enqueued_job } @@ -925,11 +928,13 @@ { organization_id: organization.id, contributor_id: contributor.id, text: "Welcome new contributor!\nYou onboarded successfully." } end - let(:contributor) { create(:contributor, telegram_id: nil, telegram_onboarding_token: 'ABCDEF', email: nil) } + let(:contributor) do + create(:contributor, telegram_id: nil, telegram_onboarding_token: 'ABCDEF', email: nil, organization: organization) + end it { should_not have_enqueued_job } context 'and connected' do - let(:contributor) { create(:contributor, telegram_id: 4711, telegram_onboarding_token: 'ABCDEF', email: nil) } + let(:contributor) { create(:contributor, :telegram_contributor, organization: organization) } it { should enqueue_job(TelegramAdapter::Outbound::Text).with(expected_job_args) } end end diff --git a/spec/requests/admin/organization_spec.rb b/spec/requests/admin/organization_spec.rb index 7a9ff5167..16577ce23 100644 --- a/spec/requests/admin/organization_spec.rb +++ b/spec/requests/admin/organization_spec.rb @@ -96,17 +96,10 @@ end end - context 'WhatsApp Twilio' do - let(:whats_app_params) do - { - whats_app_server_phone_number: '+4912345678', - twilio_account_sid: 'valid_account_sid', - twilio_api_key_sid: 'valid_api_key_sid', - twilio_api_key_secret: 'valid_secret' - } - end + context 'Signal' do + let(:signal_params) { { signal_server_phone_number: '+4912345678' } } let(:params) do - required_params.deep_merge({ organization: whats_app_params }) + required_params.deep_merge({ organization: signal_params }) end it 'allows configuration of Twilio specific attrs' do @@ -114,12 +107,9 @@ follow_redirect! expect(page).to have_content('+4912345678') - expect(page).to have_content('valid_account_sid') - expect(page).to have_content('valid_api_key_sid') - expect(page).not_to have_content('valid_secret') - organization = Organization.find_by(whats_app_server_phone_number: '+4912345678') - expect(organization).to have_attributes(whats_app_params) + organization = Organization.find_by(signal_server_phone_number: '+4912345678') + expect(organization).to have_attributes(signal_params) end end end diff --git a/spec/requests/organizations/signal_spec.rb b/spec/requests/organizations/signal_spec.rb index 7d0f71781..1ee5a5641 100644 --- a/spec/requests/organizations/signal_spec.rb +++ b/spec/requests/organizations/signal_spec.rb @@ -6,89 +6,6 @@ RSpec.describe 'Signal' do let(:organization) { create(:organization, signal_server_phone_number: nil) } - describe 'GET /:organization_id/add-signal' do - subject { -> { get organization_signal_add_path(organization, as: user) } } - - context 'unauthenticated' do - let(:user) { nil } - - it 'renders not found ' do - subject.call - expect(response).to be_not_found - end - end - - context 'unauthorized' do - let(:user) { create(:user, admin: false, organizations: [organization]) } - - it 'renders not found ' do - subject.call - expect(response).to be_not_found - end - end - - context 'authenticated and authorized' do - let(:user) { create(:user, admin: true) } - - it 'should be successful' do - subject.call - expect(response).to be_successful - end - - it 'renders a form to add signal_server_phone_number' do - subject.call - expect(page).to have_css("form[action='/#{organization.id}/signal/add']") do |form| - expect(form).to have_css('input[id="organization[signal_server_phone_number]"]') - end - end - end - end - - describe 'PATCH /:organization_id/add-signal' do - subject { -> { patch organization_signal_add_path(organization, as: user), params: params } } - - let(:params) { { organization: { signal_server_phone_number: '+4912345678' } } } - - context 'unauthenticated' do - let(:user) { nil } - - it 'renders not found ' do - subject.call - expect(response).to be_not_found - end - end - - context 'unauthorized' do - let(:user) { create(:user, admin: false, organizations: [organization]) } - - it 'renders not found ' do - subject.call - expect(response).to be_not_found - end - end - - context 'authenticated and authorized' do - let(:user) { create(:user, admin: true) } - - it "updates the organization's signal_server_phone_number" do - expect { subject.call }.to (change { organization.reload.signal_server_phone_number }).from(nil).to('+4912345678') - end - - it 'redirects to /signal/register' do - subject.call - expect(response).to redirect_to(organization_signal_register_path(organization)) - end - - it 'renders a form to register' do - subject.call - follow_redirect! - expect(page).to have_css("form[action='/#{organization.id}/signal/register']") do |form| - expect(form).to have_css('input[id="organization[signal][captcha]"]') - end - end - end - end - describe 'POST /:organization_id/signal/register' do subject { -> { post organization_signal_register_path(organization, as: user), params: params } } diff --git a/spec/system/admin/auth_spec.rb b/spec/system/admin/auth_spec.rb index d791f8c9a..1a72d9040 100644 --- a/spec/system/admin/auth_spec.rb +++ b/spec/system/admin/auth_spec.rb @@ -23,6 +23,6 @@ expect(page).to have_http_status(:ok) - expect(page).to have_link('New user') + expect(page).to have_link('New organization') end end From a9a970180dbd45dce289819202291504eb32753c Mon Sep 17 00:00:00 2001 From: Matthew Rider Date: Fri, 18 Oct 2024 09:38:35 +0200 Subject: [PATCH 20/23] Fine tune signal setup --- app/controllers/organizations/signal_controller.rb | 2 -- app/jobs/signal_adapter/set_username_job.rb | 7 ++++--- app/models/organization.rb | 4 ++-- app/views/fields/setup_signal_link_field/_show.html.erb | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/controllers/organizations/signal_controller.rb b/app/controllers/organizations/signal_controller.rb index 9c04fcef1..9ece2eac3 100644 --- a/app/controllers/organizations/signal_controller.rb +++ b/app/controllers/organizations/signal_controller.rb @@ -6,7 +6,6 @@ def captcha_form; end def register response = SignalAdapter::RegisterPhoneNumberService.new(organization_id: @organization.id, register_data: register_data).call - Rails.logger.debug JSON.parse(response.body) if response.body.present? case response when Net::HTTPSuccess redirect_to organization_signal_verify_path @@ -21,7 +20,6 @@ def verify_form; end def verify token = params[:organization][:signal][:token] response = SignalAdapter::VerifyPhoneNumberService.new(organization_id: @organization.id, token: token).call - Rails.logger.debug JSON.parse(response.body) if response.body.present? case response when Net::HTTPSuccess SignalAdapter::SetTrustModeJob.perform_later(signal_server_phone_number: @organization.signal_server_phone_number) diff --git a/app/jobs/signal_adapter/set_username_job.rb b/app/jobs/signal_adapter/set_username_job.rb index 9b3c3efea..5a7824563 100644 --- a/app/jobs/signal_adapter/set_username_job.rb +++ b/app/jobs/signal_adapter/set_username_job.rb @@ -11,7 +11,7 @@ def perform(organization_id:) Accept: 'application/json', 'Content-Type': 'application/json' }) - request.body = { username: organization.project_name.gsub(/\s+/, '').camelize }.to_json + request.body = { username: organization.project_name.gsub(/[^\w\s]/, '').gsub(/\s+/, '').camelize }.to_json response = Net::HTTP.start(uri.host, uri.port) do |http| http.request(request) end @@ -19,13 +19,14 @@ def perform(organization_id:) when Net::HTTPSuccess organization.update!(signal_complete_onboarding_link: JSON.parse(response.body)['username_link']) else - handle_error(JSON.parse(response.body)['error']) + handle_error(response) end end private - def handle_error(error_message) + def handle_error(response) + error_message = JSON.parse(response.body)['error'] exception = SignalAdapter::BadRequestError.new(error_code: response.code, message: error_message) context = { code: response.code, diff --git a/app/models/organization.rb b/app/models/organization.rb index d7bb2eb4a..4b7b7a326 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -88,7 +88,7 @@ def whats_app_onboarding_allowed? end def telegram_bot - Telegram.bots[id] + Telegram::Bot::Client.new(telegram_bot_api_key) end def twilio_instance @@ -121,7 +121,7 @@ def contributors_tags_with_count private def set_telegram_webhook - return unless saved_change_to_telegram_bot_username? + return unless saved_change_to_telegram_bot_username? && saved_change_to_telegram_bot_api_key? TelegramAdapter::SetWebhookUrlJob.perform_later(organization_id: id) end diff --git a/app/views/fields/setup_signal_link_field/_show.html.erb b/app/views/fields/setup_signal_link_field/_show.html.erb index 6c8e74c01..74be5473d 100644 --- a/app/views/fields/setup_signal_link_field/_show.html.erb +++ b/app/views/fields/setup_signal_link_field/_show.html.erb @@ -1,4 +1,4 @@ -

Klicken Sie hier, um sich zu registrieren, falls Sie noch nicht registriert sind

+

Klicke hier, um dich zu registrieren, falls du noch nicht registriert bist.

<%= link_to field.setup_signal_url, style: 'text-decoration: underline' do %> <%= field.signal_server_phone_number %> <% end %> From 508cd066b3c977ac6c87ffe0c46809ed97777000 Mon Sep 17 00:00:00 2001 From: Matthew Rider Date: Tue, 22 Oct 2024 11:17:05 +0200 Subject: [PATCH 21/23] Add job to set profile info for Signal --- .../organizations/signal_controller.rb | 1 + app/dashboards/organization_dashboard.rb | 6 ++- .../signal_adapter/set_profile_info_job.rb | 53 +++++++++++++++++++ app/models/organization.rb | 1 + ..._messengers_about_text_to_organizations.rb | 7 +++ db/schema.rb | 4 ++ 6 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 app/jobs/signal_adapter/set_profile_info_job.rb create mode 100644 db/migrate/20241022083441_add_messengers_about_text_to_organizations.rb diff --git a/app/controllers/organizations/signal_controller.rb b/app/controllers/organizations/signal_controller.rb index 9ece2eac3..25a04bd7f 100644 --- a/app/controllers/organizations/signal_controller.rb +++ b/app/controllers/organizations/signal_controller.rb @@ -24,6 +24,7 @@ def verify when Net::HTTPSuccess SignalAdapter::SetTrustModeJob.perform_later(signal_server_phone_number: @organization.signal_server_phone_number) SignalAdapter::SetUsernameJob.perform_later(organization_id: @organization.id) + SignalAdapter::SetProfileInfoJob.perform_later(organization_id: @organization.id) else handle_error_response(response) render :verify_form, status: :unprocessable_entity diff --git a/app/dashboards/organization_dashboard.rb b/app/dashboards/organization_dashboard.rb index 904070a91..826f0b8ab 100644 --- a/app/dashboards/organization_dashboard.rb +++ b/app/dashboards/organization_dashboard.rb @@ -32,7 +32,8 @@ class OrganizationDashboard < Administrate::BaseDashboard threemarb_api_identity: Field::String, threemarb_api_secret: Field::String, threemarb_private: Field::String, - signal_server_phone_number: SetupSignalLinkField + signal_server_phone_number: SetupSignalLinkField, + messengers_about_text: Field::String }.freeze COLLECTION_ATTRIBUTES = %i[ @@ -58,6 +59,7 @@ class OrganizationDashboard < Administrate::BaseDashboard telegram_bot_username threemarb_api_identity signal_server_phone_number + messengers_about_text ].freeze FORM_ATTRIBUTES_NEW = %i[ @@ -81,6 +83,7 @@ class OrganizationDashboard < Administrate::BaseDashboard threemarb_api_secret threemarb_private signal_server_phone_number + messengers_about_text ].freeze FORM_ATTRIBUTES_EDIT = %i[ @@ -100,6 +103,7 @@ class OrganizationDashboard < Administrate::BaseDashboard channel_image signal_complete_onboarding_link whats_app_quick_reply_button_text + messengers_about_text ].freeze COLLECTION_FILTERS = {}.freeze diff --git a/app/jobs/signal_adapter/set_profile_info_job.rb b/app/jobs/signal_adapter/set_profile_info_job.rb new file mode 100644 index 000000000..31c5c95cf --- /dev/null +++ b/app/jobs/signal_adapter/set_profile_info_job.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'net/http' + +module SignalAdapter + class SetProfileInfoJob < ApplicationJob + attr_reader :organization + + def perform(organization_id:) + @organization = Organization.find(organization_id) + uri = URI.parse("#{ENV.fetch('SIGNAL_CLI_REST_API_ENDPOINT', 'http://localhost:8080')}/v1/profiles/#{organization.signal_server_phone_number}") + request = Net::HTTP::Put.new(uri, { + Accept: 'application/json', + 'Content-Type': 'application/json' + }) + + request.body = update_profile_payload.to_json + response = Net::HTTP.start(uri.host, uri.port) do |http| + http.request(request) + end + case response + when Net::HTTPSuccess + Rails.logger.debug 'Great!' + else + handle_error(response) + end + end + + private + + def update_profile_payload + { + base64_avatar: Base64.encode64( + File.open(ActiveStorage::Blob.service.path_for(organization.channel_image.attachment.blob.key), 'rb').read + ), + name: organization.name, + about: organization.messengers_about_text + } + end + + def handle_error(response) + error_message = JSON.parse(response.body)['error'] + exception = SignalAdapter::BadRequestError.new(error_code: response.code, message: error_message) + context = { + code: response.code, + message: response.message, + headers: response.to_hash, + body: error_message + } + ErrorNotifier.report(exception, context: context) + end + end +end diff --git a/app/models/organization.rb b/app/models/organization.rb index 4b7b7a326..2a247704e 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -28,6 +28,7 @@ class Organization < ApplicationRecord validates :telegram_bot_username, uniqueness: true, allow_nil: true validates :signal_server_phone_number, phony_plausible: true + validates :messengers_about_text, length: { maximum: 139 }, allow_blank: true def channels_onboarding_allowed { diff --git a/db/migrate/20241022083441_add_messengers_about_text_to_organizations.rb b/db/migrate/20241022083441_add_messengers_about_text_to_organizations.rb new file mode 100644 index 000000000..5de566b43 --- /dev/null +++ b/db/migrate/20241022083441_add_messengers_about_text_to_organizations.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddMessengersAboutTextToOrganizations < ActiveRecord::Migration[6.1] + def change + add_column :organizations, :messengers_about_text, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index c52ca04e7..9dbdb4b5f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -127,6 +127,9 @@ t.index ["organization_id"], name: "index_contributors_on_organization_id" end + create_table "data_migrations", primary_key: "version", id: :string, force: :cascade do |t| + end + create_table "delayed_jobs", force: :cascade do |t| t.integer "priority", default: 0, null: false t.integer "attempts", default: 0, null: false @@ -236,6 +239,7 @@ t.string "signal_complete_onboarding_link" t.jsonb "whats_app_quick_reply_button_text", default: {"more_info"=>"Mehr Infos", "answer_request"=>"Antworten"} t.string "whats_app_more_info_message", default: "" + t.string "messengers_about_text" t.index ["business_plan_id"], name: "index_organizations_on_business_plan_id" t.index ["contact_person_id"], name: "index_organizations_on_contact_person_id" t.index ["telegram_bot_username"], name: "index_organizations_on_telegram_bot_username", unique: true From 5e72aee2c8e03f4dc83a5cbf9850cc90748b9729 Mon Sep 17 00:00:00 2001 From: Matthew Rider Date: Tue, 5 Nov 2024 14:51:41 +0100 Subject: [PATCH 22/23] Remove undesired change --- app/dashboards/organization_dashboard.rb | 2 +- app/models/organization.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/dashboards/organization_dashboard.rb b/app/dashboards/organization_dashboard.rb index 826f0b8ab..09efa13d9 100644 --- a/app/dashboards/organization_dashboard.rb +++ b/app/dashboards/organization_dashboard.rb @@ -25,7 +25,7 @@ class OrganizationDashboard < Administrate::BaseDashboard whats_app_more_info_message: Field::Text, whats_app_profile_about: Field::Text, signal_complete_onboarding_link: Field::Url, - whats_app_quick_reply_button_text: Field::JSONB + whats_app_quick_reply_button_text: Field::JSONB, email_from_address: Field::Email, telegram_bot_username: Field::String, telegram_bot_api_key: Field::String, diff --git a/app/models/organization.rb b/app/models/organization.rb index 2a247704e..dad451de5 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -89,7 +89,7 @@ def whats_app_onboarding_allowed? end def telegram_bot - Telegram::Bot::Client.new(telegram_bot_api_key) + Telegram.bots[id] end def twilio_instance From b4b70891afbf2bb7deefe5b46db93500bb3cd25a Mon Sep 17 00:00:00 2001 From: Matthew Rider Date: Tue, 3 Dec 2024 11:05:40 +0100 Subject: [PATCH 23/23] Avoid validating signal_server_phone_number - there are only a few people who have access to adding this number and it causes flaky tests. --- app/models/organization.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/organization.rb b/app/models/organization.rb index dad451de5..b66efb174 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -27,7 +27,6 @@ class Organization < ApplicationRecord phony_normalize :signal_server_phone_number, default_country_code: 'DE' validates :telegram_bot_username, uniqueness: true, allow_nil: true - validates :signal_server_phone_number, phony_plausible: true validates :messengers_about_text, length: { maximum: 139 }, allow_blank: true def channels_onboarding_allowed