belt 0.3.15 → 0.3.17

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 491ae37769b6a21cfd1823301d425d0af0b3f1e3c19952b909febebc7f2c3b32
4
- data.tar.gz: a2427f68a34d38be0cf3af42ac33a8d3c3d190f36505c54ef531219cd0c3fb66
3
+ metadata.gz: d0f68a846160c9f0b0c3ecd69a74a3bf500531098cd29bf77b6a21cea22ece7a
4
+ data.tar.gz: 55278ed99e4a8c886e55d12e83ed313aad88ff2953a3258b870e818c22a22a91
5
5
  SHA512:
6
- metadata.gz: 895fb4f7238141ba4752cdfc9200c44b2ef870bbc61585a436d3a9e36f39c689e8429d97ad4cf0101d92cbea06b3771864c1d31e4892dae54448a2f0e8ec138b
7
- data.tar.gz: 4b2fee884c2f7ad9ba78bd7a46dacf75d1d5c8aa5da43d151ab7d03889aa740dd60a12a1fce5c18467f7c3bbb54aacb886a55efdcc58a997ed6367148ea37f15
6
+ metadata.gz: f22fef0b437a5e67f680ae5fc0a72ed9bc286006d635e3bddc68d886722cbdae8040953a8248972c2dbcd545d1eefcef3ce83dfb25c4caae6919afa2e1fafdc0
7
+ data.tar.gz: c3e96290df0527f0b6da7c07469d8672c1dd41fcde7832cd2edb0997665b60f5ed69bd253f4189d23a24f28c910dc152d1572a7916aed0bca5a6eb5aea3bcb8c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.16
4
+
5
+ ### Enhancement
6
+
7
+ - **`belt generate auth --ses-email` for custom email domains**: By default,
8
+ Cognito sends verification emails from `no-reply@verificationemail.com`.
9
+ The new `--ses-email` flag configures Cognito to use Amazon SES instead,
10
+ allowing you to send from your own domain (e.g., `noreply@yourapp.com`).
11
+
12
+ The generator creates:
13
+ - `cognito_variables.tf` with variables for SES email ARN, from address, and
14
+ optional reply-to address
15
+ - `email_configuration` block in `cognito.tf` with `DEVELOPER` sending mode
16
+ - Example variable values in environment tfvars files
17
+
18
+ Prerequisites:
19
+ 1. Verify your domain or email address in Amazon SES
20
+ 2. Move out of SES sandbox for production use
21
+
22
+ Usage: `belt g auth --ses-email`
23
+
3
24
  ## 0.3.15
4
25
 
5
26
  ### Bug Fix
@@ -21,10 +21,11 @@ module Belt
21
21
 
22
22
  force = args.delete('--force') || args.delete('-f')
23
23
  signup = args.delete('--signup')
24
+ ses_email = args.delete('--ses-email')
24
25
  frontend_name = FrontendRegistry.extract_flag!(args, '--frontend')
25
26
  pools = parse_pools(args)
26
27
 
27
- new(pools: pools, force: force, signup: signup, frontend_name: frontend_name).generate
28
+ new(pools: pools, force: force, signup: signup, ses_email: ses_email, frontend_name: frontend_name).generate
28
29
  end
29
30
 
30
31
  def self.destroy(_args)
@@ -39,6 +40,7 @@ module Belt
39
40
 
40
41
  Options:
41
42
  --signup Allow public user registration (generates frontend views)
43
+ --ses-email Use Amazon SES for emails (custom from address/domain)
42
44
  --frontend NAME Target frontend when several exist
43
45
  --force, -f Overwrite existing cognito.tf (skip collision check)
44
46
 
@@ -49,6 +51,7 @@ module Belt
49
51
  Examples:
50
52
  belt g auth # Admin-only, single pool
51
53
  belt g auth --signup # Public signup with frontend views
54
+ belt g auth --ses-email # Custom email domain via SES
52
55
  belt g auth web # Named pool: "web"
