belt 0.2.17 → 0.3.15

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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +241 -0
  3. data/README.md +42 -3
  4. data/lib/belt/action_router.rb +34 -19
  5. data/lib/belt/cli/app_detection.rb +30 -3
  6. data/lib/belt/cli/auth_command.rb +395 -0
  7. data/lib/belt/cli/console_command.rb +17 -2
  8. data/lib/belt/cli/deploy_command.rb +51 -17
  9. data/lib/belt/cli/destroy_command.rb +62 -25
  10. data/lib/belt/cli/doctor_command.rb +166 -22
  11. data/lib/belt/cli/environment_command.rb +12 -0
  12. data/lib/belt/cli/explain_command.rb +112 -0
  13. data/lib/belt/cli/frontend_command.rb +31 -10
  14. data/lib/belt/cli/frontend_deploy_command.rb +110 -21
  15. data/lib/belt/cli/frontend_env_command.rb +56 -19
  16. data/lib/belt/cli/frontend_env_map.rb +30 -10
  17. data/lib/belt/cli/frontend_registry.rb +373 -0
  18. data/lib/belt/cli/frontend_setup_command.rb +71 -5
  19. data/lib/belt/cli/generate_command.rb +61 -42
  20. data/lib/belt/cli/index_command.rb +192 -0
  21. data/lib/belt/cli/lambda_config_command.rb +9 -3
  22. data/lib/belt/cli/routes_command/request_model_inference.rb +131 -0
  23. data/lib/belt/cli/routes_command/route_inference.rb +4 -3
  24. data/lib/belt/cli/routes_command.rb +31 -19
  25. data/lib/belt/cli/server_command.rb +28 -15
  26. data/lib/belt/cli/tables_command.rb +3 -1
  27. data/lib/belt/cli/terraform_command.rb +9 -0
  28. data/lib/belt/cli/views_command.rb +15 -15
  29. data/lib/belt/cli/zip_artifact_builder.rb +199 -0
  30. data/lib/belt/cli.rb +17 -9
  31. data/lib/belt/docs/backups.md +101 -0
  32. data/lib/belt/docs/console.md +65 -0
  33. data/lib/belt/docs/controllers.md +155 -0
  34. data/lib/belt/docs/deployment.md +166 -0
  35. data/lib/belt/docs/frontend.md +103 -0
  36. data/lib/belt/docs/generators.md +137 -0
  37. data/lib/belt/docs/lambda_handler.md +105 -0
  38. data/lib/belt/docs/models.md +160 -0
  39. data/lib/belt/docs/observability.md +94 -0
  40. data/lib/belt/docs/plugins.md +94 -0
  41. data/lib/belt/docs/routing.md +138 -0
  42. data/lib/belt/docs/structure.md +96 -0
  43. data/lib/belt/inflector.rb +8 -0
  44. data/lib/belt/route_dsl.rb +318 -100
  45. data/lib/belt/version.rb +1 -1
  46. data/lib/templates/frontend/react/env.yml.example +2 -2
  47. data/lib/templates/generate/auth/cognito.tf.erb +71 -0
  48. data/lib/templates/generate/auth/cognito_outputs.tf.erb +19 -0
  49. data/lib/templates/generate/auth/frontend/ConfirmEmail.jsx +55 -0
  50. data/lib/templates/generate/auth/frontend/Login.jsx +79 -0
  51. data/lib/templates/generate/auth/frontend/ProtectedRoute.jsx +9 -0
  52. data/lib/templates/generate/auth/frontend/SignUp.jsx +58 -0
  53. data/lib/templates/generate/auth/frontend/apiClient.js +34 -0
  54. data/lib/templates/generate/auth/frontend/auth.css +157 -0
  55. data/lib/templates/generate/auth/frontend/auth.js +134 -0
  56. data/lib/templates/generate/model.rb.erb +4 -6
  57. data/lib/templates/module/frontend.tf.erb +49 -43
  58. data/lib/templates/module/outputs.tf.erb +1 -1
  59. data/lib/templates/new_app/AGENTS.md.erb +170 -20
  60. data/lib/templates/new_app/config/lambda/api.yml.erb +1 -0
  61. data/lib/templates/new_app/config/routes.rb.erb +8 -3
  62. data/lib/templates/new_app/lambda/api.rb.erb +1 -1
  63. metadata +56 -1
