command_tower 0.16.0 → 0.18.0
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/README.md +8 -8
- data/app/controllers/command_tower/application_controller.rb +1 -0
- data/app/controllers/command_tower/me/inbox_controller.rb +16 -0
- data/app/controllers/concerns/command_tower/execution/client_compatibility_boundary.rb +39 -0
- data/app/deserializers/command_tower/deserializers/messaging/inbox.rb +85 -0
- data/app/errors/command_tower/errors/client_update_required_error.rb +27 -0
- data/app/jobs/command_tower/messaging/communications/produce_recipient_job.rb +2 -0
- data/app/models/command_tower/messaging/communication.rb +31 -0
- data/app/serializers/command_tower/serializers/messaging/inbox.rb +45 -1
- data/app/services/command_tower/email_theme/resolver.rb +45 -0
- data/app/services/command_tower/messaging/accept/coordinator.rb +16 -1
- data/app/services/command_tower/messaging/accept/persister.rb +13 -0
- data/app/services/command_tower/messaging/contract/mappers/communication_mapper.rb +7 -0
- data/app/services/command_tower/messaging/contract/results/communication_result.rb +1 -0
- data/app/services/command_tower/messaging/inbox/conversation_result.rb +31 -0
- data/app/services/command_tower/messaging/inbox/entry_result.rb +36 -0
- data/app/services/command_tower/messaging/inbox/reader.rb +231 -18
- data/app/services/command_tower/messaging/inbox.rb +4 -0
- data/app/services/command_tower/messaging/notification_types/declaration.rb +14 -1
- data/app/services/command_tower/messaging/rendering/channel_renderer.rb +44 -13
- data/app/services/command_tower/messaging/rendering/inbox_document.rb +131 -0
- data/app/services/command_tower/messaging/rendering/inbox_document_renderer.rb +224 -0
- data/app/services/command_tower/messaging/rendering/inbox_presentation_resolver.rb +52 -0
- data/app/services/command_tower/messaging/rendering/inbox_presentation_snapshot.rb +64 -0
- data/app/services/command_tower/messaging/rendering/template_resolver.rb +128 -0
- data/app/services/command_tower/messaging.rb +5 -1
- data/app/services/command_tower/services/client_compatibility/evaluate.rb +219 -0
- data/app/services/command_tower/services/messaging/communications/produce.rb +4 -0
- data/app/services/command_tower/services/messaging/communications/produce_many.rb +6 -0
- data/app/services/command_tower/services/messaging/inbox.rb +55 -2
- data/app/views/command_tower/email_verification_mailer/verify_email.html.erb +18 -17
- data/app/views/command_tower/messaging/rendering/email.html.erb +6 -5
- data/app/views/command_tower/password_reset_mailer/reset_password.html.erb +24 -23
- data/app/workflows/command_tower/workflows/auth/plain_text/login_workflow.rb +3 -0
- data/app/workflows/command_tower/workflows/auth/session/show_workflow.rb +3 -0
- data/app/workflows/command_tower/workflows/client_compatibility/evaluate_workflow.rb +83 -0
- data/app/workflows/command_tower/workflows/client_compatibility/recommendation_meta.rb +25 -0
- data/app/workflows/command_tower/workflows/messaging/communications/produce_recipient_workflow.rb +5 -1
- data/app/workflows/command_tower/workflows/messaging/inbox.rb +15 -1
- data/config/routes.rb +1 -0
- data/db/migrate/20261010000001_add_inbox_presentation_snapshot_to_messaging_communications.rb +7 -0
- data/db/migrate/20261010000002_add_conversation_identity_to_messaging_communications.rb +10 -0
- data/docs/api_reference.md +13 -2
- data/docs/authorization.md +1 -1
- data/docs/bootstrap/00-ownership.md +77 -0
- data/docs/bootstrap/01-docker-make-compose.md +267 -0
- data/docs/bootstrap/02-create-the-rails-app.md +75 -0
- data/docs/bootstrap/03-pin-the-gem.md +53 -0
- data/docs/bootstrap/04-secrets-and-env.md +59 -0
- data/docs/bootstrap/05-install-migrate-doctor.md +74 -0
- data/docs/bootstrap/06-mount-and-health.md +53 -0
- data/docs/bootstrap/07-execution-bases.md +38 -0
- data/docs/bootstrap/08-initializer.md +90 -0
- data/docs/bootstrap/09-rbac.md +83 -0
- data/docs/bootstrap/10-roles-and-gates.md +62 -0
- data/docs/bootstrap/11-auth-client-path.md +57 -0
- data/docs/bootstrap/12-smoke-check.md +81 -0
- data/docs/bootstrap/13-optional.md +50 -0
- data/docs/bootstrap/14-sanity-checks.md +44 -0
- data/docs/bootstrap/README.md +90 -0
- data/docs/cookie_authentication_guide.md +11 -0
- data/docs/extending.md +5 -4
- data/docs/host_integration_guide.md +3 -246
- data/docs/initializing.md +37 -28
- data/docs/messaging_integration_guide.md +39 -1
- data/docs/principal_capabilities.md +1 -1
- data/docs/upgrades/0.10.0.md +5 -5
- data/docs/upgrades/0.11.0.md +5 -5
- data/docs/upgrades/0.17.0.md +33 -0
- data/docs/upgrades/0.18.0.md +35 -0
- data/docs/upgrades/README.md +3 -1
- data/lib/command_tower/authorization/default.yml +1 -0
- data/lib/command_tower/client_compatibility/version.rb +45 -0
- data/lib/command_tower/client_compatibility.rb +23 -0
- data/lib/command_tower/configuration/config.rb +6 -0
- data/lib/command_tower/configuration/email_theme/config.rb +69 -0
- data/lib/command_tower/configuration/registry/client_compatibility/binding_definition.rb +42 -0
- data/lib/command_tower/configuration/registry/client_compatibility/config.rb +353 -0
- data/lib/command_tower/configuration/registry/client_compatibility/contract_definition.rb +16 -0
- data/lib/command_tower/configuration/registry/client_compatibility/entity_requirement_definition.rb +67 -0
- data/lib/command_tower/configuration/registry/client_compatibility/minimum_overrides.rb +46 -0
- data/lib/command_tower/configuration/registry/client_compatibility/platform_definition.rb +66 -0
- data/lib/command_tower/configuration/registry/config.rb +14 -0
- data/lib/command_tower/configuration/registry/inbox_presentations/config.rb +105 -0
- data/lib/command_tower/configuration/registry/inbox_presentations/presentation_definition.rb +71 -0
- data/lib/command_tower/current.rb +1 -0
- data/lib/command_tower/engine.rb +4 -0
- data/lib/command_tower/inbox_presentations.rb +19 -0
- data/lib/command_tower/install/baseline.rb +2 -0
- data/lib/command_tower/version.rb +1 -1
- metadata +47 -2
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Services
|
|
5
|
+
module ClientCompatibility
|
|
6
|
+
# Pure, deterministic client-version-compatibility decision.
|
|
7
|
+
#
|
|
8
|
+
# Same (headers, controller/action, registry state) always produces the
|
|
9
|
+
# same complete `Decision`. This service MUST NOT mutate
|
|
10
|
+
# `CommandTower::Current` or any other request-global — it has zero side
|
|
11
|
+
# effects beyond returning its result. Any placement of recommendation
|
|
12
|
+
# metadata onto `Current` is transport plumbing that belongs to the
|
|
13
|
+
# workflow or boundary layer, never here.
|
|
14
|
+
class Evaluate
|
|
15
|
+
DECISIONS = %i[compatible recommended incompatible identity_invalid].freeze
|
|
16
|
+
|
|
17
|
+
# Immutable decision/projection. `recommendation_projection` is the
|
|
18
|
+
# camelCase `clientCompatibility` shape (authority §13), present only
|
|
19
|
+
# when identity is parseable and current >= effective minimum.
|
|
20
|
+
class Decision
|
|
21
|
+
attr_reader :decision, :platform, :current_version, :effective_minimum, :scope,
|
|
22
|
+
:recovery, :update_url, :matched_entity_names, :recommendation_projection
|
|
23
|
+
|
|
24
|
+
def initialize(
|
|
25
|
+
decision:,
|
|
26
|
+
platform: nil,
|
|
27
|
+
current_version: nil,
|
|
28
|
+
effective_minimum: nil,
|
|
29
|
+
scope: nil,
|
|
30
|
+
recovery: nil,
|
|
31
|
+
update_url: nil,
|
|
32
|
+
matched_entity_names: [],
|
|
33
|
+
recommendation_projection: nil
|
|
34
|
+
)
|
|
35
|
+
@decision = decision
|
|
36
|
+
@platform = platform
|
|
37
|
+
@current_version = current_version
|
|
38
|
+
@effective_minimum = effective_minimum
|
|
39
|
+
@scope = scope
|
|
40
|
+
@recovery = recovery
|
|
41
|
+
@update_url = update_url
|
|
42
|
+
@matched_entity_names = matched_entity_names
|
|
43
|
+
@recommendation_projection = recommendation_projection
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def compatible? = %i[compatible recommended].include?(@decision)
|
|
47
|
+
|
|
48
|
+
def incompatible? = !compatible?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.call(app_version_header:, platform_header:, controller_class:, action_name:, registry: CommandTower.config.registry.client_compatibility)
|
|
52
|
+
new(
|
|
53
|
+
app_version_header:,
|
|
54
|
+
platform_header:,
|
|
55
|
+
controller_class:,
|
|
56
|
+
action_name:,
|
|
57
|
+
registry:
|
|
58
|
+
).call
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def initialize(app_version_header:, platform_header:, controller_class:, action_name:, registry:)
|
|
62
|
+
@app_version_header = app_version_header
|
|
63
|
+
@platform_header = platform_header
|
|
64
|
+
@controller_class = controller_class
|
|
65
|
+
@action_name = action_name.to_s
|
|
66
|
+
@registry = registry
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def call
|
|
70
|
+
return no_op_decision unless registry.opted_in?
|
|
71
|
+
|
|
72
|
+
platform = normalized_platform
|
|
73
|
+
version = normalized_version
|
|
74
|
+
return identity_invalid_decision(platform:) if platform.nil? || version.nil?
|
|
75
|
+
|
|
76
|
+
policy = registry.platform_policy(platform)
|
|
77
|
+
return identity_invalid_decision(platform:, current_version: version) if policy.nil?
|
|
78
|
+
|
|
79
|
+
matched = matched_entity_names
|
|
80
|
+
effective_minimum, scope = effective_minimum_and_scope(policy:, platform:, matched:)
|
|
81
|
+
|
|
82
|
+
current = Gem::Version.new(version)
|
|
83
|
+
minimum = Gem::Version.new(effective_minimum)
|
|
84
|
+
|
|
85
|
+
if current < minimum
|
|
86
|
+
return incompatible_decision(
|
|
87
|
+
platform:, current_version: version, effective_minimum:, scope:, matched:, policy:
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
compatible_or_recommended_decision(
|
|
92
|
+
platform:, current_version: version, effective_minimum:, matched:, policy:, current:
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
attr_reader :controller_class, :action_name, :registry
|
|
99
|
+
|
|
100
|
+
def normalized_platform
|
|
101
|
+
token = @platform_header.to_s.strip.downcase
|
|
102
|
+
return nil if token.empty?
|
|
103
|
+
return nil unless CommandTower::Configuration::Registry::ClientCompatibility::Config::PLATFORMS.map(&:to_s).include?(token)
|
|
104
|
+
|
|
105
|
+
token.to_sym
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def normalized_version
|
|
109
|
+
CommandTower::ClientCompatibility::Version.normalize(@app_version_header)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def matched_entity_names
|
|
113
|
+
CommandTower::Authorization::Entity.entities.values
|
|
114
|
+
.select { |entity| entity.matches?(controller: controller_class, method: action_name.to_sym) == true }
|
|
115
|
+
.map { |entity| entity.name.to_s }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def effective_minimum_and_scope(policy:, platform:, matched:)
|
|
119
|
+
strictest = policy.minimum
|
|
120
|
+
scope = :application
|
|
121
|
+
|
|
122
|
+
matched.each do |entity_name|
|
|
123
|
+
requirement = registry.entity_requirement(entity_name)
|
|
124
|
+
next if requirement.nil?
|
|
125
|
+
|
|
126
|
+
entity_minimum = entity_minimum_for(requirement, platform:)
|
|
127
|
+
next if entity_minimum.nil?
|
|
128
|
+
|
|
129
|
+
if Gem::Version.new(entity_minimum) > Gem::Version.new(strictest)
|
|
130
|
+
strictest = entity_minimum
|
|
131
|
+
scope = :capability
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
[strictest, scope]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def entity_minimum_for(requirement, platform:)
|
|
139
|
+
if requirement.client_contract.present?
|
|
140
|
+
binding = registry.binding_for(requirement.client_contract)
|
|
141
|
+
binding&.for_platform(platform)
|
|
142
|
+
else
|
|
143
|
+
requirement.minimum.for_platform(platform)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def no_op_decision
|
|
148
|
+
Decision.new(decision: :compatible)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def identity_invalid_decision(platform: nil, current_version: nil)
|
|
152
|
+
Decision.new(
|
|
153
|
+
decision: :identity_invalid,
|
|
154
|
+
platform:,
|
|
155
|
+
current_version:,
|
|
156
|
+
scope: :application,
|
|
157
|
+
recovery: recovery_for(platform),
|
|
158
|
+
update_url: update_url_for(platform)
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def incompatible_decision(platform:, current_version:, effective_minimum:, scope:, matched:, policy:)
|
|
163
|
+
Decision.new(
|
|
164
|
+
decision: :incompatible,
|
|
165
|
+
platform:,
|
|
166
|
+
current_version:,
|
|
167
|
+
effective_minimum:,
|
|
168
|
+
scope:,
|
|
169
|
+
recovery: recovery_for(platform),
|
|
170
|
+
update_url: update_url_for(platform),
|
|
171
|
+
matched_entity_names: matched,
|
|
172
|
+
recommendation_projection: nil
|
|
173
|
+
)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def compatible_or_recommended_decision(platform:, current_version:, effective_minimum:, matched:, policy:, current:)
|
|
177
|
+
recommended = policy.recommended
|
|
178
|
+
update_available = recommended.present? && current < Gem::Version.new(recommended)
|
|
179
|
+
|
|
180
|
+
Decision.new(
|
|
181
|
+
decision: update_available ? :recommended : :compatible,
|
|
182
|
+
platform:,
|
|
183
|
+
current_version:,
|
|
184
|
+
effective_minimum:,
|
|
185
|
+
scope: :application,
|
|
186
|
+
matched_entity_names: matched,
|
|
187
|
+
recommendation_projection: recommendation_projection(
|
|
188
|
+
platform:, current_version:, effective_minimum:, recommended:, update_available:
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def recommendation_projection(platform:, current_version:, effective_minimum:, recommended:, update_available:)
|
|
194
|
+
{
|
|
195
|
+
platform: platform.to_s,
|
|
196
|
+
currentVersion: current_version,
|
|
197
|
+
minimumVersion: effective_minimum,
|
|
198
|
+
recommendedVersion: recommended,
|
|
199
|
+
updateAvailable: update_available,
|
|
200
|
+
recovery: recovery_for(platform).to_s,
|
|
201
|
+
updateUrl: update_url_for(platform)
|
|
202
|
+
}.compact
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def recovery_for(platform)
|
|
206
|
+
return :reload if platform.nil? || platform == :web
|
|
207
|
+
|
|
208
|
+
:app_store
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def update_url_for(platform)
|
|
212
|
+
return nil if platform.nil? || platform == :web
|
|
213
|
+
|
|
214
|
+
registry.platform_policy(platform)&.update_url
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
@@ -15,6 +15,8 @@ module CommandTower
|
|
|
15
15
|
validate :body, is_a: String, required: true
|
|
16
16
|
validate :metadata, is_a: Hash, required: false
|
|
17
17
|
validate :platform_enabled_channels, is_a: Array, required: true
|
|
18
|
+
validate :conversation_key, is_a: String, required: false
|
|
19
|
+
validate :conversation_title, is_a: String, required: false
|
|
18
20
|
|
|
19
21
|
def call
|
|
20
22
|
recipient_result = CommandTower::Services::Messaging::Recipients.call(user: user)
|
|
@@ -33,6 +35,8 @@ module CommandTower
|
|
|
33
35
|
preference_state: nil,
|
|
34
36
|
platform_enabled_channels:,
|
|
35
37
|
message_overrides: nil,
|
|
38
|
+
conversation_key:,
|
|
39
|
+
conversation_title:,
|
|
36
40
|
)
|
|
37
41
|
|
|
38
42
|
# Use communication_status — ServiceResult strips Interactor reserved :status.
|
|
@@ -18,6 +18,8 @@ module CommandTower
|
|
|
18
18
|
validate :body, is_a: String, required: true
|
|
19
19
|
validate :platform_enabled_channels, is_a: Array, required: true
|
|
20
20
|
validate :metadata, is_a: Hash, required: false
|
|
21
|
+
validate :conversation_key, is_a: String, required: false
|
|
22
|
+
validate :conversation_title, is_a: String, required: false
|
|
21
23
|
validate :execution_mode, is_one: EXECUTION_MODES, required: false, default: :async
|
|
22
24
|
|
|
23
25
|
def call
|
|
@@ -128,6 +130,8 @@ module CommandTower
|
|
|
128
130
|
body:,
|
|
129
131
|
metadata:,
|
|
130
132
|
platform_enabled_channels:,
|
|
133
|
+
conversation_key:,
|
|
134
|
+
conversation_title:,
|
|
131
135
|
)
|
|
132
136
|
end
|
|
133
137
|
|
|
@@ -139,6 +143,8 @@ module CommandTower
|
|
|
139
143
|
"body" => body,
|
|
140
144
|
"platform_enabled_channels" => platform_enabled_channels,
|
|
141
145
|
"metadata" => metadata,
|
|
146
|
+
"conversation_key" => conversation_key,
|
|
147
|
+
"conversation_title" => conversation_title,
|
|
142
148
|
}
|
|
143
149
|
end
|
|
144
150
|
|
|
@@ -34,7 +34,7 @@ module CommandTower
|
|
|
34
34
|
def call
|
|
35
35
|
with_recipient do |recipient_id|
|
|
36
36
|
result = CommandTower::Messaging::Inbox.list(recipient_id:, limit:, offset:, scope:)
|
|
37
|
-
context.items = result.items.map { |
|
|
37
|
+
context.items = result.items.map { |entry| Inbox.entry(entry) }
|
|
38
38
|
context.pagination = { limit: result.limit, offset: result.offset, total_count: result.total_count }
|
|
39
39
|
end
|
|
40
40
|
end
|
|
@@ -80,6 +80,37 @@ module CommandTower
|
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
class Conversation < CommandTower::Services::ApplicationService
|
|
84
|
+
include FailureMapping
|
|
85
|
+
validate :user, is_a: User, required: true
|
|
86
|
+
validate :key, is_a: String, required: true
|
|
87
|
+
validate :scope, is_a: String, required: false, default: "inbox"
|
|
88
|
+
validate :around, is_a: [String, Integer], required: false
|
|
89
|
+
validate :before_id, is_a: Integer, required: false
|
|
90
|
+
validate :after_id, is_a: Integer, required: false
|
|
91
|
+
validate :limit, is_a: Integer, required: false
|
|
92
|
+
|
|
93
|
+
def call
|
|
94
|
+
with_recipient do |recipient_id|
|
|
95
|
+
found = CommandTower::Messaging::Inbox.conversation(
|
|
96
|
+
recipient_id:, key:, scope:, around:, before_id:, after_id:, limit:,
|
|
97
|
+
)
|
|
98
|
+
messages = found.message_ids.map do |inbox_item_id|
|
|
99
|
+
Inbox.detail(recipient_id:, inbox_item_id:)
|
|
100
|
+
end
|
|
101
|
+
context.conversation = {
|
|
102
|
+
scope: found.scope,
|
|
103
|
+
subject: found.subject,
|
|
104
|
+
message_count: found.message_count,
|
|
105
|
+
unread_count: found.unread_count,
|
|
106
|
+
inbox_item_ids: found.inbox_item_ids,
|
|
107
|
+
messages:,
|
|
108
|
+
window: found.window,
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
83
114
|
class UnreadCount < CommandTower::Services::ApplicationService
|
|
84
115
|
include FailureMapping
|
|
85
116
|
validate :user, is_a: User, required: true
|
|
@@ -115,12 +146,34 @@ module CommandTower
|
|
|
115
146
|
{ id: item.id, title: item.title, status: item.status, viewed_at: item.viewed_at, created_at: item.created_at, updated_at: item.updated_at }
|
|
116
147
|
end
|
|
117
148
|
|
|
149
|
+
def entry(entry)
|
|
150
|
+
{
|
|
151
|
+
kind: entry.kind,
|
|
152
|
+
id: entry.id,
|
|
153
|
+
title: entry.title,
|
|
154
|
+
preview: entry.preview,
|
|
155
|
+
activity_at: entry.activity_at,
|
|
156
|
+
created_at: entry.created_at,
|
|
157
|
+
message_count: entry.message_count,
|
|
158
|
+
unread_count: entry.unread_count,
|
|
159
|
+
read: entry.read,
|
|
160
|
+
inbox_item_ids: entry.inbox_item_ids,
|
|
161
|
+
}
|
|
162
|
+
end
|
|
163
|
+
|
|
118
164
|
def detail(recipient_id:, inbox_item_id:)
|
|
119
165
|
item = CommandTower::Messaging::Inbox.show(recipient_id:, inbox_item_id:)
|
|
120
166
|
communication = CommandTower::Messaging::Contract::Communications.find(
|
|
121
167
|
CommandTower::Messaging::Contract::Requests::FindCommunication.build(communication_id: item.communication_id, recipient_id:)
|
|
122
168
|
)
|
|
123
|
-
|
|
169
|
+
content = CommandTower::Messaging::Rendering::InboxDocumentRenderer.render(communication:, recipient_id:).to_h
|
|
170
|
+
|
|
171
|
+
item(item).merge(
|
|
172
|
+
body: communication.body,
|
|
173
|
+
metadata: communication.metadata,
|
|
174
|
+
notification_type_key: communication.notification_type_key,
|
|
175
|
+
content:,
|
|
176
|
+
)
|
|
124
177
|
end
|
|
125
178
|
end
|
|
126
179
|
end
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
<% theme = CommandTower::EmailTheme::Resolver.resolve -%>
|
|
2
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: <%= theme[:canvas_background] %>; padding: 40px 20px;">
|
|
2
3
|
<tr>
|
|
3
4
|
<td align="center">
|
|
4
|
-
<table width="600" cellpadding="0" cellspacing="0" border="0" style="background-color:
|
|
5
|
+
<table width="600" cellpadding="0" cellspacing="0" border="0" style="background-color: <%= theme[:surface_background] %>; border-radius: 12px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); overflow: hidden;">
|
|
5
6
|
<!-- Header -->
|
|
6
7
|
<tr>
|
|
7
|
-
<td style="background:
|
|
8
|
-
<h1 style="margin: 0; color:
|
|
8
|
+
<td style="background: <%= theme[:accent] %>; padding: 40px 30px; text-align: center;">
|
|
9
|
+
<h1 style="margin: 0; color: <%= theme[:text_on_accent] %>; font-size: 28px; font-weight: 600; letter-spacing: -0.5px;">
|
|
9
10
|
Welcome to <%= CommandTower.config.app.communication_name %>!
|
|
10
11
|
</h1>
|
|
11
12
|
</td>
|
|
@@ -14,44 +15,44 @@
|
|
|
14
15
|
<!-- Content -->
|
|
15
16
|
<tr>
|
|
16
17
|
<td style="padding: 40px 30px;">
|
|
17
|
-
<p style="margin: 0 0 20px 0; color:
|
|
18
|
-
Hello <strong style="color:
|
|
18
|
+
<p style="margin: 0 0 20px 0; color: <%= theme[:body_text] %>; font-size: 16px; line-height: 1.6;">
|
|
19
|
+
Hello <strong style="color: <%= theme[:primary_text] %>;"><%= @user.full_name %></strong>,
|
|
19
20
|
</p>
|
|
20
21
|
|
|
21
|
-
<p style="margin: 0 0 30px 0; color:
|
|
22
|
+
<p style="margin: 0 0 30px 0; color: <%= theme[:body_text] %>; font-size: 16px; line-height: 1.6;">
|
|
22
23
|
Thank you for joining us! To complete your registration and verify your email address, please use the verification code below:
|
|
23
24
|
</p>
|
|
24
25
|
|
|
25
26
|
<!-- Verification Code Box -->
|
|
26
27
|
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin: 30px 0;">
|
|
27
28
|
<tr>
|
|
28
|
-
<td align="center" style="padding: 30px; background:
|
|
29
|
-
<div style="font-size: 36px; font-weight: 700; letter-spacing: 8px; color:
|
|
29
|
+
<td align="center" style="padding: 30px; background-color: <%= theme[:canvas_background] %>; border-radius: 8px; border: 2px dashed <%= theme[:surface_border] %>;">
|
|
30
|
+
<div style="font-size: 36px; font-weight: 700; letter-spacing: 8px; color: <%= theme[:accent] %>; font-family: 'Courier New', monospace; text-align: center;">
|
|
30
31
|
<%= @code %>
|
|
31
32
|
</div>
|
|
32
|
-
<p style="margin: 15px 0 0 0; color:
|
|
33
|
+
<p style="margin: 15px 0 0 0; color: <%= theme[:muted_text] %>; font-size: 12px; text-transform: uppercase; letter-spacing: 1px;">
|
|
33
34
|
Verification Code
|
|
34
35
|
</p>
|
|
35
36
|
</td>
|
|
36
37
|
</tr>
|
|
37
38
|
</table>
|
|
38
39
|
|
|
39
|
-
<p style="margin: 30px 0 20px 0; color:
|
|
40
|
-
<strong style="color:
|
|
40
|
+
<p style="margin: 30px 0 20px 0; color: <%= theme[:muted_text] %>; font-size: 14px; line-height: 1.6;">
|
|
41
|
+
<strong style="color: <%= theme[:body_text] %>;">Important:</strong> This code will expire in <%= CommandTower.config.login.plain_text.email_verify.verify_code_link_valid_for.inspect %>. If you didn't request this code, please ignore this email.
|
|
41
42
|
</p>
|
|
42
43
|
</td>
|
|
43
44
|
</tr>
|
|
44
45
|
|
|
45
46
|
<!-- Footer -->
|
|
46
47
|
<tr>
|
|
47
|
-
<td style="padding: 30px; background-color:
|
|
48
|
-
<p style="margin: 0 0 15px 0; color:
|
|
48
|
+
<td style="padding: 30px; background-color: <%= theme[:canvas_background] %>; border-top: 1px solid <%= theme[:surface_border] %>;">
|
|
49
|
+
<p style="margin: 0 0 15px 0; color: <%= theme[:body_text] %>; font-size: 14px; text-align: center; line-height: 1.6;">
|
|
49
50
|
Best regards,<br>
|
|
50
|
-
<strong style="color:
|
|
51
|
+
<strong style="color: <%= theme[:accent] %>;"><%= CommandTower.config.app.communication_name %> Team</strong>
|
|
51
52
|
</p>
|
|
52
53
|
|
|
53
54
|
<p style="margin: 0; text-align: center;">
|
|
54
|
-
<a href="<%= CommandTower.config.app.composed_url %>" target="_blank" style="color:
|
|
55
|
+
<a href="<%= CommandTower.config.app.composed_url %>" target="_blank" style="color: <%= theme[:primary_action] %>; text-decoration: none; font-size: 14px; font-weight: 500;">
|
|
55
56
|
Visit <%= CommandTower.config.app.communication_name %>
|
|
56
57
|
</a>
|
|
57
58
|
</p>
|
|
@@ -63,7 +64,7 @@
|
|
|
63
64
|
<table width="600" cellpadding="0" cellspacing="0" border="0" style="margin-top: 20px;">
|
|
64
65
|
<tr>
|
|
65
66
|
<td style="text-align: center; padding: 20px 0;">
|
|
66
|
-
<p style="margin: 0; color:
|
|
67
|
+
<p style="margin: 0; color: <%= theme[:muted_text] %>; font-size: 12px; line-height: 1.5;">
|
|
67
68
|
This is an automated message. Please do not reply to this email.
|
|
68
69
|
</p>
|
|
69
70
|
</td>
|
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
<% theme = CommandTower::EmailTheme::Resolver.resolve -%>
|
|
2
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: <%= theme[:canvas_background] %>; padding: 32px 16px;">
|
|
2
3
|
<tr>
|
|
3
4
|
<td align="center">
|
|
4
|
-
<table width="600" cellpadding="0" cellspacing="0" border="0" style="background-color:
|
|
5
|
+
<table width="600" cellpadding="0" cellspacing="0" border="0" style="background-color: <%= theme[:surface_background] %>; border: 1px solid <%= theme[:surface_border] %>; border-radius: 8px; overflow: hidden;">
|
|
5
6
|
<tr>
|
|
6
7
|
<td style="padding: 28px 28px 8px 28px;">
|
|
7
|
-
<h1 style="margin: 0; color:
|
|
8
|
+
<h1 style="margin: 0; color: <%= theme[:primary_text] %>; font-size: 22px; font-weight: 600; line-height: 1.35;">
|
|
8
9
|
<%= h.call(title) %>
|
|
9
10
|
</h1>
|
|
10
11
|
</td>
|
|
11
12
|
</tr>
|
|
12
13
|
<tr>
|
|
13
14
|
<td style="padding: 8px 28px 28px 28px;">
|
|
14
|
-
<p style="margin: 0; color:
|
|
15
|
+
<p style="margin: 0; color: <%= theme[:body_text] %>; font-size: 16px; line-height: 1.6; white-space: pre-wrap;">
|
|
15
16
|
<%= h.call(body) %></p>
|
|
16
17
|
<% if deep_link -%>
|
|
17
18
|
<p style="margin: 24px 0 0 0;">
|
|
18
|
-
<a href="<%= h.call(deep_link) %>" style="color:
|
|
19
|
+
<a href="<%= h.call(deep_link) %>" style="color: <%= theme[:primary_action] %>; font-size: 14px; text-decoration: underline;">
|
|
19
20
|
<%= h.call(deep_link) %>
|
|
20
21
|
</a>
|
|
21
22
|
</p>
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
<% theme = CommandTower::EmailTheme::Resolver.resolve -%>
|
|
2
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: <%= theme[:canvas_background] %>; padding: 40px 20px;">
|
|
2
3
|
<tr>
|
|
3
4
|
<td align="center">
|
|
4
|
-
<table width="600" cellpadding="0" cellspacing="0" border="0" style="background-color:
|
|
5
|
+
<table width="600" cellpadding="0" cellspacing="0" border="0" style="background-color: <%= theme[:surface_background] %>; border-radius: 12px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); overflow: hidden;">
|
|
5
6
|
<!-- Header -->
|
|
6
7
|
<tr>
|
|
7
|
-
<td style="background:
|
|
8
|
-
<h1 style="margin: 0; color:
|
|
8
|
+
<td style="background: <%= theme[:accent] %>; padding: 40px 30px; text-align: center;">
|
|
9
|
+
<h1 style="margin: 0; color: <%= theme[:text_on_accent] %>; font-size: 28px; font-weight: 600; letter-spacing: -0.5px;">
|
|
9
10
|
Reset Your Password
|
|
10
11
|
</h1>
|
|
11
12
|
</td>
|
|
@@ -14,22 +15,22 @@
|
|
|
14
15
|
<!-- Content -->
|
|
15
16
|
<tr>
|
|
16
17
|
<td style="padding: 40px 30px;">
|
|
17
|
-
<p style="margin: 0 0 20px 0; color:
|
|
18
|
-
Hello <strong style="color:
|
|
18
|
+
<p style="margin: 0 0 20px 0; color: <%= theme[:body_text] %>; font-size: 16px; line-height: 1.6;">
|
|
19
|
+
Hello <strong style="color: <%= theme[:primary_text] %>;"><%= @user.full_name %></strong>,
|
|
19
20
|
</p>
|
|
20
21
|
|
|
21
|
-
<p style="margin: 0 0 30px 0; color:
|
|
22
|
+
<p style="margin: 0 0 30px 0; color: <%= theme[:body_text] %>; font-size: 16px; line-height: 1.6;">
|
|
22
23
|
We received a request to reset your password for your <%= CommandTower.config.app.communication_name %> account. Click the button below to reset your password:
|
|
23
24
|
</p>
|
|
24
25
|
|
|
25
26
|
<!-- Reset Token Box -->
|
|
26
27
|
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin: 30px 0;">
|
|
27
28
|
<tr>
|
|
28
|
-
<td align="center" style="padding: 30px; background:
|
|
29
|
-
<div style="font-size: 18px; font-weight: 600; color:
|
|
29
|
+
<td align="center" style="padding: 30px; background-color: <%= theme[:canvas_background] %>; border-radius: 8px; border: 2px dashed <%= theme[:surface_border] %>;">
|
|
30
|
+
<div style="font-size: 18px; font-weight: 600; color: <%= theme[:accent] %>; font-family: 'Courier New', monospace; text-align: center; word-break: break-all; padding: 10px;">
|
|
30
31
|
<%= @token %>
|
|
31
32
|
</div>
|
|
32
|
-
<p style="margin: 15px 0 0 0; color:
|
|
33
|
+
<p style="margin: 15px 0 0 0; color: <%= theme[:muted_text] %>; font-size: 12px; text-transform: uppercase; letter-spacing: 1px;">
|
|
33
34
|
Reset Token
|
|
34
35
|
</p>
|
|
35
36
|
</td>
|
|
@@ -42,30 +43,30 @@
|
|
|
42
43
|
<td align="center" style="padding: 15px 0;">
|
|
43
44
|
<% reset_url = "#{CommandTower.config.app.composed_url}#{@reset_password_path}?token=#{@token}" %>
|
|
44
45
|
<% reset_url += "&email=#{CGI.escape(@email)}" if @require_email %>
|
|
45
|
-
<a href="<%= reset_url %>" style="display: inline-block; padding: 14px 32px; background:
|
|
46
|
+
<a href="<%= reset_url %>" style="display: inline-block; padding: 14px 32px; background-color: <%= theme[:primary_action] %>; color: <%= theme[:text_on_accent] %>; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 16px; text-align: center;">
|
|
46
47
|
Reset Password
|
|
47
48
|
</a>
|
|
48
49
|
</td>
|
|
49
50
|
</tr>
|
|
50
51
|
</table>
|
|
51
52
|
|
|
52
|
-
<p style="margin: 30px 0 20px 0; color:
|
|
53
|
-
<strong style="color:
|
|
53
|
+
<p style="margin: 30px 0 20px 0; color: <%= theme[:muted_text] %>; font-size: 14px; line-height: 1.6;">
|
|
54
|
+
<strong style="color: <%= theme[:body_text] %>;">Important:</strong> This link will expire in <%= CommandTower.config.login.plain_text.password_reset.token_valid_for.inspect %>. If you didn't request a password reset, please ignore this email. Your password will remain unchanged.
|
|
54
55
|
</p>
|
|
55
56
|
|
|
56
|
-
<p style="margin: 20px 0 0 0; color:
|
|
57
|
+
<p style="margin: 20px 0 0 0; color: <%= theme[:muted_text] %>; font-size: 14px; line-height: 1.6;">
|
|
57
58
|
If the button above doesn't work, copy and paste the reset token above into the password reset form on our website.
|
|
58
59
|
</p>
|
|
59
60
|
|
|
60
61
|
<!-- Alternative Reset URL Link -->
|
|
61
62
|
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin: 30px 0 0 0;">
|
|
62
63
|
<tr>
|
|
63
|
-
<td align="center" style="padding: 20px; background-color:
|
|
64
|
-
<p style="margin: 0 0 10px 0; color:
|
|
64
|
+
<td align="center" style="padding: 20px; background-color: <%= theme[:canvas_background] %>; border-radius: 8px; border: 1px solid <%= theme[:surface_border] %>;">
|
|
65
|
+
<p style="margin: 0 0 10px 0; color: <%= theme[:body_text] %>; font-size: 14px; font-weight: 600;">
|
|
65
66
|
Or visit the reset password page directly:
|
|
66
67
|
</p>
|
|
67
|
-
<p style="margin: 0; color:
|
|
68
|
-
<a href="<%= CommandTower.config.app.composed_url %><%= @reset_password_path %>" style="color:
|
|
68
|
+
<p style="margin: 0; color: <%= theme[:primary_action] %>; font-size: 14px; word-break: break-all;">
|
|
69
|
+
<a href="<%= CommandTower.config.app.composed_url %><%= @reset_password_path %>" style="color: <%= theme[:primary_action] %>; text-decoration: underline;">
|
|
69
70
|
<%= CommandTower.config.app.composed_url %><%= @reset_password_path %>
|
|
70
71
|
</a>
|
|
71
72
|
</p>
|
|
@@ -77,14 +78,14 @@
|
|
|
77
78
|
|
|
78
79
|
<!-- Footer -->
|
|
79
80
|
<tr>
|
|
80
|
-
<td style="padding: 30px; background-color:
|
|
81
|
-
<p style="margin: 0 0 15px 0; color:
|
|
81
|
+
<td style="padding: 30px; background-color: <%= theme[:canvas_background] %>; border-top: 1px solid <%= theme[:surface_border] %>;">
|
|
82
|
+
<p style="margin: 0 0 15px 0; color: <%= theme[:body_text] %>; font-size: 14px; text-align: center; line-height: 1.6;">
|
|
82
83
|
Best regards,<br>
|
|
83
|
-
<strong style="color:
|
|
84
|
+
<strong style="color: <%= theme[:accent] %>;"><%= CommandTower.config.app.communication_name %> Team</strong>
|
|
84
85
|
</p>
|
|
85
86
|
|
|
86
87
|
<p style="margin: 0; text-align: center;">
|
|
87
|
-
<a href="<%= CommandTower.config.app.composed_url %>" target="_blank" style="color:
|
|
88
|
+
<a href="<%= CommandTower.config.app.composed_url %>" target="_blank" style="color: <%= theme[:primary_action] %>; text-decoration: none; font-size: 14px; font-weight: 500;">
|
|
88
89
|
Visit <%= CommandTower.config.app.communication_name %>
|
|
89
90
|
</a>
|
|
90
91
|
</p>
|
|
@@ -96,7 +97,7 @@
|
|
|
96
97
|
<table width="600" cellpadding="0" cellspacing="0" border="0" style="margin-top: 20px;">
|
|
97
98
|
<tr>
|
|
98
99
|
<td style="text-align: center; padding: 20px 0;">
|
|
99
|
-
<p style="margin: 0; color:
|
|
100
|
+
<p style="margin: 0; color: <%= theme[:muted_text] %>; font-size: 12px; line-height: 1.5;">
|
|
100
101
|
This is an automated message. Please do not reply to this email.
|
|
101
102
|
</p>
|
|
102
103
|
</td>
|
|
@@ -5,6 +5,8 @@ module CommandTower
|
|
|
5
5
|
module Auth
|
|
6
6
|
module PlainText
|
|
7
7
|
class LoginWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
8
|
+
include CommandTower::Workflows::ClientCompatibility::RecommendationMeta
|
|
9
|
+
|
|
8
10
|
retry_strategy :none
|
|
9
11
|
|
|
10
12
|
def call(input:, request_context: nil)
|
|
@@ -24,6 +26,7 @@ module CommandTower
|
|
|
24
26
|
token_expires_at: data[:expires_at]
|
|
25
27
|
),
|
|
26
28
|
http_status: :created,
|
|
29
|
+
meta: client_compatibility_meta,
|
|
27
30
|
response_effects: {
|
|
28
31
|
set_token: { token: data[:token], expires_at: data[:expires_at] },
|
|
29
32
|
ensure_csrf_cookie: { rotate: csrf_rotate_on_login? }
|
|
@@ -5,6 +5,8 @@ module CommandTower
|
|
|
5
5
|
module Auth
|
|
6
6
|
module Session
|
|
7
7
|
class ShowWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
8
|
+
include CommandTower::Workflows::ClientCompatibility::RecommendationMeta
|
|
9
|
+
|
|
8
10
|
retry_strategy :none
|
|
9
11
|
|
|
10
12
|
def call(current_user:, auth_context:)
|
|
@@ -30,6 +32,7 @@ module CommandTower
|
|
|
30
32
|
actor: auth_context.actor_user
|
|
31
33
|
),
|
|
32
34
|
http_status: :ok,
|
|
35
|
+
meta: client_compatibility_meta,
|
|
33
36
|
response_effects: response_effects
|
|
34
37
|
)
|
|
35
38
|
end
|