53
56
  belt g auth web mobile # Two pools
54
57
  belt g auth --force # Overwrite existing
@@ -71,6 +74,19 @@ module Belt
71
74
  frontend/src/pages/auth/Login.jsx Login page
72
75
  frontend/src/components/ProtectedRoute.jsx Route guard component
73
76
 
77
+ With --ses-email:
78
+ Uses Amazon SES instead of Cognito's default email service. Allows sending
79
+ from your custom domain (e.g., noreply@yourapp.com) instead of verificationemail.com.
80
+
81
+ Prerequisites:
82
+ 1. Verify your domain or email address in Amazon SES
83
+ 2. Request production access (move out of SES sandbox) for production use
84
+
85
+ After generation, set these variables in your tfvars:
86
+ cognito_ses_email_arn = "arn:aws:ses:us-east-1:123456789:identity/yourapp.com"
87
+ cognito_from_email = "noreply@yourapp.com"
88
+ cognito_reply_to_email = "support@yourapp.com" # optional
89
+
74
90
  It also patches:
75
91
  infrastructure/modules/app/main.tf Adds cognito_user_pool_arns to conveyor_belt
76
92
 
@@ -89,10 +105,12 @@ module Belt
89
105
  names.map(&:downcase).map { |n| n.gsub(/[^a-z0-9_]/, '_') }
90
106
  end
91
107
 
92
- def initialize(pools:, force: false, signup: false, frontend_name: nil, skip_frontend_resolve: false)
108
+ def initialize(pools:, force: false, signup: false, ses_email: false, frontend_name: nil,
109
+ skip_frontend_resolve: false)
93
110
  @pool_names = pools
94
111
  @force = force
95
112
  @signup = signup
113
+ @ses_email = ses_email
96
114
  @app_name = detect_app_name
97
115
  @pools = build_pool_metadata
98
116
  @frontend = skip_frontend_resolve ? nil : resolve_frontend(frontend_name)
@@ -104,6 +122,7 @@ module Belt
104
122
 
105
123
  write_cognito_tf
106
124
  write_cognito_outputs_tf
125
+ write_cognito_variables_tf if @ses_email
107
126
  patch_main_tf
108
127
  patch_env_outputs
109
128
  generate_frontend_auth if frontend?
@@ -114,14 +133,29 @@ module Belt
114
133
 
115
134
  def print_next_steps
116
135
  puts "\nNext steps:"
117
- puts ' 1. Add auth: :cognito to your routes namespace:'
136
+ step = 1
137
+
138
+ if @ses_email
139
+ puts " #{step}. Configure SES email variables in your tfvars files:"
140
+ puts ''
141
+ puts ' # Verify your domain in SES first, then add:'
142
+ puts ' cognito_ses_email_arn = "arn:aws:ses:REGION:ACCOUNT:identity/yourdomain.com"'
143
+ puts ' cognito_from_email = "noreply@yourdomain.com"'
144
+ puts ' cognito_reply_to_email = "support@yourdomain.com" # optional'
145
+ puts ''
146
+ step += 1
147
+ end
148
+
149
+ puts " #{step}. Add auth: :cognito to your routes namespace:"
118
150
  puts ''
119
151
  puts ' namespace :api, auth: :cognito do'
120
152
  puts ' # your resources...'
121
153
  puts ' end'
122
154
  puts ''
155
+ step += 1
156
+
123
157
  if frontend?
124
- puts " 2. Wire auth into your #{@frontend.src_dir}/App.jsx:"
158
+ puts " #{step}. Wire auth into your #{@frontend.src_dir}/App.jsx:"
125
159
  puts ''
126
160
  puts " import Login from './pages/auth/Login'"
127
161
  puts " import ProtectedRoute from './components/ProtectedRoute'"
@@ -132,12 +166,11 @@ module Belt
132
166
  puts ' // Wrap protected routes:'