@@ -0,0 +1,71 @@
1
+ # Cognito User Pool — authentication for <%= @app_name %>
2
+ # Generated by: belt generate auth
3
+
4
+ <% @pools.each do |pool| -%>
5
+ resource "aws_cognito_user_pool" "<%= pool[:name] %>" {
6
+ name = "${var.app_name}-${var.environment}<%= pool[:suffix] %>"
7
+
8
+ password_policy {
9
+ minimum_length = 8
10
+ require_lowercase = true
11
+ require_numbers = true
12
+ require_symbols = false
13
+ require_uppercase = true
14
+ }
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 -%>
24
+ auto_verified_attributes = ["email"]
25
+
26
+ account_recovery_setting {
27
+ recovery_mechanism {
28
+ name = "verified_email"
29
+ priority = 1
30
+ }
31
+ }
32
+
33
+ schema {
34
+ name = "email"
35
+ attribute_data_type = "String"
36
+ required = true
37
+ mutable = true
38
+
39
+ string_attribute_constraints {
40
+ min_length = 1
41
+ max_length = 256
42
+ }
43
+ }
44
+
45
+ deletion_protection = var.environment == "prod" ? "ACTIVE" : "INACTIVE"
46
+ }
47
+
48
+ resource "aws_cognito_user_pool_client" "<%= pool[:name] %>" {
49
+ name = "${var.app_name}-${var.environment}<%= pool[:suffix] %>-client"
50
+ user_pool_id = aws_cognito_user_pool.<%= pool[:name] %>.id
51
+
52
+ explicit_auth_flows = [
53
+ "ALLOW_USER_SRP_AUTH",
54
+ "ALLOW_USER_PASSWORD_AUTH",
55
+ "ALLOW_REFRESH_TOKEN_AUTH"
56
+ ]
57
+
58
+ prevent_user_existence_errors = "ENABLED"
59
+
60
+ token_validity_units {
61
+ access_token = "hours"
62
+ id_token = "hours"
63
+ refresh_token = "days"
64
+ }
65
+
66
+ access_token_validity = 1
67
+ id_token_validity = 1
68
+ refresh_token_validity = 30
69
+ }
70
+
71
+ <% end -%>
@@ -0,0 +1,19 @@
1
+ # Cognito outputs — generated by: belt generate auth
2
+
3
+ <% @pools.each do |pool| -%>
4
+ output "cognito_user_pool_id<%= pool[:suffix] %>" {
5
+ description = "Cognito User Pool ID<%= pool[:label] %>"
6
+ value = aws_cognito_user_pool.<%= pool[:name] %>.id
7
+ }
8
+
9
+ output "cognito_user_pool_arn<%= pool[:suffix] %>" {
10
+ description = "Cognito User Pool ARN<%= pool[:label] %>"
11
+ value = aws_cognito_user_pool.<%= pool[:name] %>.arn
12
+ }
13
+
14
+ output "cognito_client_id<%= pool[:suffix] %>" {
15
+ description = "Cognito User Pool Client ID<%= pool[:label] %>"
16
+ value = aws_cognito_user_pool_client.<%= pool[:name] %>.id
17
+ }
18
+
19
+ <% end -%>
@@ -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,79 @@
1
+ import { useState } from 'react'
2
+ import { signIn, completeNewPassword } from '../../lib/auth'
3
+ import './auth.css'
4
+
5
+ export default function Login({ onLogin }) {
6
+ const [email, setEmail] = useState('')
7
+ const [password, setPassword] = useState('')
8
+ const [newPassword, setNewPassword] = useState('')
9
+ const [error, setError] = useState('')
10
+ const [challenge, setChallenge] = useState(null)
11
+ const [loading, setLoading] = useState(false)
12
+
13
+ async function handleSubmit(e) {
14
+ e.preventDefault()
15
+ setError('')
16
+ setLoading(true)
17
+
18
+ try {
19
+ const result = await signIn(email, password)
20
+ if (result.challenge === 'NEW_PASSWORD_REQUIRED') {
21
+ setChallenge(result)
22
+ } else {
23
+ onLogin()
24
+ }
25
+ } catch (err) {
26
+ setError(err.message || 'Sign in failed')
27
+ } finally {
28
+ setLoading(false)
29
+ }
30
+ }
31
+
32
+ async function handleNewPassword(e) {
33
+ e.preventDefault()
34
+ setError('')
35
+ setLoading(true)
36
+
37
+ try {
38
+ await completeNewPassword(email, newPassword, challenge.session)
39
+ onLogin()
40
+ } catch (err) {
41
+ setError(err.message || 'Password change failed')
42
+ } finally {
43
+ setLoading(false)
44
+ }
45
+ }
46
+
47
+ if (challenge) {
48
+ return (
49
+ <div className="auth-page">
50
+ <form className="auth-form" onSubmit={handleNewPassword}>
51
+ <h2>Set New Password</h2>
52
+ <p>Please set a permanent password for your account.</p>
53
+ {error && <div className="auth-error">{error}</div>}
54
+ <input type="password" placeholder="New password" value={newPassword}
55
+ onChange={e => setNewPassword(e.target.value)} required autoFocus />
56
+ <button type="submit" disabled={loading}>
57
+ {loading ? 'Saving...' : 'Set Password'}
58
+ </button>
59
+ </form>
60
+ </div>
61
+ )
62
+ }
63
+
64
+ return (
65
+ <div className="auth-page">
66
+ <form className="auth-form" onSubmit={handleSubmit}>
67
+ <h2>Sign In</h2>
68
+ {error && <div className="auth-error">{error}</div>}
69
+ <input type="email" placeholder="Email" value={email}
70
+ onChange={e => setEmail(e.target.value)} required autoFocus />
71
+ <input type="password" placeholder="Password" value={password}
72
+ onChange={e => setPassword(e.target.value)} required />
73
+ <button type="submit" disabled={loading}>
74
+ {loading ? 'Signing in...' : 'Sign In'}
75
+ </button>
76
+ </form>
77
+ </div>
78
+ )
79
+ }
@@ -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,157 @@
1
+ .auth-page {
2
+ --auth-bg: #f6f8fa;
3
+ --auth-card-bg: #ffffff;
4
+ --auth-border: #d0d7de;
5
+ --auth-text: #1f2328;
6
+ --auth-muted: #656d76;
7
+ --auth-input-bg: #f6f8fa;
8
+ --auth-btn: #2da44e;
9
+ --auth-btn-hover: #2c974b;
10
+ --auth-btn-disabled-bg: #94d3a2;
11
+ --auth-btn-disabled-text: #fff;
12
+ --auth-error-bg: rgba(248, 81, 73, 0.08);
13
+ --auth-error-border: rgba(248, 81, 73, 0.4);
14
+ --auth-error-text: #cf222e;
15
+ --auth-link: #0969da;
16
+ --auth-focus: #0969da;
17
+ }
18
+
19
+ @media (prefers-color-scheme: dark) {
20
+ .auth-page {
21
+ --auth-bg: #0d1117;
22
+ --auth-card-bg: #161b22;
23
+ --auth-border: #30363d;
24
+ --auth-text: #e6edf3;
25
+ --auth-muted: #8b949e;
26
+ --auth-input-bg: #0d1117;
27
+ --auth-btn: #238636;
28
+ --auth-btn-hover: #2ea043;
29
+ --auth-btn-disabled-bg: #21262d;
30
+ --auth-btn-disabled-text: #484f58;
31
+ --auth-error-bg: rgba(248, 81, 73, 0.1);
32
+ --auth-error-border: rgba(248, 81, 73, 0.4);
33
+ --auth-error-text: #f85149;
34
+ --auth-link: #58a6ff;
35
+ --auth-focus: #58a6ff;
36
+ }
37
+ }
38
+
39
+ .auth-page {
40
+ min-height: 100vh;
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: center;
44
+ background: var(--auth-bg);
45
+ padding: 1rem;
46
+ }
47
+
48
+ .auth-form {
49
+ background: var(--auth-card-bg);
50
+ border: 1px solid var(--auth-border);
51
+ border-radius: 12px;
52
+ padding: 2.5rem;
53
+ width: 100%;
54
+ max-width: 380px;
55
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
56
+ }
57
+
58
+ .auth-form h2 {
59
+ color: var(--auth-text);
60
+ font-size: 1.5rem;
61
+ margin: 0 0 0.5rem;
62
+ text-align: center;
63
+ }
64
+
65
+ .auth-form p {
66
+ color: var(--auth-muted);
67
+ font-size: 0.85rem;
68
+ text-align: center;
69
+ margin: 0 0 1.5rem;
70
+ }
71
+
72
+ .auth-form input {
73
+ display: block;
74
+ width: 100%;
75
+ padding: 0.75rem 1rem;
76
+ margin-bottom: 0.75rem;
77
+ background: var(--auth-input-bg);
78
+ border: 1px solid var(--auth-border);
79
+ border-radius: 8px;
80
+ color: var(--auth-text);
81
+ font-size: 0.9rem;
82
+ outline: none;
83
+ transition: border-color 0.2s;
84
+ box-sizing: border-box;
85
+ }
86
+
87
+ .auth-form input:focus {
88
+ border-color: var(--auth-focus);
89
+ }
90
+
91
+ .auth-form input::placeholder {
92
+ color: var(--auth-muted);
93
+ }
94
+
95
+ .auth-form button {
96
+ display: block;
97
+ width: 100%;
98
+ padding: 0.75rem;
99
+ margin-top: 0.5rem;
100
+ background: var(--auth-btn);
101
+ border: none;
102
+ border-radius: 8px;
103
+ color: #fff;
104
+ font-size: 0.9rem;
105
+ font-weight: 600;
106
+ cursor: pointer;
107
+ transition: background 0.2s;
108
+ }
109
+
110
+ .auth-form button:hover {
111
+ background: var(--auth-btn-hover);
112
+ }
113
+
114
+ .auth-form button:disabled {
115
+ background: var(--auth-btn-disabled-bg);
116
+ color: var(--auth-btn-disabled-text);
117
+ cursor: not-allowed;
118
+ }
119
+
120
+ .auth-error {
121
+ background: var(--auth-error-bg);
122
+ border: 1px solid var(--auth-error-border);
123
+ border-radius: 8px;
124
+ color: var(--auth-error-text);
125
+ padding: 0.6rem 0.75rem;
126
+ font-size: 0.8rem;
127
+ margin-bottom: 1rem;
128
+ text-align: center;
129
+ }
130
+
131
+ .auth-link {
132
+ color: var(--auth-muted);
133
+ font-size: 0.8rem;
134
+ text-align: center;
135
+ margin-top: 1.25rem !important;
136
+ }
137
+
138
+ .auth-link a {
139
+ color: var(--auth-link);
140
+ text-decoration: none;
141
+ }
142
+
143
+ .auth-link a:hover {
144
+ text-decoration: underline;
145
+ }
146
+
147
+ .auth-btn {
148
+ display: inline-block;
149
+ padding: 0.6rem 1.5rem;
150
+ background: var(--auth-btn);
151
+ border-radius: 8px;
152
+ color: #fff;
153
+ text-decoration: none;
154
+ font-weight: 600;
155
+ font-size: 0.85rem;
156
+ margin-top: 1rem;
157
+ }
@@ -0,0 +1,134 @@
1
+ import {
2
+ CognitoUserPool,
3
+ CognitoUser,
4
+ AuthenticationDetails,
5
+ CognitoUserAttribute
6
+ } from 'amazon-cognito-identity-js'
7
+
8
+ const REGION = import.meta.env.VITE_AWS_REGION
9
+ const CLIENT_ID = import.meta.env.VITE_COGNITO_CLIENT_ID
10
+ const USER_POOL_ID = import.meta.env.VITE_COGNITO_USER_POOL_ID
11
+
12
+ // amazon-cognito-identity-js uses SRP (Secure Remote Password) by default.
13
+ // The password is NEVER sent over the wire — only the SRP_A proof is
14
+ // transmitted, so credentials can't leak into request logs or proxies.
15
+ const userPool = new CognitoUserPool({
16
+ UserPoolId: USER_POOL_ID,
17
+ ClientId: CLIENT_ID
18
+ })
19
+
20
+ let idToken = null
21
+ let refreshToken = null
22
+
23
+ export function getToken() { return idToken }
24
+ export function isAuthenticated() { return idToken !== null }
25
+
26
+ export function signIn(username, password) {
27
+ const cognitoUser = new CognitoUser({ Username: username, Pool: userPool })
28
+ const authDetails = new AuthenticationDetails({ Username: username, Password: password })
29
+
30
+ return new Promise((resolve, reject) => {
31
+ cognitoUser.authenticateUser(authDetails, {
32
+ onSuccess: (session) => {
33
+ setTokens({
34
+ IdToken: session.getIdToken().getJwtToken(),
35
+ RefreshToken: session.getRefreshToken().getToken()
36
+ })
37
+ resolve({ success: true })
38
+ },
39
+ onFailure: (err) => reject(err),
40
+ newPasswordRequired: (_userAttributes, requiredAttributes) => {
41
+ // Stash the user + the attributes Cognito actually requires so
42
+ // completeNewPassword can continue the SRP session and supply exactly
43
+ // what the pool asks for — no more, no less.
44
+ pendingUser = cognitoUser
45
+ pendingRequiredAttributes = requiredAttributes || []
46
+ resolve({ challenge: 'NEW_PASSWORD_REQUIRED' })
47
+ }
48
+ })
49
+ })
50
+ }
51
+
52
+ // Holds the CognitoUser mid-challenge for the NEW_PASSWORD_REQUIRED flow,
53
+ // plus the list of attributes Cognito flagged as required for the challenge.
54
+ let pendingUser = null
55
+ let pendingRequiredAttributes = []
56
+
57
+ export function completeNewPassword(email, newPassword) {
58
+ if (!pendingUser) {
59
+ return Promise.reject(new Error('No pending password challenge. Sign in again.'))
60
+ }
61
+
62
+ // Only supply attributes Cognito actually asks for in this challenge.
63
+ // - If the pool requires `email` and the user doesn't have it yet, it shows
64
+ // up in requiredAttributes → we send it (username is the email). Skipping
65
+ // it would fail with "Invalid attributes given, email is missing".
66
+ // - If the user already has `email` (e.g. admin-created with it), it is NOT
67
+ // in requiredAttributes → we must NOT send it, or Cognito rejects with
68
+ // "Cannot modify an already provided email".
69
+ const attributes = {}
70
+ if (pendingRequiredAttributes.includes('email')) {
71
+ attributes.email = email
72
+ }
73
+
74
+ return new Promise((resolve, reject) => {
75
+ pendingUser.completeNewPasswordChallenge(newPassword, attributes, {
76
+ onSuccess: (session) => {
77
+ setTokens({
78
+ IdToken: session.getIdToken().getJwtToken(),
79
+ RefreshToken: session.getRefreshToken().getToken()
80
+ })
81
+ pendingUser = null
82
+ pendingRequiredAttributes = []
83
+ resolve({ success: true })
84
+ },
85
+ onFailure: (err) => {
86
+ pendingUser = null
87
+ pendingRequiredAttributes = []
88
+ reject(err)
89
+ }
90
+ })
91
+ })
92
+ }
93
+
94
+ export function signUp(email, password) {
95
+ const attributes = [new CognitoUserAttribute({ Name: 'email', Value: email })]
96
+
97
+ return new Promise((resolve, reject) => {
98
+ userPool.signUp(email, password, attributes, null, (err) => {
99
+ if (err) return reject(err)
100
+ resolve({ needsConfirmation: true })
101
+ })
102
+ })
103
+ }
104
+
105
+ export function confirmSignUp(email, code) {
106
+ const cognitoUser = new CognitoUser({ Username: email, Pool: userPool })
107
+
108
+ return new Promise((resolve, reject) => {
109
+ cognitoUser.confirmRegistration(code, true, (err) => {
110
+ if (err) return reject(err)
111
+ resolve({ success: true })
112
+ })
113
+ })
114
+ }
115
+
116
+ export function signOut() {
117
+ const cognitoUser = userPool.getCurrentUser()
118
+ if (cognitoUser) cognitoUser.signOut()
119
+ idToken = null
120
+ refreshToken = null
121
+ localStorage.removeItem('idToken')
122
+ localStorage.removeItem('refreshToken')
123
+ }
124
+
125
+ function setTokens(authResult) {
126
+ idToken = authResult.IdToken
127
+ refreshToken = authResult.RefreshToken
128
+ localStorage.setItem('idToken', idToken)
129
+ if (refreshToken) localStorage.setItem('refreshToken', refreshToken)
130
+ }
131
+
132
+ // Restore on page load
133
+ const stored = localStorage.getItem('idToken')
134
+ if (stored) idToken = stored
@@ -4,13 +4,11 @@ class <%= @class_name %> < ApplicationRecord
4
4
  <% @references.each do |ref| -%>
5
5
  belongs_to :<%= ref[:referenced_model] %>
6
6
  <% end -%>
7
- <% if @references.any? && (@regular_fields.any? || @references.any?) -%>
7
+ <% if @references.any? && @regular_fields.any? -%>
8
8
 
9
9
  <% end -%>
10
- <% @references.each do |ref| -%>
11
- attr_accessor :<%= ref[:referenced_model] %>_id
12
- <% end -%>
13
- <% @regular_fields.each do |field| -%>
14
- attr_accessor :<%= field[:name] %>
10
+ <% all_accessors = @regular_fields.map { |f| ":#{f[:name]}" } -%>
11
+ <% if all_accessors.any? -%>
12
+ attr_accessor <%= all_accessors.join(', ') %>
15
13
  <% end -%>
16
14
  end