belt 0.2.18 → 0.2.19

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: b5e83d99ec1a6a02931418e3ade22cb51b291028884529e25abbd74f7c23a41f
4
- data.tar.gz: 938cc1a121a468de4dcb838c04fa44b7a01b78e5165a5da5f805cb330c88d59d
3
+ metadata.gz: 7f706cafa79246d178c2aee6bfb05536bcb5f03111a24f13624e769ef7d9916a
4
+ data.tar.gz: a99d09c6d472d4826acf78dee6a4fded952c8745071b63c6ea4f54dbfd6b0aef
5
5
  SHA512:
6
- metadata.gz: d581ff695fda0a3288f809d0c52805091dd9de73a69af5b21f4675a0f65f3d04431f72e18db3f099f6299a3a9f5b0045323218a60a54982d58312021c9bcedbd
7
- data.tar.gz: 59927af373dde4560edbaede9b9d8f81f43b229804479d0ff874ea67193b1a97bcf67e51afc388f66b3359fc7105df1ac0b2aef61a166d29fbcd43d509881004
6
+ metadata.gz: 2009deb03f2f725c4e960e73abf7745b8a9fdd5899ea76178b3942f21c504fe76338912ba57f05b913db3a18494f635c508e4696a3c347e40ddcb973e4c61541
7
+ data.tar.gz: b84417ce5814e266715b5dc54b4a965261f8011fee552f9f594c82137751565ff29b20f17444b2be986abfd79c02059b935ae5aa97fcbdbbfdcc79ffafa0421b
@@ -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 # Single pool: "main"
46
- belt g auth web # Single pool: "web"
47
- belt g auth web mobile # Two pools: "web" and "mobile"
48
- belt g auth web android ios # Three pools: "web", "android", "ios"
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. Customize password policy, MFA, triggers as needed
61
- 3. Run `belt apply <env>` to deploy
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,14 @@ module Belt
86
101
  write_cognito_tf
87
102
  write_cognito_outputs_tf
88
103
  patch_main_tf
104
+ generate_frontend_auth if frontend?
89
105
 
90
106
  puts "\n✓ Auth generated!"
91
107
  puts "\nNext steps:"
92
108
  puts ' 1. Review infrastructure/modules/app/cognito.tf'
93
109
  puts ' 2. Customize password policy, MFA, or Lambda triggers as needed'
94
110
  puts ' 3. Run `belt apply <env>` to deploy'
111
+ puts ' 4. Create your account: aws cognito-idp admin-create-user ...' unless @signup
95
112
  end
96
113
 
97
114
  def remove
@@ -134,6 +151,54 @@ module Belt
134
151
  end
135
152
  end
136
153
 
154
+ def frontend?
155
+ Dir.exist?('frontend/src')
156
+ end
157
+
158
+ def generate_frontend_auth
159
+ frontend_template_dir = File.join(TEMPLATE_DIR, 'frontend')
160
+
161
+ # Generate auth lib files
162
+ lib_dir = 'frontend/src/lib'
163
+ FileUtils.mkdir_p(lib_dir)
164
+ copy_frontend_file(frontend_template_dir, 'auth.js', File.join(lib_dir, 'auth.js'))
165
+ copy_frontend_file(frontend_template_dir, 'apiClient.js', File.join(lib_dir, 'apiClient.js'))
166
+
167
+ pages_dir = 'frontend/src/pages/auth'
168
+ FileUtils.mkdir_p(pages_dir)
169
+ copy_frontend_file(frontend_template_dir, 'Login.jsx', File.join(pages_dir, 'Login.jsx'))
170
+ if @signup
171
+ # Generate auth pages
172
+ copy_frontend_file(frontend_template_dir, 'SignUp.jsx', File.join(pages_dir, 'SignUp.jsx'))
173
+ copy_frontend_file(frontend_template_dir, 'ConfirmEmail.jsx', File.join(pages_dir, 'ConfirmEmail.jsx'))
174
+ end
175
+
176
+ # Generate ProtectedRoute component
177
+ components_dir = 'frontend/src/components'
178
+ FileUtils.mkdir_p(components_dir)
179
+ copy_frontend_file(frontend_template_dir, 'ProtectedRoute.jsx',
180
+ File.join(components_dir, 'ProtectedRoute.jsx'))
181
+
182
+ install_cognito_sdk
183
+ end
184
+
185
+ def copy_frontend_file(template_dir, filename, dest)
186
+ src = File.join(template_dir, filename)
187
+ FileUtils.cp(src, dest)
188
+ puts " create #{dest}"
189
+ end
190
+
191
+ def install_cognito_sdk
192
+ puts "\n Installing @aws-sdk/client-cognito-identity-provider..."
193
+ success = system('npm', 'install', '@aws-sdk/client-cognito-identity-provider',
194
+ '--prefix', 'frontend', '--no-fund', '--no-audit', '--silent')
195
+ if success
196
+ puts ' ✓ npm dependency installed'
197
+ else
198
+ puts ' ⚠ npm install failed — run: cd frontend && npm install @aws-sdk/client-cognito-identity-provider'
199
+ end
200
+ end
201
+
137
202
  def check_collision!