133
167
  puts ' <Route path="/*" element={<ProtectedRoute><YourApp /></ProtectedRoute>} />'
134
168
  puts ''
135
- puts ' 3. Deploy: belt deploy'
136
- print_create_user_step(4) unless @signup
137
- else
138
- puts ' 2. Deploy: belt deploy'
139
- print_create_user_step(3) unless @signup
169
+ step += 1
140
170
  end
171
+ puts " #{step}. Deploy: belt deploy"
172
+ step += 1
173
+ print_create_user_step(step) unless @signup
141
174
  end
142
175
 
143
176
  def print_create_user_step(step_num)
@@ -154,6 +187,7 @@ module Belt
154
187
 
155
188
  cognito_tf = File.join(MODULE_DIR, 'cognito.tf')
156
189
  cognito_outputs_tf = File.join(MODULE_DIR, 'cognito_outputs.tf')
190
+ cognito_variables_tf = File.join(MODULE_DIR, 'cognito_variables.tf')
157
191
 
158
192
  if File.exist?(cognito_tf)
159
193
  FileUtils.rm(cognito_tf)
@@ -167,6 +201,12 @@ module Belt
167
201
  puts " remove #{cognito_outputs_tf}"
168
202
  end
169
203
 
204
+ if File.exist?(cognito_variables_tf)
205
+ FileUtils.rm(cognito_variables_tf)
206
+ removed << cognito_variables_tf
207
+ puts " remove #{cognito_variables_tf}"
208
+ end
209
+
170
210
  unpatch_main_tf
171
211
  removed << File.join(MODULE_DIR, 'main.tf') if @main_tf_patched
172
212
 
@@ -275,6 +315,92 @@ module Belt
275
315
  puts " create #{dest}"
276
316
  end
277
317
 
318
+ def write_cognito_variables_tf
319
+ dest = File.join(MODULE_DIR, 'cognito_variables.tf')
320
+ content = <<~HCL
321
+ # Cognito SES email configuration variables
322
+ # Generated by: belt generate auth --ses-email
323
+ #
324
+ # Set these in your environment tfvars (e.g., infrastructure/dev/terraform.tfvars):
325
+ # cognito_ses_email_arn = "arn:aws:ses:us-east-1:123456789:identity/yourapp.com"
326
+ # cognito_from_email = "noreply@yourapp.com"
327
+ # cognito_reply_to_email = "support@yourapp.com" # optional
328
+
329
+ variable "cognito_ses_email_arn" {
330
+ description = "ARN of verified SES email address or domain for Cognito emails"
331
+ type = string
332
+ }
333
+
334
+ variable "cognito_from_email" {
335
+ description = "From email address for Cognito emails (e.g., noreply@yourapp.com)"
336
+ type = string
337
+ }
338
+
339
+ variable "cognito_reply_to_email" {
340
+ description = "Reply-to email address for Cognito emails (optional)"
341
+ type = string
342
+ default = ""
343
+ }
344
+ HCL
345
+
346
+ File.write(dest, content)
347
+ puts " create #{dest}"
348
+
349
+ # Also patch the environment tfvars examples
350
+ patch_env_vars_for_ses
351
+ end
352
+
353
+ def patch_env_vars_for_ses
354
+ # Find environment directories (e.g., infrastructure/dev/, infrastructure/prod/)
355
+ env_dirs = Dir.glob('infrastructure/*/terraform.tfvars')
356
+ .reject { |f| f.include?('modules') }
357
+
358
+ env_dirs.each do |tfvars_file|
359
+ content = File.read(tfvars_file)
360
+ next if content.include?('cognito_ses_email_arn')
361
+
362
+ domain = detect_domain_from_tfvars(tfvars_file)
363
+ ses_vars = build_ses_vars_block(domain)
364
+
365
+ File.write(tfvars_file, content + ses_vars)
366
+ env_name = File.basename(File.dirname(tfvars_file))
367
+ puts " update #{tfvars_file} (added SES email variables for #{env_name})"
368
+ end
369
+ end
370
+
371
+ def detect_domain_from_tfvars(tfvars_file)
372
+ return nil unless File.exist?(tfvars_file)
373
+
374
+ match = File.read(tfvars_file).match(/^\s*domain\s*=\s*"([^"]+)"/)
375
+ match[1] if match
376
+ end
377
+
378
+ def build_ses_vars_block(domain)
379
+ if domain
380
+ # User has a domain configured — provide smart defaults
381
+ <<~HCL
382
+
383
+ # Cognito SES email configuration (generated by: belt g auth --ses-email)
384
+ # Sends emails from your domain instead of verificationemail.com
385
+ # Prerequisites: 1) Verify domain in SES 2) Request SES production access for prod
386
+ cognito_ses_email_arn = "arn:aws:ses:REGION:ACCOUNT:identity/#{domain}"
387
+ cognito_from_email = "noreply@#{domain}"
388
+ # cognito_reply_to_email = "support@#{domain}"
389
+ HCL
390
+ else
391
+ # No domain configured — provide placeholder examples
392
+ <<~HCL
393
+
394
+ # Cognito SES email configuration (generated by: belt g auth --ses-email)
395
+ # Uncomment and configure to send emails from your domain instead of verificationemail.com
396
+ # Prerequisites: 1) Verify domain in SES 2) Request SES production access for prod
397
+ # cognito_ses_email_arn = "arn:aws:ses:REGION:ACCOUNT:identity/yourdomain.com"
398
+ # cognito_from_email = "noreply@yourdomain.com"
399
+ # cognito_reply_to_email = "support@yourdomain.com"
400
+ HCL
401
+ end
402
+ end
403
+
278
404
  def patch_main_tf
