belt 0.2.18 → 0.2.20
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/lib/belt/cli/auth_command.rb +150 -14
- data/lib/belt/cli/console_command.rb +17 -2
- data/lib/belt/cli/deploy_command.rb +9 -0
- data/lib/belt/cli/destroy_command.rb +3 -1
- data/lib/belt/cli/doctor_command.rb +166 -22
- data/lib/belt/cli/generate_command.rb +24 -31
- data/lib/belt/cli/index_command.rb +192 -0
- data/lib/belt/cli/lambda_config_command.rb +2 -1
- data/lib/belt/cli/routes_command/route_inference.rb +4 -3
- data/lib/belt/cli/routes_command.rb +0 -19
- data/lib/belt/cli/views_command.rb +3 -9
- data/lib/belt/cli.rb +1 -0
- data/lib/belt/inflector.rb +8 -0
- data/lib/belt/route_dsl.rb +3 -2
- data/lib/belt/version.rb +1 -1
- data/lib/templates/generate/auth/cognito.tf.erb +11 -0
- data/lib/templates/generate/auth/frontend/ConfirmEmail.jsx +55 -0
- data/lib/templates/generate/auth/frontend/Login.jsx +79 -0
- data/lib/templates/generate/auth/frontend/ProtectedRoute.jsx +9 -0
- data/lib/templates/generate/auth/frontend/SignUp.jsx +58 -0
- data/lib/templates/generate/auth/frontend/apiClient.js +34 -0
- data/lib/templates/generate/auth/frontend/auth.css +157 -0
- data/lib/templates/generate/auth/frontend/auth.js +80 -0
- data/lib/templates/generate/model.rb.erb +4 -6
- data/lib/templates/new_app/config/lambda/api.yml.erb +1 -0
- metadata +37 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 983c166381b409ee19c2a23c1439be35180892299af76570097d206dea1ba1a4
|
|
4
|
+
data.tar.gz: f7888dca2b10805bed099da525b968b90e511c1b8381990ba85ab80dc23a9263
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 253f76decf02f960c12011b95570b04213c33800eecfaf70415071f8a2792cae2480a71251cc0ec1b565b837b1c9229f6522c6452124f63560334c6cb6b838d4
|
|
7
|
+
data.tar.gz: 5393e1c8cdef68b50dee5bca41a299d02560ecc9e65718e8ed45e2cb09b5ffabaf74eefc11b47e59f35fd7b78b602b866db7028eae82246e0da76b14910eca53
|
|
@@ -19,9 +19,10 @@ module Belt
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
force = args.delete('--force') || args.delete('-f')
|
|
22
|
+
signup = args.delete('--signup')
|
|
22
23
|
pools = parse_pools(args)
|
|
23
24
|
|
|
24
|
-
new(pools: pools, force: force).generate
|
|
25
|
+
new(pools: pools, force: force, signup: signup).generate
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
def self.destroy(_args)
|
|
@@ -35,6 +36,7 @@ module Belt
|
|
|
35
36
|
Usage: belt generate auth [pool_names...] [options]
|
|
36
37
|
|
|
37
38
|
Options:
|
|
39
|
+
--signup Allow public user registration (generates frontend views)
|
|
38
40
|
--force, -f Overwrite existing cognito.tf (skip collision check)
|
|
39
41
|
|
|
40
42
|
Arguments:
|
|
@@ -42,26 +44,38 @@ module Belt
|
|
|
42
44
|
If omitted, generates a single pool named "main".
|
|
43
45
|
|
|
44
46
|
Examples:
|
|
45
|
-
belt g auth #
|
|
46
|
-
belt g auth
|
|
47
|
-
belt g auth web
|
|
48
|
-
belt g auth web
|
|
47
|
+
belt g auth # Admin-only, single pool
|
|
48
|
+
belt g auth --signup # Public signup with frontend views
|
|
49
|
+
belt g auth web # Named pool: "web"
|
|
50
|
+
belt g auth web mobile # Two pools
|
|
49
51
|
belt g auth --force # Overwrite existing
|
|
50
52
|
|
|
51
53
|
What this generates:
|
|
52
54
|
infrastructure/modules/app/cognito.tf User pool + client resources
|
|
53
55
|
infrastructure/modules/app/cognito_outputs.tf Pool ID, ARN, and client ID outputs
|
|
54
56
|
|
|
57
|
+
With --signup (when frontend/ exists):
|
|
58
|
+
frontend/src/lib/auth.js Auth module (signIn, signUp, etc.)
|
|
59
|
+
frontend/src/lib/apiClient.js API client with Authorization header
|
|
60
|
+
frontend/src/pages/auth/Login.jsx Login page
|
|
61
|
+
frontend/src/pages/auth/SignUp.jsx Registration page
|
|
62
|
+
frontend/src/pages/auth/ConfirmEmail.jsx Email verification page
|
|
63
|
+
frontend/src/components/ProtectedRoute.jsx Route guard component
|
|
64
|
+
|
|
65
|
+
Without --signup (admin-only, default):
|
|
66
|
+
frontend/src/lib/auth.js Auth module (signIn only)
|
|
67
|
+
frontend/src/lib/apiClient.js API client with Authorization header
|
|
68
|
+
frontend/src/pages/auth/Login.jsx Login page
|
|
69
|
+
frontend/src/components/ProtectedRoute.jsx Route guard component
|
|
70
|
+
|
|
55
71
|
It also patches:
|
|
56
72
|
infrastructure/modules/app/main.tf Adds cognito_user_pool_arns to conveyor_belt
|
|
57
73
|
|
|
58
74
|
After running:
|
|
59
75
|
1. Review the generated Cognito config in cognito.tf
|
|
60
|
-
2.
|
|
61
|
-
3. Run `belt
|
|
62
|
-
|
|
63
|
-
To remove:
|
|
64
|
-
belt destroy auth
|
|
76
|
+
2. Add auth: :cognito to your routes namespace
|
|
77
|
+
3. Run `belt deploy` to create the user pool
|
|
78
|
+
4. Create your account (admin-only): aws cognito-idp admin-create-user ...
|
|
65
79
|
HELP
|
|
66
80
|
end
|
|
67
81
|
|
|
@@ -72,9 +86,10 @@ module Belt
|
|
|
72
86
|
names.map(&:downcase).map { |n| n.gsub(/[^a-z0-9_]/, '_') }
|
|
73
87
|
end
|
|
74
88
|
|
|
75
|
-
def initialize(pools:, force: false)
|
|
89
|
+
def initialize(pools:, force: false, signup: false)
|
|
76
90
|
@pool_names = pools
|
|
77
91
|
@force = force
|
|
92
|
+
@signup = signup
|
|
78
93
|
@app_name = detect_app_name
|
|
79
94
|
@pools = build_pool_metadata
|
|
80
95
|
end
|
|
@@ -86,12 +101,48 @@ module Belt
|
|
|
86
101
|
write_cognito_tf
|
|
87
102
|
write_cognito_outputs_tf
|
|
88
103
|
patch_main_tf
|
|
104
|
+
patch_env_outputs
|
|
105
|
+
generate_frontend_auth if frontend?
|
|
89
106
|
|
|
90
107
|
puts "\n✓ Auth generated!"
|
|
108
|
+
print_next_steps
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def print_next_steps
|
|
91
112
|
puts "\nNext steps:"
|
|
92
|
-
puts ' 1.
|
|
93
|
-
puts '
|
|
94
|
-
puts '
|
|
113
|
+
puts ' 1. Add auth: :cognito to your routes namespace:'
|
|
114
|
+
puts ''
|
|
115
|
+
puts ' namespace :api, auth: :cognito do'
|
|
116
|
+
puts ' # your resources...'
|
|
117
|
+
puts ' end'
|
|
118
|
+
puts ''
|
|
119
|
+
if frontend?
|
|
120
|
+
puts ' 2. Wire auth into your frontend/src/App.jsx:'
|
|
121
|
+
puts ''
|
|
122
|
+
puts " import Login from './pages/auth/Login'"
|
|
123
|
+
puts " import ProtectedRoute from './components/ProtectedRoute'"
|
|
124
|
+
puts ''
|
|
125
|
+
puts ' // Add login route:'
|
|
126
|
+
puts ' <Route path="/login" element={<Login onLogin={() => window.location.href = \'/\'} />} />'
|
|
127
|
+
puts ''
|
|
128
|
+
puts ' // Wrap protected routes:'
|
|
129
|
+
puts ' <Route path="/*" element={<ProtectedRoute><YourApp /></ProtectedRoute>} />'
|
|
130
|
+
puts ''
|
|
131
|
+
puts ' 3. Deploy: belt deploy'
|
|
132
|
+
print_create_user_step(4) unless @signup
|
|
133
|
+
else
|
|
134
|
+
puts ' 2. Deploy: belt deploy'
|
|
135
|
+
print_create_user_step(3) unless @signup
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def print_create_user_step(step_num)
|
|
140
|
+
puts " #{step_num}. Create your account:"
|
|
141
|
+
puts ' aws cognito-idp admin-create-user \\'
|
|
142
|
+
puts ' --user-pool-id <pool-id-from-terraform-output> \\'
|
|
143
|
+
puts ' --username your@email.com \\'
|
|
144
|
+
puts ' --temporary-password TempPass123 \\'
|
|
145
|
+
puts ' --message-action SUPPRESS'
|
|
95
146
|
end
|
|
96
147
|
|
|
97
148
|
def remove
|
|
@@ -134,6 +185,55 @@ module Belt
|
|
|
134
185
|
end
|
|
135
186
|
end
|
|
136
187
|
|
|
188
|
+
def frontend?
|
|
189
|
+
Dir.exist?('frontend/src')
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def generate_frontend_auth
|
|
193
|
+
frontend_template_dir = File.join(TEMPLATE_DIR, 'frontend')
|
|
194
|
+
|
|
195
|
+
# Generate auth lib files
|
|
196
|
+
lib_dir = 'frontend/src/lib'
|
|
197
|
+
FileUtils.mkdir_p(lib_dir)
|
|
198
|
+
copy_frontend_file(frontend_template_dir, 'auth.js', File.join(lib_dir, 'auth.js'))
|
|
199
|
+
copy_frontend_file(frontend_template_dir, 'apiClient.js', File.join(lib_dir, 'apiClient.js'))
|
|
200
|
+
|
|
201
|
+
pages_dir = 'frontend/src/pages/auth'
|
|
202
|
+
FileUtils.mkdir_p(pages_dir)
|
|
203
|
+
copy_frontend_file(frontend_template_dir, 'Login.jsx', File.join(pages_dir, 'Login.jsx'))
|
|
204
|
+
copy_frontend_file(frontend_template_dir, 'auth.css', File.join(pages_dir, 'auth.css'))
|
|
205
|
+
if @signup
|
|
206
|
+
# Generate auth pages
|
|
207
|
+
copy_frontend_file(frontend_template_dir, 'SignUp.jsx', File.join(pages_dir, 'SignUp.jsx'))
|
|
208
|
+
copy_frontend_file(frontend_template_dir, 'ConfirmEmail.jsx', File.join(pages_dir, 'ConfirmEmail.jsx'))
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Generate ProtectedRoute component
|
|
212
|
+
components_dir = 'frontend/src/components'
|
|
213
|
+
FileUtils.mkdir_p(components_dir)
|
|
214
|
+
copy_frontend_file(frontend_template_dir, 'ProtectedRoute.jsx',
|
|
215
|
+
File.join(components_dir, 'ProtectedRoute.jsx'))
|
|
216
|
+
|
|
217
|
+
install_cognito_sdk
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def copy_frontend_file(template_dir, filename, dest)
|
|
221
|
+
src = File.join(template_dir, filename)
|
|
222
|
+
FileUtils.cp(src, dest)
|
|
223
|
+
puts " create #{dest}"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def install_cognito_sdk
|
|
227
|
+
puts "\n Installing @aws-sdk/client-cognito-identity-provider..."
|
|
228
|
+
success = system('npm', 'install', '@aws-sdk/client-cognito-identity-provider',
|
|
229
|
+
'--prefix', 'frontend', '--no-fund', '--no-audit', '--silent')
|
|
230
|
+
if success
|
|
231
|
+
puts ' ✓ npm dependency installed'
|
|
232
|
+
else
|
|
233
|
+
puts ' ⚠ npm install failed — run: cd frontend && npm install @aws-sdk/client-cognito-identity-provider'
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
137
237
|
def check_collision!
|
|
138
238
|
cognito_tf = File.join(MODULE_DIR, 'cognito.tf')
|
|
139
239
|
return unless File.exist?(cognito_tf)
|
|
@@ -179,6 +279,42 @@ module Belt
|
|
|
179
279
|
insert_cognito_into_resource(content, main_tf, arn_value)
|
|
180
280
|
end
|
|
181
281
|
|
|
282
|
+
def patch_env_outputs
|
|
283
|
+
env_dirs = Dir.glob('infrastructure/*/outputs.tf')
|
|
284
|
+
.reject { |f| f.include?('modules') }
|
|
285
|
+
|
|
286
|
+
env_dirs.each do |outputs_file|
|
|
287
|
+
content = File.read(outputs_file)
|
|
288
|
+
|
|
289
|
+
next if content.include?('cognito_user_pool_id')
|
|
290
|
+
|
|
291
|
+
cognito_outputs = @pools.map do |pool|
|
|
292
|
+
suffix = pool[:suffix]
|
|
293
|
+
<<~HCL
|
|
294
|
+
|
|
295
|
+
output "cognito_user_pool_id#{suffix}" {
|
|
296
|
+
description = "Cognito User Pool ID#{pool[:label]}"
|
|
297
|
+
value = module.app.cognito_user_pool_id#{suffix}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
output "cognito_client_id#{suffix}" {
|
|
301
|
+
description = "Cognito User Pool Client ID#{pool[:label]}"
|
|
302
|
+
value = module.app.cognito_client_id#{suffix}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
output "cognito_region" {
|
|
306
|
+
description = "AWS region for Cognito"
|
|
307
|
+
value = var.aws_region
|
|
308
|
+
}
|
|
309
|
+
HCL
|
|
310
|
+
end.join
|
|
311
|
+
|
|
312
|
+
File.write(outputs_file, content + cognito_outputs)
|
|
313
|
+
env_name = File.basename(File.dirname(outputs_file))
|
|
314
|
+
puts " update #{outputs_file} (added cognito outputs for #{env_name})"
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
182
318
|
def build_arn_value(arns)
|
|
183
319
|
if arns.length == 1
|
|
184
320
|
"[#{arns.first}]"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'optparse'
|
|
4
|
+
require 'stringio'
|
|
4
5
|
require_relative 'environment_config'
|
|
5
6
|
|
|
6
7
|
module Belt
|
|
@@ -59,7 +60,10 @@ module Belt
|
|
|
59
60
|
boot_app
|
|
60
61
|
puts banner
|
|
61
62
|
ARGV.clear
|
|
62
|
-
|
|
63
|
+
suppress_warnings do
|
|
64
|
+
require 'irb'
|
|
65
|
+
require 'rdoc'
|
|
66
|
+
end
|
|
63
67
|
IRB.start
|
|
64
68
|
end
|
|
65
69
|
|
|
@@ -72,7 +76,7 @@ module Belt
|
|
|
72
76
|
end
|
|
73
77
|
|
|
74
78
|
def boot_app
|
|
75
|
-
require 'bundler/setup'
|
|
79
|
+
suppress_warnings { require 'bundler/setup' }
|
|
76
80
|
|
|
77
81
|
environment_file = File.join(Belt.root, 'lambda', 'config', 'environment.rb')
|
|
78
82
|
if File.exist?(environment_file)
|
|
@@ -132,6 +136,17 @@ module Belt
|
|
|
132
136
|
result.inspect
|
|
133
137
|
end
|
|
134
138
|
end
|
|
139
|
+
|
|
140
|
+
def suppress_warnings
|
|
141
|
+
original_verbose = $VERBOSE
|
|
142
|
+
$VERBOSE = nil
|
|
143
|
+
original_stderr = $stderr
|
|
144
|
+
$stderr = StringIO.new
|
|
145
|
+
yield
|
|
146
|
+
ensure
|
|
147
|
+
$stderr = original_stderr
|
|
148
|
+
$VERBOSE = original_verbose
|
|
149
|
+
end
|
|
135
150
|
end
|
|
136
151
|
end
|
|
137
152
|
end
|
|
@@ -145,6 +145,7 @@ module Belt
|
|
|
145
145
|
|
|
146
146
|
def run
|
|
147
147
|
validate!
|
|
148
|
+
run_preflight_checks!
|
|
148
149
|
env_dir = File.join(@infra_dir, @env)
|
|
149
150
|
|
|
150
151
|
load_and_apply_env_config!
|
|
@@ -263,6 +264,14 @@ module Belt
|
|
|
263
264
|
"Create it with: belt generate environment #{@env}"
|
|
264
265
|
end
|
|
265
266
|
|
|
267
|
+
def run_preflight_checks!
|
|
268
|
+
require_relative 'doctor_command'
|
|
269
|
+
doctor = DoctorCommand.new(preflight: true)
|
|
270
|
+
return if doctor.run
|
|
271
|
+
|
|
272
|
+
abort 'Deploy blocked. Fix the issues above, then try again.'
|
|
273
|
+
end
|
|
274
|
+
|
|
266
275
|
def validate_aws!
|
|
267
276
|
stdout, status = Open3.capture2('aws', 'sts', 'get-caller-identity', '--output', 'json')
|
|
268
277
|
unless status.success?
|
|
@@ -10,7 +10,7 @@ require_relative '../inflector'
|
|
|
10
10
|
module Belt
|
|
11
11
|
module CLI
|
|
12
12
|
class DestroyCommand
|
|
13
|
-
GENERATORS = %w[scaffold resource model controller environment frontend views auth].freeze
|
|
13
|
+
GENERATORS = %w[scaffold resource model controller environment frontend views index auth].freeze
|
|
14
14
|
|
|
15
15
|
include AppDetection
|
|
16
16
|
|
|
@@ -61,6 +61,8 @@ module Belt
|
|
|
61
61
|
exit 1
|
|
62
62
|
end
|
|
63
63
|
new(generator, name, args.map { |a| parse_field(a) }).destroy
|
|
64
|
+
when 'index'
|
|
65
|
+
Belt::CLI::IndexCommand.run(['remove'] + args)
|
|
64
66
|
else
|
|
65
67
|
name = args.shift
|
|
66
68
|
if name.nil? || name.empty?
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'open3'
|
|
5
|
+
require_relative '../../belt/inflector'
|
|
5
6
|
|
|
6
7
|
module Belt
|
|
7
8
|
module CLI
|
|
@@ -17,13 +18,14 @@ module Belt
|
|
|
17
18
|
|
|
18
19
|
def self.run(args)
|
|
19
20
|
verbose = args.include?('--verbose') || args.include?('-v')
|
|
21
|
+
preflight = args.include?('--preflight')
|
|
20
22
|
|
|
21
23
|
if args.include?('--help') || args.include?('-h')
|
|
22
24
|
puts usage
|
|
23
25
|
exit 0
|
|
24
26
|
end
|
|
25
27
|
|
|
26
|
-
new(verbose: verbose).run
|
|
28
|
+
new(verbose: verbose, preflight: preflight).run
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
def self.usage
|
|
@@ -33,24 +35,50 @@ module Belt
|
|
|
33
35
|
Check system dependencies and AWS configuration for Belt.
|
|
34
36
|
|
|
35
37
|
Options:
|
|
36
|
-
-v, --verbose
|
|
37
|
-
-
|
|
38
|
+
-v, --verbose Show detailed output for each check
|
|
39
|
+
--preflight Run only deploy-critical checks (indexes, credentials)
|
|
40
|
+
-h, --help Show this help
|
|
38
41
|
USAGE
|
|
39
42
|
end
|
|
40
43
|
|
|
41
|
-
def initialize(verbose: false)
|
|
44
|
+
def initialize(verbose: false, preflight: false)
|
|
42
45
|
@verbose = verbose
|
|
46
|
+
@preflight = preflight
|
|
43
47
|
@issues = []
|
|
44
48
|
@warnings = []
|
|
45
49
|
end
|
|
46
50
|
|
|
47
|
-
def run
|
|
51
|
+
def run # rubocop:disable Naming/PredicateMethod
|
|
52
|
+
if @preflight
|
|
53
|
+
run_preflight
|
|
54
|
+
else
|
|
55
|
+
run_full
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
@issues.empty?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Returns true if all preflight checks pass, false otherwise.
|
|
62
|
+
def run_preflight
|
|
63
|
+
check_aws_identity_quiet
|
|
64
|
+
check_table_indexes
|
|
65
|
+
check_cognito_auth
|
|
66
|
+
return if @issues.empty?
|
|
67
|
+
|
|
68
|
+
puts "\nPreflight checks failed:\n"
|
|
69
|
+
@issues.each { |i| puts " ✗ #{i}" }
|
|
70
|
+
puts ''
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def run_full
|
|
48
74
|
puts "Belt Doctor\n"
|
|
49
75
|
puts "Checking your system for Belt prerequisites...\n\n"
|
|
50
76
|
|
|
51
77
|
check_tools
|
|
52
78
|
check_aws_credentials
|
|
53
79
|
check_aws_identity
|
|
80
|
+
check_table_indexes
|
|
81
|
+
check_cognito_auth
|
|
54
82
|
|
|
55
83
|
puts ''
|
|
56
84
|
print_summary
|
|
@@ -132,28 +160,144 @@ module Belt
|
|
|
132
160
|
print_ok('Authentication', "account #{account}")
|
|
133
161
|
puts " ARN: #{arn}" if @verbose
|
|
134
162
|
else
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
163
|
+
handle_aws_identity_error(output.strip)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def check_aws_identity_quiet
|
|
168
|
+
_, status = Open3.capture2e('aws', 'sts', 'get-caller-identity')
|
|
169
|
+
return if status.success?
|
|
170
|
+
|
|
171
|
+
@issues << 'AWS credentials invalid or expired — run `aws sso login`'
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def handle_aws_identity_error(error)
|
|
175
|
+
if error.include?('ExpiredToken') || error.include?('expired')
|
|
176
|
+
print_fail('Authentication', 'credentials expired')
|
|
177
|
+
puts ' Run: aws sso login'
|
|
178
|
+
@issues << 'AWS credentials are expired — run `aws sso login`'
|
|
179
|
+
elsif error.include?('InvalidClientTokenId') || error.include?('SignatureDoesNotMatch')
|
|
180
|
+
print_fail('Authentication', 'invalid credentials')
|
|
181
|
+
puts ' Your access key or secret is incorrect.'
|
|
182
|
+
puts ' Run: aws configure'
|
|
183
|
+
@issues << 'AWS credentials are invalid'
|
|
184
|
+
elsif error.include?('Could not connect') || error.include?('Unable to locate credentials')
|
|
185
|
+
print_fail('Authentication', 'unable to authenticate')
|
|
186
|
+
puts ' Run: aws configure sso # or set AWS_PROFILE'
|
|
187
|
+
@issues << 'Unable to authenticate with AWS'
|
|
188
|
+
else
|
|
189
|
+
print_fail('Authentication', 'failed')
|
|
190
|
+
puts " #{error.lines.first&.strip}" if error.length.positive?
|
|
191
|
+
@issues << 'AWS authentication failed'
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def check_table_indexes
|
|
196
|
+
return unless Belt.root?
|
|
197
|
+
|
|
198
|
+
models_dir = File.join(Belt.root, 'lambda/models')
|
|
199
|
+
dynamodb_tf = File.join(Belt.root, 'infrastructure/modules/app/dynamodb.tf')
|
|
200
|
+
|
|
201
|
+
return unless Dir.exist?(models_dir)
|
|
202
|
+
|
|
203
|
+
puts ''
|
|
204
|
+
puts '── Tables & Indexes ──'
|
|
205
|
+
|
|
206
|
+
unless File.exist?(dynamodb_tf)
|
|
207
|
+
print_warn('dynamodb.tf', 'not found — run `belt setup tables` to generate')
|
|
208
|
+
@warnings << 'dynamodb.tf not generated — run `belt setup tables`'
|
|
209
|
+
return
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
tf_content = File.read(dynamodb_tf)
|
|
213
|
+
model_files = Dir.glob(File.join(models_dir, '*.rb'))
|
|
214
|
+
.reject { |f| File.basename(f) == 'application_record.rb' }
|
|
215
|
+
|
|
216
|
+
model_files.each do |file|
|
|
217
|
+
check_model_indexes(file, tf_content)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def check_model_indexes(file, tf_content)
|
|
222
|
+
content = File.read(file)
|
|
223
|
+
class_match = content.match(/^class\s+(\w+)\s*<\s*ApplicationRecord/)
|
|
224
|
+
return unless class_match
|
|
225
|
+
|
|
226
|
+
model_name = Belt::Inflector.underscore(class_match[1])
|
|
227
|
+
table_name = Belt::Inflector.pluralize(model_name)
|
|
228
|
+
|
|
229
|
+
# Check if table exists in dynamodb.tf
|
|
230
|
+
unless tf_content.include?("resource \"aws_dynamodb_table\" \"#{table_name}\"")
|
|
231
|
+
print_fail(table_name, 'table not in dynamodb.tf')
|
|
232
|
+
@issues << "Table '#{table_name}' missing from dynamodb.tf — run `belt setup tables`"
|
|
233
|
+
return
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Extract expected indexes from belongs_to
|
|
237
|
+
expected_indexes = extract_expected_indexes(content)
|
|
238
|
+
return if expected_indexes.empty?
|
|
239
|
+
|
|
240
|
+
# Check each expected index exists in the TF file
|
|
241
|
+
# Scope check to this table's resource block
|
|
242
|
+
table_block = tf_content[/resource "aws_dynamodb_table" "#{table_name}" \{.*?^\}/m]
|
|
243
|
+
return unless table_block
|
|
244
|
+
|
|
245
|
+
missing = []
|
|
246
|
+
expected_indexes.each do |idx|
|
|
247
|
+
if table_block.include?("name = \"#{idx[:name]}\"")
|
|
248
|
+
print_ok(table_name, idx[:name]) if @verbose
|
|
149
249
|
else
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
250
|
+
missing << idx
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
if missing.empty?
|
|
255
|
+
print_ok(table_name, "#{expected_indexes.size} index#{'es' if expected_indexes.size > 1} configured")
|
|
256
|
+
else
|
|
257
|
+
missing.each do |idx|
|
|
258
|
+
print_fail(table_name, "missing #{idx[:name]} (required by belongs_to :#{idx[:association]})")
|
|
259
|
+
@issues << "Table '#{table_name}' missing index '#{idx[:name]}' " \
|
|
260
|
+
'— run `belt setup tables` then `belt deploy`'
|
|
153
261
|
end
|
|
154
262
|
end
|
|
155
263
|
end
|
|
156
264
|
|
|
265
|
+
def extract_expected_indexes(content)
|
|
266
|
+
indexes = []
|
|
267
|
+
content.lines.reject { |line| line.strip.start_with?('#') }.join
|
|
268
|
+
.scan(/belongs_to\s+:(\w+)/) do |match|
|
|
269
|
+
association_name = match[0]
|
|
270
|
+
indexes << {
|
|
271
|
+
name: "#{Belt::Inflector.classify(association_name)}Index",
|
|
272
|
+
association: association_name
|
|
273
|
+
}
|
|
274
|
+
end
|
|
275
|
+
indexes
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def check_cognito_auth
|
|
279
|
+
return unless Belt.root?
|
|
280
|
+
|
|
281
|
+
routes_file = Belt.routes_file
|
|
282
|
+
return unless routes_file
|
|
283
|
+
|
|
284
|
+
content = File.read(routes_file)
|
|
285
|
+
|
|
286
|
+
# Check if any routes use auth: :cognito
|
|
287
|
+
uses_cognito = content.include?('auth: :cognito')
|
|
288
|
+
return unless uses_cognito
|
|
289
|
+
|
|
290
|
+
# Check if cognito.tf exists
|
|
291
|
+
cognito_tf = File.join(Belt.root, 'infrastructure/modules/app/cognito.tf')
|
|
292
|
+
return if File.exist?(cognito_tf)
|
|
293
|
+
|
|
294
|
+
puts ''
|
|
295
|
+
puts '── Authentication ──'
|
|
296
|
+
print_warn('Cognito', 'routes use auth: :cognito but no cognito.tf found')
|
|
297
|
+
puts ' Run: belt generate auth'
|
|
298
|
+
@warnings << 'Routes use auth: :cognito but no Cognito pool is configured — run `belt generate auth`'
|
|
299
|
+
end
|
|
300
|
+
|
|
157
301
|
def print_summary
|
|
158
302
|
if @issues.empty? && @warnings.empty?
|
|
159
303
|
puts '✓ All checks passed — you\'re ready to belt!'
|
|
@@ -15,7 +15,8 @@ module Belt
|
|
|
15
15
|
module CLI
|
|
16
16
|
class GenerateCommand
|
|
17
17
|
TEMPLATE_DIR = File.expand_path('../../templates/generate', __dir__)
|
|
18
|
-
|
|
18
|
+
|
|
19
|
+
GENERATORS = %w[scaffold resource model controller environment frontend views index auth].freeze
|
|
19
20
|
|
|
20
21
|
include AppDetection
|
|
21
22
|
|
|
@@ -127,6 +128,8 @@ module Belt
|
|
|
127
128
|
|
|
128
129
|
return Belt::CLI::ViewsCommand.run(args) if generator == 'views'
|
|
129
130
|
|
|
131
|
+
return Belt::CLI::IndexCommand.run(['add'] + args) if generator == 'index'
|
|
132
|
+
|
|
130
133
|
return Belt::CLI::AuthCommand.run(args) if generator == 'auth'
|
|
131
134
|
|
|
132
135
|
name = args.shift
|
|
@@ -290,7 +293,9 @@ module Belt
|
|
|
290
293
|
when 'model'
|
|
291
294
|
check_model_collision! unless @force
|
|
292
295
|
generate_model_standalone
|
|
293
|
-
when 'controller'
|
|
296
|
+
when 'controller'
|
|
297
|
+
generate_controller
|
|
298
|
+
inject_routes
|
|
294
299
|
end
|
|
295
300
|
end
|
|
296
301
|
|
|
@@ -351,18 +356,6 @@ module Belt
|
|
|
351
356
|
inject_parent_associations
|
|
352
357
|
sync_tables
|
|
353
358
|
generate_views_if_frontend
|
|
354
|
-
puts "\n✓ Scaffold '#{@singular_name}' generated!"
|
|
355
|
-
puts "\nFiles created/updated:"
|
|
356
|
-
puts " lambda/models/#{@singular_name}.rb"
|
|
357
|
-
puts " lambda/controllers/#{@app_name}/#{@resource_name}_controller.rb"
|
|
358
|
-
puts " #{find_routes_file_path || 'config/routes.rb'} (updated)"
|
|
359
|
-
puts " #{find_contracts_file_path || 'config/contracts.rb'} (updated)"
|
|
360
|
-
puts " lambda/lib/routes/#{@app_name}_routes.rb (updated)"
|
|
361
|
-
puts " frontend/src/pages/#{@resource_name}/ (views)" if Dir.exist?('frontend/src')
|
|
362
|
-
@references.each do |ref|
|
|
363
|
-
parent_model_path = "lambda/models/#{ref[:referenced_model]}.rb"
|
|
364
|
-
puts " #{parent_model_path} (updated — added has_many)" if File.exist?(parent_model_path)
|
|
365
|
-
end
|
|
366
359
|
end
|
|
367
360
|
|
|
368
361
|
def generate_model_standalone
|
|
@@ -386,25 +379,26 @@ module Belt
|
|
|
386
379
|
|
|
387
380
|
def inject_routes
|
|
388
381
|
routes_file = find_routes_file_path
|
|
389
|
-
return unless routes_file && File.exist?(routes_file)
|
|
390
382
|
|
|
391
|
-
|
|
392
|
-
|
|
383
|
+
if routes_file && File.exist?(routes_file)
|
|
384
|
+
content = File.read(routes_file)
|
|
385
|
+
tables_arg = @fields.any? ? ", tables: [:#{@resource_name}]" : ''
|
|
393
386
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
387
|
+
# If this resource has a reference to a parent that already has routes, nest it
|
|
388
|
+
parent_ref = @references.find do |ref|
|
|
389
|
+
parent_resource = Belt::Inflector.pluralize(ref[:referenced_model])
|
|
390
|
+
content.match?(/resources :#{Regexp.escape(parent_resource)}\b/)
|
|
391
|
+
end
|
|
399
392
|
|
|
400
|
-
|
|
401
|
-
|
|
393
|
+
if parent_ref
|
|
394
|
+
inject_nested_route(content, routes_file, parent_ref, tables_arg)
|
|
395
|
+
else
|
|
396
|
+
inject_top_level_route(content, routes_file, tables_arg)
|
|
397
|
+
end
|
|
402
398
|
else
|
|
403
|
-
|
|
399
|
+
# Legacy projects without config/routes.rb — update the manifest directly
|
|
400
|
+
inject_route_manifest
|
|
404
401
|
end
|
|
405
|
-
|
|
406
|
-
# Also update route manifest
|
|
407
|
-
inject_route_manifest
|
|
408
402
|
end
|
|
409
403
|
|
|
410
404
|
def inject_nested_route(content, routes_file, parent_ref, tables_arg)
|
|
@@ -596,8 +590,7 @@ module Belt
|
|
|
596
590
|
next unless File.exist?(parent_model_path)
|
|
597
591
|
|
|
598
592
|
content = File.read(parent_model_path)
|
|
599
|
-
has_many_line = " has_many :#{@resource_name}
|
|
600
|
-
"index: '#{Belt::Inflector.classify(ref[:referenced_model])}Index'"
|
|
593
|
+
has_many_line = " has_many :#{@resource_name}"
|
|
601
594
|
|
|
602
595
|
next if content.include?("has_many :#{@resource_name}")
|
|
603
596
|
|
|
@@ -632,7 +625,7 @@ module Belt
|
|
|
632
625
|
return unless Dir.exist?('frontend/src')
|
|
633
626
|
return if @skip_views
|
|
634
627
|
|
|
635
|
-
Belt::CLI::ViewsCommand.new(@name, @fields, force: @force).generate
|
|
628
|
+
Belt::CLI::ViewsCommand.new(@name, @fields, force: @force, quiet: true).generate
|
|
636
629
|
end
|
|
637
630
|
end
|
|
638
631
|
end
|