hephaestus 0.1.2 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +27 -1
- data/bin/hephaestus +9 -3
- data/lib/hephaestus/app_builder.rb +27 -6
- data/lib/hephaestus/exit_on_failure.rb +4 -4
- data/lib/hephaestus/generators/app_generator.rb +148 -56
- data/lib/hephaestus/generators/base.rb +1 -1
- data/lib/hephaestus/generators/config_generator.rb +30 -2
- data/lib/hephaestus/generators/core_generator.rb +31 -2
- data/lib/hephaestus/generators/deployment_generator.rb +9 -3
- data/lib/hephaestus/generators/lib_generator.rb +2 -1
- data/lib/hephaestus/generators/sorbet_generator.rb +1 -1
- data/lib/hephaestus/version.rb +1 -1
- data/lib/hephaestus.rb +8 -0
- data/templates/.dockerignore +39 -0
- data/templates/.env.sample +6 -0
- data/templates/.github/dependabot.yml +12 -7
- data/templates/.github/workflows/automerge.yml +5 -85
- data/templates/.github/workflows/deploy.yml +30 -0
- data/templates/.github/workflows/licenses.yml +11 -31
- data/templates/.github/workflows/lint.yml +14 -34
- data/templates/.github/workflows/security.yml +9 -32
- data/templates/.github/workflows/sorbet.yml +7 -37
- data/templates/.github/workflows/test.yml +7 -41
- data/templates/.licensed.yml +7 -3
- data/templates/.ruby-version +1 -0
- data/templates/Dockerfile +79 -0
- data/templates/Gemfile.erb +23 -26
- data/templates/Procfile.debug +1 -1
- data/templates/app/controllers/app_controller.rb +71 -0
- data/templates/app/controllers/application_controller.rb +14 -1
- data/templates/app/controllers/concerns/authable.rb +20 -2
- data/templates/app/controllers/settings_controller.rb +32 -2
- data/templates/app/jobs/update_yetto_job.rb +6 -7
- data/templates/app/lib/body_parameter/yetto_parameters.rb +32 -0
- data/templates/app/lib/path_parameter/settings_parameters.rb +22 -0
- data/templates/app/lib/plug_app/middleware/openapi_validation.rb +5 -5
- data/templates/app/lib/plug_app/middleware/tracing_attributes.rb +1 -1
- data/templates/app/services/http_service.rb +27 -0
- data/templates/app/services/yetto_service.rb +24 -32
- data/templates/app/views/settings/new.json.jbuilder +21 -0
- data/templates/bin/docker-entrypoint +14 -0
- data/templates/compose.yml +7 -0
- data/templates/config/initializers/environment.rb +5 -8
- data/templates/config/initializers/filter_parameter_logging.rb +3 -0
- data/templates/config/initializers/opentelemetry.rb +32 -0
- data/templates/config/locales/en.yml +32 -0
- data/templates/config/locales/settings/en.yml +5 -0
- data/templates/lib/plug_app/schemas/api/2023-03-06/components/schemas/yetto.json +2 -2
- data/templates/lib/plug_app/schemas/api/2023-03-06/openapi.json +4 -4
- data/templates/lib/plug_app/schemas/api/2023-03-06/paths/plug.json +1 -1
- data/templates/lib/plug_app/schemas/api/2023-03-06/paths/yetto/after_create_message.json +2 -2
- data/templates/lib/plug_app/schemas/api/2023-03-06/paths/yetto/after_create_plug_installation.json +2 -2
- data/templates/lib/tasks/test_tasks.rake +6 -2
- data/templates/script/edit-credentials +29 -0
- data/templates/script/hmac_text +1 -1
- data/templates/script/licenses +4 -48
- data/templates/script/server +62 -2
- data/templates/script/sorbet +7 -0
- data/templates/test/controllers/application_controller_test.rb +32 -0
- data/templates/test/controllers/settings_controller_test.rb +1 -1
- data/templates/test/controllers/yetto_controller_test.rb +1 -1
- data/templates/test/fixtures/files/fake_pem_file/fake.pem +27 -0
- data/templates/test/jobs/update_yetto_job_test.rb +3 -18
- data/templates/test/support/api.rb +1 -1
- data/templates/test/support/rails.rb +1 -1
- data/templates/test/support/webmocks/slack_webmock.rb +2 -2
- data/templates/test/support/webmocks/yetto_webmock.rb +119 -0
- data/templates/test/test_helper.rb +16 -3
- data/templates/vendor/fly/fly-production.toml +38 -0
- data/templates/vendor/fly/fly-staging.toml +33 -0
- metadata +39 -16
- data/templates/.env.test +0 -4
- data/templates/.github/actions/license/action.yml +0 -11
- data/templates/.github/actions/setup/action.yml +0 -10
- data/templates/.github/actions/sisyphus/action.yml +0 -11
- data/templates/.github/actions/sorbet/action.yml +0 -19
- data/templates/app/views/settings/index.json.jbuilder +0 -15
- data/templates/config/initializers/open_telemetry.rb +0 -27
- data/templates/script/security_checks/brakeman +0 -5
- data/templates/script/security_checks/bundle-audit +0 -5
- data/templates/script/server-debug +0 -5
- data/templates/script/typecheck +0 -44
- data/templates/test/fixtures/files/.keep +0 -0
- data/templates/test/support/webmocks/yetto.rb +0 -94
data/templates/Gemfile.erb
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
"https://github.com/#{repo_name}.git"
|
|
6
|
-
end
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
|
7
5
|
|
|
8
|
-
ruby "
|
|
6
|
+
ruby File.read(".ruby-version").strip
|
|
9
7
|
|
|
10
8
|
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
|
|
11
9
|
gem "rails", "~> 7.0"
|
|
12
10
|
|
|
13
11
|
# Use the Puma web server [https://github.com/puma/puma]
|
|
14
|
-
gem "puma", "~> 6.
|
|
12
|
+
gem "puma", "~> 6.3"
|
|
15
13
|
|
|
16
14
|
# for making kick-ass http queries
|
|
17
|
-
gem "
|
|
15
|
+
gem "httpsensible", "~> 0.1"
|
|
18
16
|
|
|
19
17
|
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
|
|
20
18
|
gem "jbuilder", "~> 2.11"
|
|
@@ -32,18 +30,18 @@ gem "lograge", "~> 0.12"
|
|
|
32
30
|
gem "openapi_first", "~> 0.20"
|
|
33
31
|
|
|
34
32
|
# For Honeycomb.io
|
|
35
|
-
gem "opentelemetry-sdk", "~> 1.
|
|
36
|
-
gem "opentelemetry-exporter-otlp", "~> 0.
|
|
37
|
-
gem "opentelemetry-semantic_conventions", "~> 1.
|
|
33
|
+
gem "opentelemetry-sdk", "~> 1.2"
|
|
34
|
+
gem "opentelemetry-exporter-otlp", "~> 0.25"
|
|
35
|
+
gem "opentelemetry-semantic_conventions", "~> 1.10"
|
|
38
36
|
|
|
39
|
-
gem "opentelemetry-instrumentation-rack", "~> 0.
|
|
40
|
-
gem "opentelemetry-instrumentation-rails", "~> 0.
|
|
41
|
-
gem "opentelemetry-instrumentation-concurrent_ruby", "~> 0.
|
|
37
|
+
gem "opentelemetry-instrumentation-rack", "~> 0.23"
|
|
38
|
+
gem "opentelemetry-instrumentation-rails", "~> 0.27"
|
|
39
|
+
gem "opentelemetry-instrumentation-concurrent_ruby", "~> 0.21"
|
|
42
40
|
|
|
43
|
-
gem "opentelemetry-instrumentation-net_http", "~> 0.
|
|
41
|
+
gem "opentelemetry-instrumentation-net_http", "~> 0.22"
|
|
44
42
|
|
|
45
|
-
gem "opentelemetry-instrumentation-active_job", "~> 0.
|
|
46
|
-
gem "opentelemetry-instrumentation-redis", "~> 0.
|
|
43
|
+
gem "opentelemetry-instrumentation-active_job", "~> 0.5"
|
|
44
|
+
gem "opentelemetry-instrumentation-redis", "~> 0.25"
|
|
47
45
|
gem "opentelemetry-instrumentation-sidekiq", "~> 0.23"
|
|
48
46
|
|
|
49
47
|
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
|
|
@@ -55,10 +53,10 @@ gem "bootsnap", require: false
|
|
|
55
53
|
gem "safety_dance", "~> 1.0"
|
|
56
54
|
|
|
57
55
|
# Use Sidekiq for the jobs queue
|
|
58
|
-
gem "sidekiq", "~> 7.
|
|
56
|
+
gem "sidekiq", "~> 7.1"
|
|
59
57
|
|
|
60
58
|
# sends logs to Slack
|
|
61
|
-
gem "slack_webhook_logger", "~> 0.
|
|
59
|
+
gem "slack_webhook_logger", "~> 0.5"
|
|
62
60
|
|
|
63
61
|
group :development, :test do
|
|
64
62
|
# better debug output with `ap`
|
|
@@ -73,13 +71,15 @@ group :development, :test do
|
|
|
73
71
|
end
|
|
74
72
|
|
|
75
73
|
group :development do
|
|
74
|
+
gem "dockerfile-rails", "~> 1.5"
|
|
75
|
+
|
|
76
76
|
gem "dotenv-rails"
|
|
77
77
|
|
|
78
78
|
gem "foreman", "~> 0.87"
|
|
79
79
|
|
|
80
|
-
gem "licensed", "~> 4.
|
|
80
|
+
gem "licensed", "~> 4.4"
|
|
81
81
|
|
|
82
|
-
gem "ruby-lsp", "~> 0.
|
|
82
|
+
gem "ruby-lsp", "~> 0.6", require: false
|
|
83
83
|
|
|
84
84
|
gem "spoom"
|
|
85
85
|
gem "sorbet"
|
|
@@ -88,8 +88,8 @@ end
|
|
|
88
88
|
gem "sorbet-runtime"
|
|
89
89
|
|
|
90
90
|
group :test do
|
|
91
|
-
|
|
92
|
-
gem "
|
|
91
|
+
gem "simplecov", "~> 0.18", require: false
|
|
92
|
+
gem "simplecov-console", "~> 0.7", require: false
|
|
93
93
|
|
|
94
94
|
# track down flakey tests
|
|
95
95
|
gem "minitest-bisect"
|
|
@@ -106,9 +106,6 @@ group :test do
|
|
|
106
106
|
# jump around through time
|
|
107
107
|
gem "timecop", "~> 0.9"
|
|
108
108
|
|
|
109
|
-
# Easy installation and use of web drivers to run system tests with browsers
|
|
110
|
-
gem "webdrivers", "~> 5.0", require: false
|
|
111
|
-
|
|
112
109
|
# prevents real http requests
|
|
113
110
|
gem "webmock", "~> 3.8"
|
|
114
111
|
end
|
data/templates/Procfile.debug
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
web: rdbg -n
|
|
1
|
+
web: rdbg -n -O -c -- bin/rails server -p 6661
|
|
2
2
|
worker: bundle exec sidekiq -c 2
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
class AppController < ApplicationController
|
|
5
|
+
include Authable
|
|
6
|
+
|
|
7
|
+
include Constants::PlugApp
|
|
8
|
+
include PathParameter::AppParameters
|
|
9
|
+
include BodyParameter::AppParameters
|
|
10
|
+
|
|
11
|
+
before_action :from__app_?
|
|
12
|
+
|
|
13
|
+
# Inbound message from ${App}
|
|
14
|
+
def webhook
|
|
15
|
+
# Error if necesary parameters from ${App} are missing
|
|
16
|
+
return bad_request unless has_inbound__app__params?
|
|
17
|
+
|
|
18
|
+
response = YettoService.get_plug_installation(pparam_organization_id, pparam_inbox_id, pparam_plug_installation_id)
|
|
19
|
+
|
|
20
|
+
# Error if Yetto is down
|
|
21
|
+
return service_unavailable(response) if response.unavailable?
|
|
22
|
+
|
|
23
|
+
plug_installation = response.parse_json_body
|
|
24
|
+
installed_on_inbox = plug_installation.fetch("installed_on_inbox", {})
|
|
25
|
+
|
|
26
|
+
organization = installed_on_inbox.fetch("organization", {})
|
|
27
|
+
|
|
28
|
+
# Bail if the organization is not active
|
|
29
|
+
return forbidden unless organization.fetch("status", "") == "active"
|
|
30
|
+
|
|
31
|
+
plug_id = plug_installation.fetch("plug", {}).fetch("id", "")
|
|
32
|
+
inbox_id = installed_on_inbox.fetch("id", "")
|
|
33
|
+
organization_id = organization.fetch("id", "")
|
|
34
|
+
|
|
35
|
+
return bad_request if plug_id.blank? || inbox_id.blank?
|
|
36
|
+
|
|
37
|
+
# Send the message to Yetto
|
|
38
|
+
update_data = {
|
|
39
|
+
type: "create_message",
|
|
40
|
+
inbox: { id: inbox_id },
|
|
41
|
+
organization: { id: organization_id },
|
|
42
|
+
payload: {
|
|
43
|
+
creator: {
|
|
44
|
+
id: plug_id,
|
|
45
|
+
},
|
|
46
|
+
message: {
|
|
47
|
+
title: title,
|
|
48
|
+
text_content: text_body,
|
|
49
|
+
is_public: true,
|
|
50
|
+
author: {
|
|
51
|
+
name: from_email,
|
|
52
|
+
},
|
|
53
|
+
attachments: bparam_attachments,
|
|
54
|
+
metadata: {
|
|
55
|
+
cc_addresses: cc_addresses,
|
|
56
|
+
postmark_message_id: bparam_message_id,
|
|
57
|
+
email_message_id: email_message_id,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
UpdateYettoJob.perform_later(update_data)
|
|
64
|
+
|
|
65
|
+
created
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def process_inbound
|
|
69
|
+
no_content
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -65,7 +65,7 @@ class ApplicationController < ActionController::Base
|
|
|
65
65
|
)
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
-
def not_acceptable
|
|
68
|
+
def not_acceptable
|
|
69
69
|
render(
|
|
70
70
|
json: ::ErrorSerializer.format("Not Acceptable").to_json,
|
|
71
71
|
status: :not_acceptable,
|
|
@@ -104,4 +104,17 @@ class ApplicationController < ActionController::Base
|
|
|
104
104
|
status: :bad_gateway,
|
|
105
105
|
)
|
|
106
106
|
end
|
|
107
|
+
|
|
108
|
+
def internal_server_error
|
|
109
|
+
render(
|
|
110
|
+
json: {
|
|
111
|
+
errors: [
|
|
112
|
+
{
|
|
113
|
+
message: "Internal Server Error",
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
}.to_json,
|
|
117
|
+
status: :internal_server_error,
|
|
118
|
+
)
|
|
119
|
+
end
|
|
107
120
|
end
|
|
@@ -12,6 +12,24 @@ module Authable
|
|
|
12
12
|
|
|
13
13
|
SHA256_DIGEST = OpenSSL::Digest.new("sha256")
|
|
14
14
|
|
|
15
|
+
sig { void }
|
|
16
|
+
def from__app_?
|
|
17
|
+
state = params.fetch(:state, "")
|
|
18
|
+
_, _, gh_nonce, _, _, _, _ = parse_state(state)
|
|
19
|
+
|
|
20
|
+
return if ActiveSupport::SecurityUtils.secure_compare((gh_nonce || ""), PLUG_APP_NONCE)
|
|
21
|
+
|
|
22
|
+
self.status = PlugApp::HTTP::BAD_REQUEST_I
|
|
23
|
+
self.response_body = ::ErrorSerializer.format(PlugApp::HTTP::BAD_REQUEST)
|
|
24
|
+
|
|
25
|
+
return true if response.status == 200
|
|
26
|
+
|
|
27
|
+
# status is annoyingly set to 401, but we want
|
|
28
|
+
# to hide that an issue exists
|
|
29
|
+
response.status = PlugApp::HTTP::BAD_REQUEST_I
|
|
30
|
+
response.body = ::ErrorSerializer.format(PlugApp::HTTP::BAD_REQUEST)
|
|
31
|
+
end
|
|
32
|
+
|
|
15
33
|
sig { void }
|
|
16
34
|
def from_yetto?
|
|
17
35
|
return bad_request if request.headers.blank?
|
|
@@ -21,9 +39,9 @@ module Authable
|
|
|
21
39
|
return bad_request unless yetto_signature.start_with?("sha256=")
|
|
22
40
|
|
|
23
41
|
hmac_header = yetto_signature.split("sha256=").last
|
|
24
|
-
body = request.env
|
|
42
|
+
body = request.env.fetch("RAW_POST_DATA", "")
|
|
25
43
|
|
|
26
|
-
calculated_hmac = OpenSSL::HMAC.hexdigest(SHA256_DIGEST,
|
|
44
|
+
calculated_hmac = OpenSSL::HMAC.hexdigest(SHA256_DIGEST, SIGNING_SECRET, body)
|
|
27
45
|
|
|
28
46
|
return true if ActiveSupport::SecurityUtils.secure_compare(calculated_hmac, hmac_header)
|
|
29
47
|
|
|
@@ -1,7 +1,37 @@
|
|
|
1
|
-
# typed:
|
|
1
|
+
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
class SettingsController < ApplicationController
|
|
5
|
-
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
include PathParameter::SettingsParameters
|
|
8
|
+
|
|
9
|
+
sig { void }
|
|
10
|
+
def new
|
|
11
|
+
@step = T.let(params.fetch(:step, 1).to_i || 1, T.nilable(Integer))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
sig { void }
|
|
15
|
+
def edit
|
|
16
|
+
@step = params.fetch(:step, 1).to_i || 1
|
|
17
|
+
|
|
18
|
+
response = YettoService.get_plug_installation(pparam_plug_installation_id)
|
|
19
|
+
|
|
20
|
+
if response.unavailable?
|
|
21
|
+
logger.error("Fetching Yetto inbox failed: `#{response}`")
|
|
22
|
+
|
|
23
|
+
return not_acceptable
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
plug_installation = response.parse_json_body
|
|
27
|
+
access_token = plug_installation.fetch("credentials", {}).fetch("access_token", "")
|
|
28
|
+
|
|
29
|
+
if access_token.blank?
|
|
30
|
+
logger.error("Fetching Yetto access_token failed: `#{response}`")
|
|
31
|
+
|
|
32
|
+
return not_acceptable
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
not_found
|
|
6
36
|
end
|
|
7
37
|
end
|
|
@@ -15,13 +15,12 @@ class UpdateYettoJob < ApplicationJob
|
|
|
15
15
|
plug_installation_id = params.fetch(:plug_installation, {}).fetch(:id, nil)
|
|
16
16
|
|
|
17
17
|
case type
|
|
18
|
-
when "
|
|
19
|
-
YettoService.update_installation(
|
|
20
|
-
when "
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
YettoService.create_message(organization_id, inbox_id, params)
|
|
18
|
+
when "update_plug_installation"
|
|
19
|
+
YettoService.update_installation(plug_installation_id, params)
|
|
20
|
+
when "create_inbox_switch"
|
|
21
|
+
YettoService.create_inbox_switch(inbox_id, plug_installation_id, params)
|
|
22
|
+
when "create_message"
|
|
23
|
+
YettoService.create_message(inbox_id, plug_installation_id, params)
|
|
25
24
|
end
|
|
26
25
|
end
|
|
27
26
|
end
|
|
@@ -4,5 +4,37 @@
|
|
|
4
4
|
module BodyParameter
|
|
5
5
|
module YettoParameters
|
|
6
6
|
extend T::Sig
|
|
7
|
+
|
|
8
|
+
sig { returns(T::Boolean) }
|
|
9
|
+
def has_update_plug_installation_params?
|
|
10
|
+
return false if update_plug_installation_params.blank?
|
|
11
|
+
|
|
12
|
+
true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
sig { returns(T::Hash[Symbol, String]) }
|
|
16
|
+
def update_plug_installation_params
|
|
17
|
+
return {} if params.blank?
|
|
18
|
+
|
|
19
|
+
return {} if (plug_installation_id = params.fetch(:plug_installation, {}).fetch(:id, "")).blank?
|
|
20
|
+
return {} if (organization_id = params.fetch(:organization, {}).fetch(:id, "")).blank?
|
|
21
|
+
return {} if (plug_id = params.fetch(:plug, {}).fetch(:id, "")).blank?
|
|
22
|
+
return {} if (inbox_id = params.fetch(:inbox, {}).fetch(:id, "")).blank?
|
|
23
|
+
|
|
24
|
+
{
|
|
25
|
+
plug_installation: {
|
|
26
|
+
id: plug_installation_id,
|
|
27
|
+
},
|
|
28
|
+
organization: {
|
|
29
|
+
id: organization_id,
|
|
30
|
+
},
|
|
31
|
+
plug: {
|
|
32
|
+
id: plug_id,
|
|
33
|
+
},
|
|
34
|
+
inbox: {
|
|
35
|
+
id: inbox_id,
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
end
|
|
7
39
|
end
|
|
8
40
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module PathParameter
|
|
5
|
+
module SettingsParameters
|
|
6
|
+
extend T::Sig
|
|
7
|
+
|
|
8
|
+
sig { returns(String) }
|
|
9
|
+
def pparam_plug_installation_id
|
|
10
|
+
yetto_path_params.fetch(:event, "")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
14
|
+
def settings_path_params
|
|
15
|
+
return {} if params.blank?
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
plug_installation_id: params.fetch(:plug_installation_id, ""),
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -8,12 +8,12 @@ module PlugApp
|
|
|
8
8
|
class OpenapiValidation
|
|
9
9
|
API_PATH_PREFIX = "/api/"
|
|
10
10
|
SPEC_PATH = Rails.root.join("lib/plug_app/schemas/api/2023-03-06/openapi.json")
|
|
11
|
+
SPEC = OpenapiFirst.load(SPEC_PATH)
|
|
11
12
|
|
|
12
13
|
def initialize(app)
|
|
13
14
|
@app = app
|
|
14
|
-
|
|
15
|
-
@
|
|
16
|
-
@request_validator = OpenapiFirst::RequestValidation.new(->(_env) {}, spec: SPEC_PATH, raise_error: true)
|
|
15
|
+
@response_validator = OpenapiFirst::ResponseValidator.new(SPEC)
|
|
16
|
+
@request_validator = OpenapiFirst::RequestValidation.new(->(_env) {}, spec: SPEC, raise_error: true)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def call(env)
|
|
@@ -33,9 +33,9 @@ module PlugApp
|
|
|
33
33
|
PlugApp::Middleware::NotFound.new.call(env)
|
|
34
34
|
rescue OpenapiFirst::RequestInvalidError => e
|
|
35
35
|
error_hsh = ::ErrorSerializer.format(e.message)
|
|
36
|
-
|
|
36
|
+
Rails.logger.error(error_hsh) if print_user_api_errors?
|
|
37
37
|
return [PlugApp::HTTP::BAD_REQUEST_I, { "Content-Type" => "application/json" }, [error_hsh]]
|
|
38
|
-
rescue => e
|
|
38
|
+
rescue StandardError => e
|
|
39
39
|
raise e unless Rails.env.production?
|
|
40
40
|
|
|
41
41
|
logger.error(
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
class HTTPService
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
CACHE_VERSION = "1"
|
|
11
|
+
|
|
12
|
+
sig { returns(T.untyped) }
|
|
13
|
+
def http_client
|
|
14
|
+
Httpsensible::Client.new(user_agent: "PlugApp/#{PlugApp::Application::GIT_SHA}")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
sig { params(url: String, params: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
|
|
18
|
+
def get(url, params: {})
|
|
19
|
+
http_client.get(url, params: params)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
sig { params(url: String, params: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
|
|
23
|
+
def post(url, params: {})
|
|
24
|
+
http_client.post(url, params: params)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -1,61 +1,53 @@
|
|
|
1
|
-
# typed:
|
|
1
|
+
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
class YettoService
|
|
5
5
|
cattr_reader :yetto_client, instance_accessor: false do
|
|
6
|
-
|
|
7
|
-
"Content-Type" => "application/json",
|
|
8
|
-
"Accept" => "application/json",
|
|
9
|
-
"User-Agent" => "PlugApp/#{PlugApp::Application::GIT_SHA}",
|
|
10
|
-
})
|
|
6
|
+
Httpsensible::Client.new(user_agent: "PlugApp/#{PlugApp::Application::GIT_SHA}")
|
|
11
7
|
end
|
|
12
8
|
|
|
13
9
|
PROTOCOL = Rails.env.development? ? "http" : "https"
|
|
14
10
|
YETTO_API_VERSION_TLD = "#{PROTOCOL}://#{YETTO_API_TLD}/#{YETTO_API_VERSION}"
|
|
11
|
+
JWT_ALGORITHM = "RS256"
|
|
15
12
|
|
|
16
13
|
class << self
|
|
17
|
-
def
|
|
18
|
-
|
|
14
|
+
def perform_token_exchange(plug_installation_id)
|
|
15
|
+
encoded_jwt = Httpsensible::JWT.encode_jwt(YETTO_PLUG_PEM, YETTO_PLUG_ID)
|
|
16
|
+
response = yetto_client.with(headers: { "Authorization" => "Bearer #{encoded_jwt}" }).post("#{YETTO_API_VERSION_TLD}/plugs/installations/#{plug_installation_id}/access_tokens")
|
|
17
|
+
body = response.parsed_json_body
|
|
18
|
+
body["token"]
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def
|
|
21
|
+
def get_plug_installation(plug_installation_id)
|
|
22
|
+
token = perform_token_exchange(plug_installation_id)
|
|
23
|
+
yetto_client.with(headers: { "Authorization" => "Bearer #{token}" }).get("#{YETTO_API_VERSION_TLD}/installations/#{plug_installation_id}")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def update_installation(plug_installation_id, params)
|
|
22
27
|
plug_installation = {}
|
|
23
28
|
plug_installation[:settings] = params[:plug_installation].fetch(:settings, {})
|
|
24
29
|
plug_installation[:credentials] = params[:plug_installation].fetch(:credentials, {})
|
|
25
|
-
body = {
|
|
26
|
-
plug_installation: plug_installation,
|
|
27
|
-
}
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
token = perform_token_exchange(plug_installation_id)
|
|
32
|
+
yetto_client.with(headers: { "Authorization" => "Bearer #{token}" }).patch("#{YETTO_API_VERSION_TLD}/installations/#{plug_installation_id}", json: plug_installation)
|
|
30
33
|
end
|
|
31
34
|
|
|
32
|
-
def
|
|
35
|
+
def create_inbox_switch(inbox_id, plug_installation_id, params)
|
|
33
36
|
inbox_id = params.fetch(:inbox).fetch(:id)
|
|
34
|
-
|
|
37
|
+
|
|
35
38
|
payload = {
|
|
36
39
|
name: "After install",
|
|
37
|
-
creator: { id: creator_id },
|
|
38
40
|
}
|
|
39
|
-
yetto_client.post("#{YETTO_API_VERSION_TLD}/organizations/#{organization_id}/inboxes/#{inbox_id}/switches", json: payload)
|
|
40
|
-
end
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
yetto_client.post("#{YETTO_API_VERSION_TLD}/organizations/#{organization_id}/inboxes/#{inbox_id}/messages", json: payload)
|
|
42
|
+
token = perform_token_exchange(plug_installation_id)
|
|
43
|
+
yetto_client.with(headers: { "Authorization" => "Bearer #{token}" }).post("#{YETTO_API_VERSION_TLD}/inboxes/#{inbox_id}/switches", json: payload)
|
|
45
44
|
end
|
|
46
45
|
|
|
47
|
-
def
|
|
48
|
-
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def unavailable?(response)
|
|
52
|
-
return true unless (200..299).cover?(response.status)
|
|
53
|
-
|
|
54
|
-
response.body.blank? || response.body == "{}"
|
|
55
|
-
end
|
|
46
|
+
def create_message(inbox_id, plug_installation_id, params)
|
|
47
|
+
payload = params[:payload]
|
|
56
48
|
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
token = perform_token_exchange(plug_installation_id)
|
|
50
|
+
yetto_client.with(headers: { "Authorization" => "Bearer #{token}" }).post("#{YETTO_API_VERSION_TLD}/inboxes/#{inbox_id}/messages", json: payload)
|
|
59
51
|
end
|
|
60
52
|
end
|
|
61
53
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
json.version("2023-03-06")
|
|
4
|
+
json.settings do
|
|
5
|
+
json.array!([
|
|
6
|
+
{
|
|
7
|
+
component: "text_field",
|
|
8
|
+
name: "from_name",
|
|
9
|
+
heading: t(:some_example_heading),
|
|
10
|
+
placeholder: "{{ organization['name'] }} - {{ inbox['name'] }}",
|
|
11
|
+
label: t(:some_example),
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
component: "email_field",
|
|
15
|
+
name: "reply_to_email",
|
|
16
|
+
heading: t(:another_example_heading),
|
|
17
|
+
placeholder: "{{ inbox['slug'] }}@{{ organization['name'] | downcase }}.biz",
|
|
18
|
+
label: t(:another_example_label),
|
|
19
|
+
},
|
|
20
|
+
])
|
|
21
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/bin/bash -e
|
|
2
|
+
|
|
3
|
+
# Add any container initialization steps here
|
|
4
|
+
git config --global --add safe.directory /plug-app
|
|
5
|
+
|
|
6
|
+
# allocate swap space
|
|
7
|
+
fallocate -l 2GB /swapfile
|
|
8
|
+
chmod 0600 /swapfile
|
|
9
|
+
mkswap /swapfile
|
|
10
|
+
echo 10 > /proc/sys/vm/swappiness
|
|
11
|
+
swapon /swapfile
|
|
12
|
+
echo 1 > /proc/sys/vm/overcommit_memory
|
|
13
|
+
|
|
14
|
+
exec "${@}"
|
|
@@ -14,16 +14,13 @@ elsif Rails.env.test?
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
YETTO_API_TLD = ENV.fetch("YETTO_API_TLD", "#{YETTO_URL}/api")
|
|
17
|
-
YETTO_API_VERSION = ENV.fetch("YETTO_API_VERSION", "
|
|
17
|
+
YETTO_API_VERSION = ENV.fetch("YETTO_API_VERSION", "2023-03-06")
|
|
18
18
|
|
|
19
|
-
SLACK_LOG_URL = ENV.fetch("SLACK_LOG_URL",
|
|
19
|
+
SLACK_LOG_URL = Rails.application.credentials.fetch(:SLACK_LOG_URL, ENV.fetch("SLACK_LOG_URL", "https://slack.com/the_log_room"))
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
OTEL_EXPORTER_OTLP_ENDPOINT = ENV.fetch("OTEL_EXPORTER_OTLP_ENDPOINT", "https://api.honeycomb.io")
|
|
25
|
-
OTEL_EXPORTER_OTLP_HEADERS = ENV.fetch("OTEL_EXPORTER_OTLP_HEADERS", "x-honeycomb-team=your-api-key")
|
|
26
|
-
OTEL_SERVICE_NAME = ENV.fetch("OTEL_SERVICE_NAME", "your-service-name")
|
|
21
|
+
YETTO_PLUG_PEM = Rails.application.credentials.fetch(:YETTO_PLUG_PEM, ENV.fetch("YETTO_PLUG_PEM", Rails.root.join("test/fixtures/files/fake_pem_file/fake.pem").read))
|
|
22
|
+
SIGNING_SECRET = Rails.application.credentials.fetch(:PLUG_EMAIL_URL, ENV.fetch("PLUG_EMAIL_URL", "super-secret"))
|
|
23
|
+
YETTO_PLUG_ID = Rails.application.credentials.fetch(:YETTO_PLUG_ID, ENV.fetch("YETTO_PLUG_ID", "super-secret"))
|
|
27
24
|
|
|
28
25
|
def productionish?
|
|
29
26
|
Rails.env.production? || Rails.env.staging?
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
unless Rails.env.development?
|
|
5
|
+
# establish the environment for OTEL
|
|
6
|
+
ENV["OTEL_EXPORTER_OTLP_ENDPOINT"] = Rails.application.credentials.fetch(:OTEL_EXPORTER_OTLP_ENDPOINT, ENV.fetch("OTEL_EXPORTER_OTLP_ENDPOINT", "https://api.honeycomb.io"))
|
|
7
|
+
ENV["OTEL_EXPORTER_OTLP_HEADERS"] = Rails.application.credentials.fetch(:OTEL_EXPORTER_OTLP_HEADERS, ENV.fetch("OTEL_EXPORTER_OTLP_HEADERS", "x-honeycomb-team=your-api-key"))
|
|
8
|
+
ENV["OTEL_SERVICE_NAME"] = Rails.application.credentials.fetch(:OTEL_SERVICE_NAME, ENV.fetch("OTEL_SERVICE_NAME", "your-service-name"))
|
|
9
|
+
|
|
10
|
+
require "opentelemetry/sdk"
|
|
11
|
+
require "opentelemetry/semantic_conventions"
|
|
12
|
+
|
|
13
|
+
OpenTelemetry::SDK.configure do |c|
|
|
14
|
+
c.logger = Rails.logger
|
|
15
|
+
|
|
16
|
+
c.use_all("OpenTelemetry::Instrumentation::PG" => { db_statement: :obfuscate })
|
|
17
|
+
|
|
18
|
+
if productionish?
|
|
19
|
+
c.add_span_processor(
|
|
20
|
+
OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
|
|
21
|
+
OpenTelemetry::Exporter::OTLP::Exporter.new,
|
|
22
|
+
),
|
|
23
|
+
)
|
|
24
|
+
else # useful for testing instrumentation
|
|
25
|
+
c.add_span_processor(
|
|
26
|
+
OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(
|
|
27
|
+
OpenTelemetry::SDK::Trace::Export::SpanExporter.new,
|
|
28
|
+
),
|
|
29
|
+
)
|
|
30
|
+
end # development is intentionally disabled
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Files in the config/locales directory are used for internationalization
|
|
2
|
+
# and are automatically loaded by Rails. If you want to use locales other
|
|
3
|
+
# than English, add the necessary files in this directory.
|
|
4
|
+
#
|
|
5
|
+
# To use the locales, use `I18n.t`:
|
|
6
|
+
#
|
|
7
|
+
# I18n.t "hello"
|
|
8
|
+
#
|
|
9
|
+
# In views, this is aliased to just `t`:
|
|
10
|
+
#
|
|
11
|
+
# <%= t("hello") %>
|
|
12
|
+
#
|
|
13
|
+
# To use a different locale, set it with `I18n.locale`:
|
|
14
|
+
#
|
|
15
|
+
# I18n.locale = :es
|
|
16
|
+
#
|
|
17
|
+
# This would use the information in config/locales/es.yml.
|
|
18
|
+
#
|
|
19
|
+
# The following keys must be escaped otherwise they will not be retrieved by
|
|
20
|
+
# the default I18n backend:
|
|
21
|
+
#
|
|
22
|
+
# true, false, on, off, yes, no
|
|
23
|
+
#
|
|
24
|
+
# Instead, surround them with single quotes.
|
|
25
|
+
#
|
|
26
|
+
# en:
|
|
27
|
+
# "true": "foo"
|
|
28
|
+
#
|
|
29
|
+
# To learn more, please read the Rails Internationalization guide
|
|
30
|
+
# available at https://guides.rubyonrails.org/i18n.html.
|
|
31
|
+
|
|
32
|
+
en:
|