279
405
  main_tf = File.join(MODULE_DIR, 'main.tf')
280
406
  return unless File.exist?(main_tf)
@@ -135,7 +135,7 @@ module Belt
135
135
  HELP
136
136
  end
137
137
 
138
- # rubocop:disable Metrics/ParameterLists -- keyword options for destroy flags
138
+ # -- keyword options for destroy flags
139
139
  def initialize(generator, name, fields, force: false, skip_terraform: false, frontend_name: nil)
140
140
  @generator = generator
141
141
  @name = name&.downcase&.gsub(/[^a-z0-9_]/, '_')
@@ -150,7 +150,6 @@ module Belt
150
150
  @removed = []
151
151
  @updated = []
152
152
  end
153
- # rubocop:enable Metrics/ParameterLists
154
153
 
155
154
  def destroy
156
155
  case @generator
@@ -59,13 +59,11 @@ module Belt
59
59
  return if @quiet || !@announce
60
60
 
61
61
  puts "\n✓ Environment '#{@env_name}' created!"
62
- puts "\nNext steps:"
63
- puts " cd #{dest_dir}"
64
- puts ' terraform init'
65
- puts ' terraform plan'
66
- puts ' terraform apply'
67
- puts "\nTo configure a custom domain, set `domain` in #{dest_dir}/terraform.tfvars:"
68
- puts ' domain = "myapp.com"'
62
+ puts "\nReview your settings in #{dest_dir}/terraform.tfvars:"
63
+ puts ' environment = "..." # environment name (defaults to directory name)'
64
+ puts ' domain = "..." # your domain (e.g., "myapp.com")'
65
+ puts "\nThen deploy:"
66
+ puts " belt deploy #{@env_name}"
69
67
  end
70
68
 
71
69
  private
@@ -15,7 +15,7 @@ module Belt
15
15
  :url_output, :cloudfront_domain_output
16
16
  attr_accessor :default, :path
17
17
 
18
- # rubocop:disable Metrics/ParameterLists -- keyword options from YAML config
18
+ # -- keyword options from YAML config
19
19
  def initialize(name:, path:, dist: nil, bucket_output: nil, distribution_output: nil,
20
20
  url_output: nil, cloudfront_domain_output: nil, default: false)