138
203
  cognito_tf = File.join(MODULE_DIR, 'cognito.tf')
139
204
  return unless File.exist?(cognito_tf)
@@ -156,7 +156,7 @@ module Belt
156
156
  def initialize(name, options = {})
157
157
  @name = name.to_s
158
158
  @routes = []
159
- @default_auth = options[:auth] || :cognito
159
+ @default_auth = options[:auth] || :none
160
160
  @default_lambda = options[:lambda] || name
161
161
  @default_cors = options.fetch(:cors, true)
162
162
  @default_tables = Array(options[:tables] || [])
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.2.18'
4
+ VERSION = '0.2.19'
5
5
  end
@@ -13,6 +13,14 @@ resource "aws_cognito_user_pool" "<%= pool[:name] %>" {
13
13
  require_uppercase = true
14
14
  }
15
15
 
16
+ # Admin-only signup — users cannot self-register.
17
+ # Remove this block to allow public sign-up.
18
+ <% unless @signup -%>
19
+ admin_create_user_config {
20
+ allow_admin_create_user_only = true
21
+ }
22
+
23
+ <% end -%>
16
24
  auto_verified_attributes = ["email"]
17
25
 
18
26
  account_recovery_setting {
@@ -43,6 +51,9 @@ resource "aws_cognito_user_pool_client" "<%= pool[:name] %>" {
43
51
 
44
52
  explicit_auth_flows = [
45
53
  "ALLOW_USER_SRP_AUTH",
54
+ <% if @signup -%>
55
+ "ALLOW_USER_PASSWORD_AUTH",
56
+ <% end -%>
46
57
  "ALLOW_REFRESH_TOKEN_AUTH"
47
58
  ]
48
59
 
@@ -0,0 +1,55 @@
1
+ import { useState } from 'react'
2
+ import { confirmSignUp } from '../lib/auth'
3
+
4
+ export default function ConfirmEmail() {
5
+ const params = new URLSearchParams(window.location.search)
6
+ const [email, setEmail] = useState(params.get('email') || '')
7
+ const [code, setCode] = useState('')
8
+ const [error, setError] = useState('')
9
+ const [loading, setLoading] = useState(false)
10
+ const [confirmed, setConfirmed] = useState(false)
11
+
12
+ async function handleSubmit(e) {
13
+ e.preventDefault()
14
+ setError('')
15
+ setLoading(true)
16
+
17
+ try {
18
+ await confirmSignUp(email, code)
19
+ setConfirmed(true)
20
+ } catch (err) {
21
+ setError(err.message || 'Confirmation failed')
22
+ } finally {
23
+ setLoading(false)
24
+ }
25
+ }
26
+
27
+ if (confirmed) {
28
+ return (
29
+ <div className="auth-page">
30
+ <div className="auth-form">
31
+ <h2>Email Verified! ✓</h2>
32
+ <p>Your account is ready.</p>
33
+ <a href="/login" className="auth-btn">Sign In</a>
34
+ </div>
35
+ </div>
36
+ )
37
+ }
38
+
39
+ return (
40
+ <div className="auth-page">
41
+ <form className="auth-form" onSubmit={handleSubmit}>
42
+ <h2>Verify Email</h2>
43
+ <p>Enter the code sent to your email.</p>
44
+ {error && <div className="auth-error">{error}</div>}
45
+ <input type="email" placeholder="Email" value={email}
46
+ onChange={e => setEmail(e.target.value)} required />
47
+ <input type="text" placeholder="Verification code" value={code}
48
+ onChange={e => setCode(e.target.value)} required autoFocus />
49
+ <button type="submit" disabled={loading}>
50
+ {loading ? 'Verifying...' : 'Verify'}
51
+ </button>
52
+ </form>
53
+ </div>
54
+ )
55
+ }
@@ -0,0 +1,81 @@
1
+ import { useState } from 'react'
2
+ import { signIn, completeNewPassword } from '../lib/auth'
3
+
4
+ export default function Login({ onLogin }) {
5
+ const [email, setEmail] = useState('')
6
+ const [password, setPassword] = useState('')
7
+ const [newPassword, setNewPassword] = useState('')
8
+ const [error, setError] = useState('')
9
+ const [challenge, setChallenge] = useState(null)
10
+ const [loading, setLoading] = useState(false)
11
+
12
+ async function handleSubmit(e) {
13
+ e.preventDefault()
14
+ setError('')
15
+ setLoading(true)
16
+
17
+ try {
18
+ const result = await signIn(email, password)
19
+ if (result.challenge === 'NEW_PASSWORD_REQUIRED') {
20
+ setChallenge(result)
21
+ } else {
22
+ onLogin()
23
+ }
24
+ } catch (err) {
25
+ setError(err.message || 'Sign in failed')
26
+ } finally {
27
+ setLoading(false)
28
+ }
29
+ }
30
+
31
+ async function handleNewPassword(e) {
32
+ e.preventDefault()
33
+ setError('')
34
+ setLoading(true)
35
+
36
+ try {
37
+ await completeNewPassword(email, newPassword, challenge.session)
38
+ onLogin()
39
+ } catch (err) {
40
+ setError(err.message || 'Password change failed')
41
+ } finally {
42
+ setLoading(false)
43
+ }
44
+ }
45
+
46
+ if (challenge) {
47
+ return (
48
+ <div className="auth-page">
49
+ <form className="auth-form" onSubmit={handleNewPassword}>
50
+ <h2>Set New Password</h2>
51
+ <p>Please set a permanent password for your account.</p>
52
+ {error && <div className="auth-error">{error}</div>}
53
+ <input type="password" placeholder="New password" value={newPassword}
54
+ onChange={e => setNewPassword(e.target.value)} required autoFocus />
55
+ <button type="submit" disabled={loading}>
56
+ {loading ? 'Saving...' : 'Set Password'}
57
+ </button>
58
+ </form>
59
+ </div>
60
+ )
61
+ }
62
+
63
+ return (
64
+ <div className="auth-page">
65
+ <form className="auth-form" onSubmit={handleSubmit}>
66
+ <h2>Sign In</h2>
67
+ {error && <div className="auth-error">{error}</div>}
68
+ <input type="email" placeholder="Email" value={email}
69
+ onChange={e => setEmail(e.target.value)} required autoFocus />
70
+ <input type="password" placeholder="Password" value={password}
71
+ onChange={e => setPassword(e.target.value)} required />
72
+ <button type="submit" disabled={loading}>
73
+ {loading ? 'Signing in...' : 'Sign In'}
74
+ </button>
75
+ <p className="auth-link">
76
+ Don't have an account? <a href="/signup">Sign up</a>
77
+ </p>
78
+ </form>
79
+ </div>
80
+ )
81
+ }
@@ -0,0 +1,9 @@
1
+ import { isAuthenticated } from '../lib/auth'
2
+ import { Navigate } from 'react-router-dom'
3
+
4
+ export default function ProtectedRoute({ children }) {
5
+ if (!isAuthenticated()) {
6
+ return <Navigate to="/login" replace />
7
+ }
8
+ return children
9
+ }
@@ -0,0 +1,58 @@
1
+ import { useState } from 'react'
2
+ import { signUp } from '../lib/auth'
3
+
4
+ export default function SignUp() {
5
+ const [email, setEmail] = useState('')
6
+ const [password, setPassword] = useState('')
7
+ const [error, setError] = useState('')
8
+ const [loading, setLoading] = useState(false)
9
+ const [needsConfirmation, setNeedsConfirmation] = useState(false)
10
+
11
+ async function handleSubmit(e) {
12
+ e.preventDefault()
13
+ setError('')
14
+ setLoading(true)
15
+
16
+ try {
17
+ const result = await signUp(email, password)
18
+ if (result.needsConfirmation) setNeedsConfirmation(true)
19
+ } catch (err) {
20
+ setError(err.message || 'Sign up failed')
21
+ } finally {
22
+ setLoading(false)
23
+ }
24
+ }
25
+
26
+ if (needsConfirmation) {
27
+ return (
28
+ <div className="auth-page">
29
+ <div className="auth-form">
30
+ <h2>Check Your Email</h2>
31
+ <p>We sent a verification code to <strong>{email}</strong>.</p>
32
+ <a href={`/confirm?email=${encodeURIComponent(email)}`} className="auth-btn">
33
+ Enter Code
34
+ </a>
35
+ </div>
36
+ </div>
37
+ )
38
+ }
39
+
40
+ return (
41
+ <div className="auth-page">
42
+ <form className="auth-form" onSubmit={handleSubmit}>
43
+ <h2>Create Account</h2>
44
+ {error && <div className="auth-error">{error}</div>}
45
+ <input type="email" placeholder="Email" value={email}
46
+ onChange={e => setEmail(e.target.value)} required autoFocus />
47
+ <input type="password" placeholder="Password (8+ characters)" value={password}
48
+ onChange={e => setPassword(e.target.value)} required minLength={8} />
49
+ <button type="submit" disabled={loading}>
50
+ {loading ? 'Creating...' : 'Create Account'}
51
+ </button>
52
+ <p className="auth-link">
53
+ Already have an account? <a href="/login">Sign in</a>
54
+ </p>
55
+ </form>
56
+ </div>
57
+ )
58
+ }
@@ -0,0 +1,34 @@
1
+ import { getToken } from './auth'
2
+
3
+ const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000'
4
+
5
+ export async function apiClient(path, options = {}) {
6
+ const { method = 'GET', body, headers = {} } = options
7
+
8
+ const token = getToken()
9
+ const config = {
10
+ method,
11
+ headers: {
12
+ Accept: 'application/json',
13
+ ...(token ? { Authorization: token } : {}),
14
+ ...headers
15
+ }
16
+ }
17
+
18
+ if (body !== undefined && body !== null) {
19
+ config.headers['Content-Type'] = 'application/json'
20
+ config.body = JSON.stringify(body)
21
+ }
22
+
23
+ const response = await fetch(`${API_URL}${path}`, config)
24
+ const data = await response.json()
25
+
26
+ if (response.status === 401) {
27
+ window.location.href = '/login'
28
+ throw new Error('Unauthorized')
29
+ }
30
+
31
+ if (!response.ok) throw new Error(data.error || `Request failed: ${response.status}`)
32
+
33
+ return data
34
+ }
@@ -0,0 +1,80 @@
1
+ import {
2
+ CognitoIdentityProviderClient,
3
+ InitiateAuthCommand,
4
+ RespondToAuthChallengeCommand,
5
+ SignUpCommand,
6
+ ConfirmSignUpCommand
7
+ } from '@aws-sdk/client-cognito-identity-provider'
8
+
9
+ const REGION = import.meta.env.VITE_AWS_REGION
10
+ const CLIENT_ID = import.meta.env.VITE_COGNITO_CLIENT_ID
11
+
12
+ const client = new CognitoIdentityProviderClient({ region: REGION })
13
+
14
+ let idToken = null
15
+ let refreshToken = null
16
+
17
+ export function getToken() { return idToken }
18
+ export function isAuthenticated() { return idToken !== null }
19
+
20
+ export async function signIn(username, password) {
21
+ const response = await client.send(new InitiateAuthCommand({
22
+ AuthFlow: 'USER_PASSWORD_AUTH',
23
+ ClientId: CLIENT_ID,
24
+ AuthParameters: { USERNAME: username, PASSWORD: password }
25
+ }))
26
+
27
+ if (response.ChallengeName === 'NEW_PASSWORD_REQUIRED') {
28
+ return { challenge: 'NEW_PASSWORD_REQUIRED', session: response.Session }
29
+ }
30
+
31
+ setTokens(response.AuthenticationResult)
32
+ return { success: true }
33
+ }
34
+
35
+ export async function signUp(email, password) {
36
+ await client.send(new SignUpCommand({
37
+ ClientId: CLIENT_ID,
38
+ Username: email,
39
+ Password: password
40
+ }))
41
+ return { needsConfirmation: true }
42
+ }
43
+
44
+ export async function confirmSignUp(email, code) {
45
+ await client.send(new ConfirmSignUpCommand({
46
+ ClientId: CLIENT_ID,
47
+ Username: email,
48
+ ConfirmationCode: code
49
+ }))
50
+ return { success: true }
51
+ }
52
+
53
+ export async function completeNewPassword(username, newPassword, session) {
54
+ const response = await client.send(new RespondToAuthChallengeCommand({
55
+ ChallengeName: 'NEW_PASSWORD_REQUIRED',
56
+ ClientId: CLIENT_ID,
57
+ Session: session,
58
+ ChallengeResponses: { USERNAME: username, NEW_PASSWORD: newPassword }
59
+ }))
60
+ setTokens(response.AuthenticationResult)
61
+ return { success: true }
62
+ }
63
+
64
+ export function signOut() {
65
+ idToken = null
66
+ refreshToken = null
67
+ localStorage.removeItem('idToken')
68
+ localStorage.removeItem('refreshToken')
69
+ }
70
+
71
+ function setTokens(authResult) {
72
+ idToken = authResult.IdToken
73
+ refreshToken = authResult.RefreshToken
74
+ localStorage.setItem('idToken', idToken)
75
+ if (refreshToken) localStorage.setItem('refreshToken', refreshToken)
76
+ }
77
+
78
+ // Restore on page load
79
+ const stored = localStorage.getItem('idToken')
80
+ if (stored) idToken = stored
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.2.18
4
+ version: 0.2.19
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stowzilla
@@ -144,6 +144,12 @@ files:
144
144
  - lib/templates/frontend_infra/frontend.tf.erb
145
145
  - lib/templates/generate/auth/cognito.tf.erb
146
146
  - lib/templates/generate/auth/cognito_outputs.tf.erb
147
+ - lib/templates/generate/auth/frontend/ConfirmEmail.jsx
148
+ - lib/templates/generate/auth/frontend/Login.jsx
149
+ - lib/templates/generate/auth/frontend/ProtectedRoute.jsx
150
+ - lib/templates/generate/auth/frontend/SignUp.jsx
151
+ - lib/templates/generate/auth/frontend/apiClient.js
152
+ - lib/templates/generate/auth/frontend/auth.js
147
153
  - lib/templates/generate/controller.rb.erb
148
154
  - lib/templates/generate/model.rb.erb
149
155
  - lib/templates/module/dns.tf.erb