21
21
  @name = name.to_s
@@ -28,7 +28,6 @@ module Belt
28
28
  @url_output = url_output || default_output('url')
29
29
  @cloudfront_domain_output = cloudfront_domain_output
30
30
  end
31
- # rubocop:enable Metrics/ParameterLists
32
31
 
33
32
  def default?
34
33
  @default
@@ -278,7 +278,7 @@ module Belt
278
278
  puts "\n#{info[:notes]}" if info[:notes]
279
279
  end
280
280
 
281
- # rubocop:disable Metrics/ParameterLists -- keyword options for generator flags
281
+ # -- keyword options for generator flags
282
282
  def initialize(generator, name, fields, skip_views: false, force: false, frontend_name: nil)
283
283
  @generator = generator
284
284
  @name = name.downcase.gsub(/[^a-z0-9_]/, '_')
@@ -293,7 +293,6 @@ module Belt
293
293
  @class_name = Belt::Inflector.classify(@singular_name)
294
294
  @references, @regular_fields = @fields.partition { |f| f[:type] == 'references' }
295
295
  end
296
- # rubocop:enable Metrics/ParameterLists
297
296
 
298
297
  def generate
299
298
  case @generator
@@ -82,7 +82,7 @@ module Belt
82
82
  ).generate
83
83
  end
84
84
 
85
- # rubocop:disable Metrics/ParameterLists -- keyword options for generator flags
85
+ # -- keyword options for generator flags
86
86
  def initialize(app_name, frontend: nil, bucket: nil, environments: nil, domain: nil, verbose: false)
87
87
  @app_name = app_name.gsub(/[^a-z0-9_-]/i, '_').downcase
88
88
  @module_name = @app_name.split(/[-_]/).map(&:capitalize).join
@@ -94,7 +94,6 @@ module Belt
94
94
  @resolved_bucket = nil
95
95
  @state_setup_succeeded = false
96
96
  end
97
- # rubocop:enable Metrics/ParameterLists
98
97
 
99
98
  def generate
100
99
  if Dir.exist?(@app_name)
data/lib/belt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Belt
4
- VERSION = '0.3.15'
4
+ VERSION = '0.3.17'
5
5
  end
@@ -30,6 +30,17 @@ resource "aws_cognito_user_pool" "<%= pool[:name] %>" {
30
30
  }
31
31
  }
32
32
 
33
+ <% if @ses_email -%>
34
+ # SES email configuration — sends from your verified domain
35
+ # Requires: verified domain or email in SES, out of sandbox for prod
36
+ email_configuration {
37
+ email_sending_account = "DEVELOPER"
38
+ source_arn = var.cognito_ses_email_arn
39
+ from_email_address = var.cognito_from_email
40
+ reply_to_email_address = var.cognito_reply_to_email != "" ? var.cognito_reply_to_email : null
41
+ }
42
+
43
+ <% end -%>
33
44
  schema {
34
45
  name = "email"
35
46
  attribute_data_type = "String"
@@ -35,9 +35,12 @@ resource "aws_acm_certificate" "app" {
35
35
  }
36
36
 
37
37
  # DNS validation record for the certificate
38
+ # Key by resource_record_name (not domain_name) because ACM uses the same
39
+ # validation CNAME for both base domain and wildcard — keying by domain_name
40
+ # creates duplicate records that Route 53 rejects.
38
41
  resource "aws_route53_record" "cert_validation" {
39
42
  for_each = local.dns_enabled ? {
40
- for dvo in aws_acm_certificate.app[0].domain_validation_options : dvo.domain_name => {
43
+ for dvo in aws_acm_certificate.app[0].domain_validation_options : dvo.resource_record_name => {
41
44
  name = dvo.resource_record_name
42
45
  type = dvo.resource_record_type
43
46
  record = dvo.resource_record_value
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: belt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.15
4
+ version: 0.3.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stowzilla