belt 0.3.44 → 0.4.1

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.
@@ -95,11 +95,11 @@ module Belt
95
95
  if @parent_environment
96
96
  puts "\nThis is a nested environment under '#{@parent_environment}'."
97
97
  puts "It will use the parent's wildcard certificate and hosted zone."
98
- if @domain
99
- puts "Domain will be: api.#{@env_name}.#{@parent_environment}.#{@domain}"
100
- else
101
- puts "Domain will be: #{@env_name}.#{@parent_environment}.<your-domain>"
102
- end
98
+ # Nested envs use an `api-<env>` prefix (not `api.<env>`) so the API
99
+ # host stays a single level under the parent's *.<parent>.<domain>
100
+ # wildcard cert. See lib/templates/module/dns.tf.erb (api_domain).
101
+ puts "Frontend will be: https://#{@env_name}.#{@parent_environment}.#{@domain}"
102
+ puts "API will be: https://api-#{@env_name}.#{@parent_environment}.#{@domain}"
103
103
  puts "\nDeploy with:"
104
104
  puts " belt deploy #{@env_name}"
105
105
  puts "\nNo DNS delegation needed — the parent environment handles DNS."
@@ -17,6 +17,12 @@ module Belt
17
17
  'model' => 'models',
18
18
  'activeitem' => 'models',
19
19
  'dynamodb' => 'models',
20
+ 'auth' => 'authentication',
21
+ 'cognito' => 'authentication',
22
+ 'users' => 'authentication',
23
+ 'user' => 'authentication',
24
+ 'current_user' => 'authentication',
25
+ 'login' => 'authentication',
20
26
  'deploy' => 'deployment',
21
27
  'deploying' => 'deployment',
22
28
  'terraform' => 'deployment',
@@ -75,6 +75,12 @@ module Belt
75
75
 
76
76
  private
77
77
 
78
+ # Load infrastructure/<env>/belt.rb and apply its aws_profile + env vars
79
+ # to the current process. Without this, `terraform output` can't reach the
80
+ # S3 state backend (403), fetch_tf_output returns nil, and the deploy aborts
81
+ # with a misleading "Could not determine S3 bucket" error. The full
82
+ # `belt deploy` path applies this before invoking the frontend deploy;
83
+ # standalone `belt deploy frontend` must do it too.
78
84
  def load_and_apply_env_config!
79
85
  env_config = EnvironmentConfig.load(@env, infra_dir: @infra_dir)
80
86
  env_config.apply!
@@ -2,8 +2,10 @@
2
2
 
3
3
  require_relative 'app_detection'
4
4
  require_relative 'env_resolver'
5
+ require_relative 'environment_config'
5
6
  require_relative 'frontend_env_map'
6
7
  require_relative 'frontend_registry'
8
+ require_relative 'terraform_command'
7
9
 
8
10
  module Belt
9
11
  module CLI
@@ -111,18 +113,19 @@ module Belt
111
113
  def initialize(env, frontend: nil)
112
114
  @env = env
113
115
  @app_name = detect_app_name
114
- @env_dir = "infrastructure/#{@env}"
116
+ @infra_dir = TerraformCommand.find_infrastructure_dir || 'infrastructure'
117
+ @env_dir = File.join(@infra_dir, @env)
115
118
  @frontend = frontend || FrontendRegistry.new.resolve!
116
119
  end
117
120
 
118
121
  def run
122
+ load_and_apply_env_config!
123
+
119
124
  unless Dir.exist?(@frontend.path)
120
125
  abort "Error: No #{@frontend.path}/ directory found. Run `belt generate frontend react` first."
121
126
  end
122
127
 
123
- unless Dir.exist?(@env_dir)
124
- abort "Error: infrastructure/#{@env} not found. Run `belt generate environment #{@env}` first."
125
- end
128
+ abort "Error: #{@env_dir} not found. Run `belt generate environment #{@env}` first." unless Dir.exist?(@env_dir)
126
129
 
127
130
  map = FrontendEnvMap.new(@env, env_dir: @env_dir, frontend_path: @frontend.path)
128
131
 
@@ -142,6 +145,12 @@ module Belt
142
145
  puts "✅ Updated #{result[:path]} (#{updated.join(', ')})"
143
146
  end
144
147
  end
148
+
149
+ private
150
+
151
+ def load_and_apply_env_config!
152
+ EnvironmentConfig.load(@env, infra_dir: @infra_dir).apply!
153
+ end
145
154
  end
146
155
  end
147
156
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'json'
4
4
  require 'open3'
5
+ require_relative 'environment_config'
6
+ require_relative 'terraform_command'
5
7
 
6
8
  module Belt
7
9
  module CLI
@@ -76,6 +78,8 @@ module Belt
76
78
  @env ||= detect_environment
77
79
  abort 'Error: Cannot determine environment. Pass -e ENV or set BELT_ENV.' unless @env
78
80
 
81
+ apply_env_config!
82
+
79
83
  @app_name = detect_app_name
80
84
  abort 'Error: Cannot determine app name.' unless @app_name
81
85
 
@@ -88,6 +92,13 @@ module Belt
88
92
 
89
93
  private
90
94
 
95
+ def apply_env_config!
96
+ infra_dir = TerraformCommand.find_infrastructure_dir || find_infra_dir
97
+ env_config = EnvironmentConfig.load(@env, infra_dir: infra_dir)
98
+ env_config.apply!
99
+ puts " 🔑 Using AWS profile: #{env_config.aws_profile}" if env_config.aws_profile?
100
+ end
101
+
91
102
  def parse_args(args)
92
103
  i = 0
93
104
  while i < args.length
@@ -3,6 +3,7 @@
3
3
  require 'base64'
4
4
  require 'json'
5
5
  require_relative 'app_detection'
6
+ require_relative 'environment_config'
6
7
  require_relative 'frontend_env_map'
7
8
  require_relative 'frontend_registry'
8
9
  require_relative 'terraform_command'
@@ -125,6 +126,9 @@ module Belt
125
126
  env_name = @deploy_env || ENV.fetch('BELT_ENV', nil) || TerraformCommand.list_environments.first
126
127
  return {} unless env_name
127
128
 
129
+ infra_dir = TerraformCommand.find_infrastructure_dir
130
+ EnvironmentConfig.load(env_name, infra_dir: infra_dir).apply!
131
+
128
132
  FrontendEnvMap.new(env_name, frontend_path: @frontend.path).process_env
129
133
  rescue StandardError
130
134
  # Fall back to legacy api_url detection if map resolution fails
@@ -274,6 +278,7 @@ module Belt
274
278
  next unless Dir.exist?(env_dir)
275
279
  next unless File.exist?(File.join(env_dir, '.terraform'))
276
280
 
281
+ EnvironmentConfig.load(env, infra_dir: infra_dir).apply!
277
282
  url = read_api_url_from_outputs(env_dir)
278
283
  if url
279
284
  @deploy_env = env
@@ -84,6 +84,9 @@ module Belt
84
84
  # Extract indexes() declaration
85
85
  indexes = extract_indexes(content)
86
86
 
87
+ # cognito_authenticatable installs a GSI without an indexes() call
88
+ indexes += extract_cognito_indexes(content)
89
+
87
90
  # Extract belongs_to associations and generate convention indexes
88
91
  indexes += extract_belongs_to_indexes(content)
89
92
 
@@ -115,15 +118,33 @@ module Belt
115
118
  indexes
116
119
  end
117
120
 
121
+ # `cognito_authenticatable` installs an EmailIndex GSI without the model ever
122
+ # calling indexes() — see Belt::Authentication::CognitoAuthenticatable. The
123
+ # generator has to know that, or the table would be created without the GSI and
124
+ # the first email lookup would fail in production instead of here.
125
+ def extract_cognito_indexes(content)
126
+ declaration = uncommented(content).match(/^\s*cognito_authenticatable\b(.*)$/)
127
+ return [] unless declaration
128
+
129
+ options = declaration[1].to_s
130
+ return [] if options.match?(/email_index:\s*false/)
131
+
132
+ name = options.match(/email_index:\s*['"]([^'"]+)['"]/)
133
+ [{ name: name ? name[1] : 'EmailIndex', partition_key: 'email', sort_key: nil }]
134
+ end
135
+
118
136
  # Extract belongs_to declarations and generate convention-based GSI indexes.
119
137
  # belongs_to :conversation → ConversationIndex with partition_key: 'conversationId'
138
+ #
139
+ # `index: false` opts out — the model is saying the reverse lookup is covered some
140
+ # other way (its own indexes() entry, usually, under a different key name).
141
+ # Generating one anyway produces a GSI on an attribute that doesn't exist.
120
142
  def extract_belongs_to_indexes(content)
121
143
  indexes = []
122
144
 
123
- # Skip commented-out belongs_to lines
124
- content.lines.reject { |line| line.strip.start_with?('#') }.join
125
- .scan(/belongs_to\s+:(\w+)/) do |match|
126
- association_name = match[0]
145
+ uncommented(content).scan(/belongs_to\s+:(\w+)([^\n]*)/) do |association_name, options|
146
+ next if options.match?(/index:\s*false/)
147
+
127
148
  index_name = "#{Belt::Inflector.classify(association_name)}Index"
128
149
  partition_key = "#{association_name}Id"
129
150
 
@@ -133,6 +154,10 @@ module Belt
133
154
  indexes
134
155
  end
135
156
 
157
+ def uncommented(content)
158
+ content.lines.reject { |line| line.strip.start_with?('#') }.join
159
+ end
160
+
136
161
  def generate_dynamodb_tf(models)
137
162
  dest = File.join(MODULE_DIR, 'dynamodb.tf')
138
163
  existing_content = File.exist?(dest) ? File.read(dest) : nil
@@ -5,6 +5,7 @@ module Belt
5
5
  #
6
6
  # Belt.configure do |config|
7
7
  # config.default_format = :json # or :html
8
+ # config.authentication.user_class = 'Account'
8
9
  # end
9
10
  #
10
11
  # Note: infrastructure/<env>/belt.rb uses a separate sandboxed DSL for CLI
@@ -18,6 +19,12 @@ module Belt
18
19
 
19
20
  attr_reader :default_format
20
21
 
22
+ # Cognito identity settings — see Belt::Authentication::Configuration.
23
+ # Resolved lazily so config can be set before the app's models are loaded.
24
+ def authentication
25
+ @authentication ||= Belt::Authentication::Configuration.new
26
+ end
27
+
21
28
  def default_format=(value)
22
29
  format = value.to_sym
23
30
  unless VALID_FORMATS.include?(format)
@@ -27,4 +34,23 @@ module Belt
27
34
  @default_format = format
28
35
  end
29
36
  end
37
+
38
+ # Runtime configuration accessors live here, not in belt.rb, so that a single
39
+ # subsystem can be required on its own (`require 'belt/authentication'`) and still
40
+ # reach Belt.configuration. Test harnesses that shadow BeltController do exactly that.
41
+ class << self
42
+ # Runtime configuration (lambda/config/environment.rb). Separate from the
43
+ # CLI sandboxed DSL in infrastructure/<env>/belt.rb.
44
+ def configuration
45
+ @configuration ||= Configuration.new
46
+ end
47
+
48
+ def configure
49
+ yield configuration
50
+ end
51
+
52
+ def reset_configuration!
53
+ @configuration = Configuration.new
54
+ end
55
+ end
30
56
  end
@@ -0,0 +1,186 @@
1
+ # Authentication
2
+
3
+ Cognito owns authentication — passwords, MFA, hosted signup, groups. Belt owns the
4
+ **record** of the human Cognito authenticated, so your app can answer its own questions
5
+ about that person without re-reading JWT claims in every controller.
6
+
7
+ Declare it on a model and you're done:
8
+
9
+ ```ruby
10
+ class User < ApplicationRecord
11
+ cognito_authenticatable
12
+ end
13
+ ```
14
+
15
+ That one line supplies:
16
+
17
+ | | |
18
+ |---|---|
19
+ | Primary key | the Cognito `sub` — resolving the caller is one `GetItem` |
20
+ | Attributes | `email`, `name`, `role`, `email_verified`, `last_seen_on` |
21
+ | GSI | `EmailIndex`, for finding a person by address |
22
+ | Class methods | `.sync_from_claims!`, `.for_sub`, `.for_email` |
23
+ | Instance methods | `#admin?`, `#email_verified?` |
24
+
25
+ Controllers get `current_user`, `authenticate_user!`, `user_signed_in?`, and
26
+ `cognito_admin?` with no `include` and no configuration.
27
+
28
+ ## The table
29
+
30
+ ```hcl
31
+ resource "aws_dynamodb_table" "users" {
32
+ name = "${var.app_name}-${var.environment}-users"
33
+ billing_mode = "PAY_PER_REQUEST"
34
+ hash_key = "id" # the Cognito sub
35
+
36
+ attribute {
37
+ name = "id"
38
+ type = "S"
39
+ }
40
+
41
+ attribute {
42
+ name = "email"
43
+ type = "S"
44
+ }
45
+
46
+ global_secondary_index {
47
+ name = "EmailIndex"
48
+ hash_key = "email"
49
+ projection_type = "ALL"
50
+ }
51
+ }
52
+ ```
53
+
54
+ Identity attributes are stored snake_case (`email_verified`, not `emailVerified`) so
55
+ they read the same in the console, in `dynamodb.tf`, and in a GSI key definition.
56
+
57
+ Grant the table to every Lambda, not per route. Authenticated requests read it *before*
58
+ the action runs, so per-route `tables:` means listing it on every route and 500ing on
59
+ whichever one you missed.
60
+
61
+ ## In a controller
62
+
63
+ ```ruby
64
+ class ProfilesController < ApplicationController
65
+ before_action :authenticate_user!
66
+
67
+ def show
68
+ @profile = current_user
69
+ end
70
+ end
71
+ ```
72
+
73
+ `authenticate_user!` raises `Belt::Authentication::NotAuthenticated`, which Belt already
74
+ maps to **401**. It has to raise: `before_action` cannot halt the chain by returning a
75
+ response — return values are discarded and the action runs anyway.
76
+
77
+ | Method | |
78
+ |---|---|
79
+ | `current_user` | the user record, or nil. Memoized per request |
80
+ | `user_signed_in?` | is there a Cognito identity on this request? |
81
+ | `authenticate_user!` | `before_action` guard → 401 |
82
+ | `cognito_admin?` | does the token carry a staff Cognito group? |
83
+ | `cognito_claims` | raw claims, if you really need them |
84
+ | `bearer_token` | the raw `Authorization: Bearer` credential |
85
+
86
+ If your app has a *second* credential scheme — an API key for machine callers, say —
87
+ override `skip_cognito_identity?` to declare that such a request is not a human:
88
+
89
+ ```ruby
90
+ def skip_cognito_identity?
91
+ agent_request? # `Authorization: Bearer fp_...`
92
+ end
93
+ ```
94
+
95
+ Usually unnecessary: a credential that isn't a Cognito ID token yields no claims and
96
+ therefore no `current_user`. It matters when a request could be read as both.
97
+
98
+ ## Just-in-time provisioning
99
+
100
+ There is no signup endpoint to keep in step with Cognito's hosted UI. The first
101
+ authenticated request from a user writes the row; later requests only write when
102
+ something actually drifted — a name changed in Cognito, a staff group was granted or
103
+ revoked, or it's the first sighting today. Steady state is one `GetItem` and no write.
104
+
105
+ `last_seen_on` is a **date**, not a timestamp, precisely so an active session doesn't
106
+ generate a write per request.
107
+
108
+ To hang your own behaviour off that moment, override the hook:
109
+
110
+ ```ruby
111
+ class User < ApplicationRecord
112
+ cognito_authenticatable
113
+
114
+ # Runs whenever an identity is resolved from a token.
115
+ def after_cognito_sync
116
+ claim_pending_invitations!
117
+ end
118
+ end
119
+ ```
120
+
121
+ ## Platform staff
122
+
123
+ `role` is platform-wide: `member` or `admin`. It mirrors a Cognito group on **every**
124
+ request, so removing someone from the group locks them out on their very next call.
125
+
126
+ ```ruby
127
+ current_user.admin? # FeatureParity staff — can see across tenants
128
+ cognito_admin? # the grant: does this token carry the group?
129
+ ```
130
+
131
+ Per-tenant roles (owner of a project, member of an org) are your domain, not this
132
+ concern's. Model them yourself.
133
+
134
+ Grant staff by hand — Terraform should create the group but not manage its members, so
135
+ that a `terraform apply` can't hand out platform-wide read access:
136
+
137
+ ```bash
138
+ aws cognito-idp admin-add-user-to-group \
139
+ --user-pool-id <pool> --username <you> --group-name admins
140
+ ```
141
+
142
+ ## Configuration
143
+
144
+ Every setting has a working default. An app whose model is `User` and whose staff group
145
+ is `admins` configures nothing.
146
+
147
+ ```ruby
148
+ # lambda/config/environment.rb
149
+ Belt.configure do |config|
150
+ config.authentication.user_class = 'Account' # default: 'User'
151
+ config.authentication.admin_groups = %w[staff] # default: ['admins'], or ADMIN_COGNITO_GROUPS
152
+ config.authentication.issuer = '...' # default: derived from COGNITO_USER_POOL_ID
153
+ end
154
+ ```
155
+
156
+ Macro options:
157
+
158
+ ```ruby
159
+ cognito_authenticatable roles: %w[member admin support],
160
+ default_role: 'member',
161
+ email_index: 'PeopleEmailIndex' # or false to skip it
162
+ ```
163
+
164
+ ## Two token shapes
165
+
166
+ A route can be authenticated either way, and both are handled:
167
+
168
+ 1. **API Gateway Cognito authorizer** — claims arrive pre-verified under
169
+ `requestContext.authorizer.claims`, with every value flattened to a string
170
+ (`"true"`, `"[admins, members]"`).
171
+ 2. **A raw `Authorization: Bearer <id token>` header** — the Lambda decodes it.
172
+
173
+ Case 2 is signature-unverified by design: where an authorizer is attached, the gateway
174
+ already checked the signature and an unsigned token never reaches your code. What can
175
+ still be checked cheaply is checked — structure, expiry, issuer, and `token_use` (an
176
+ access token is rejected; it carries no email or name).
177
+
178
+ Anything that isn't a Cognito ID token — including your own API key scheme sharing the
179
+ same header — reads as "no Cognito identity" rather than an error, so
180
+ `current_user` is simply nil.
181
+
182
+ ## See also
183
+
184
+ - `belt explain models` — ActiveItem
185
+ - `belt explain controllers` — `before_action`, response helpers
186
+ - `belt generate auth` — create the Cognito user pool
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Belt
4
+ class AuthenticationError < StandardError; end
5
+ class RecordNotFound < StandardError; end
6
+ class ActionNotFound < StandardError; end
7
+ class TemplateNotFound < StandardError; end
8
+ end
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.44'
4
+ VERSION = '0.4.1'
5
5
  end
data/lib/belt.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require 'activeitem'
4
4
  require_relative 'belt/version'
5
5
  require_relative 'belt/root'
6
+ require_relative 'belt/errors'
6
7
  require_relative 'belt/configuration'
7
8
  require_relative 'belt/http_status'
8
9
  require_relative 'belt/parameters'
@@ -11,30 +12,11 @@ require_relative 'belt/lambda_handler'
11
12
  require_relative 'belt/action_router'
12
13
 
13
14
  module Belt
14
- class AuthenticationError < StandardError; end
15
- class RecordNotFound < StandardError; end
16
- class ActionNotFound < StandardError; end
17
- class TemplateNotFound < StandardError; end
18
-
19
15
  @controller_paths = []
20
16
 
21
17
  class << self
22
18
  attr_reader :controller_paths
23
19
 
24
- # Runtime configuration (lambda/config/environment.rb). Separate from the
25
- # CLI sandboxed DSL in infrastructure/<env>/belt.rb.
26
- def configuration
27
- @configuration ||= Configuration.new
28
- end
29
-
30
- def configure
31
- yield configuration
32
- end
33
-
34
- def reset_configuration!
35
- @configuration = Configuration.new
36
- end
37
-
38
20
  # Auto-discover lambda/controllers dirs in all loaded gems
39
21
  def gem_controller_paths
40
22
  @gem_controller_paths ||= discover_gem_paths('lambda/controllers')
@@ -72,5 +54,6 @@ module Belt
72
54
  end
73
55
  end
74
56
 
57
+ require_relative 'belt/authentication'
75
58
  require_relative 'belt_controller/base'
76
59
  require_relative 'belt/controllers/welcome_controller'
@@ -3,6 +3,7 @@
3
3
  require 'json'
4
4
  require 'cgi'
5
5
  require_relative '../belt/parameters'
6
+ require_relative '../belt/authentication'
6
7
  require_relative '../belt/helpers/response'
7
8
  require_relative '../belt/helpers/error_logging'
8
9
  require_relative '../belt/helpers/cors_origin'
@@ -14,6 +15,9 @@ module BeltController
14
15
  include Belt::Helpers::Response
15
16
  include Belt::Rendering
16
17
  include ImplicitResponse
18
+ # Cognito identity: current_user, authenticate_user!, cognito_admin?.
19
+ # Inert unless the app declares `cognito_authenticatable` on a model.
20
+ include Belt::Authentication::Controller
17
21
 
18
22
  attr_reader :event, :body
19
23
 
@@ -8,6 +8,7 @@ module BeltController
8
8
  FRAMEWORK_IVARS = %i[
9
9
  @event @raw_body @params @current_action @current_user_id @user_groups
10
10
  @logger @__assigns_before @__response_status
11
+ @current_user @cognito_claims @cognito_groups
11
12
  ].freeze
12
13
 
13
14
  private
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # <%= @user_class %> — a human who signs into this app. One row per Cognito user.
4
+ #
5
+ # Cognito owns authentication (passwords, MFA, the `admins` group). `cognito_authenticatable`
6
+ # supplies everything derived from a token — the `sub` as primary key, email/name/role,
7
+ # just-in-time provisioning, an EmailIndex GSI. Run `belt explain authentication` for the
8
+ # full list.
9
+ #
10
+ # Add your own domain below: associations, per-tenant roles, whatever this app needs to
11
+ # know about a person that Cognito doesn't.
12
+ class <%= @user_class %> < ApplicationRecord
13
+ cognito_authenticatable
14
+
15
+ # has_many :memberships, foreign_key: 'cognito_sub', index: 'CognitoIndex'
16
+
17
+ # Runs whenever an identity is resolved from a token — first sight and every request
18
+ # after. Override to bind pending invitations, seed a workspace, and so on.
19
+ # def after_cognito_sync
20
+ # end
21
+ end
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.44
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stowzilla
@@ -102,6 +102,13 @@ files:
102
102
  - lib/belt/action_router.rb
103
103
  - lib/belt/assets/belt-default.jpg
104
104
  - lib/belt/assets/welcome.css
105
+ - lib/belt/authentication.rb
106
+ - lib/belt/authentication/claims.rb
107
+ - lib/belt/authentication/cognito_authenticatable.rb
108
+ - lib/belt/authentication/cognito_authenticatable/class_methods.rb
109
+ - lib/belt/authentication/configuration.rb
110
+ - lib/belt/authentication/controller.rb
111
+ - lib/belt/authentication/model_macro.rb
105
112
  - lib/belt/cli.rb
106
113
  - lib/belt/cli/apex_dns_sync.rb
107
114
  - lib/belt/cli/app_detection.rb
@@ -149,6 +156,7 @@ files:
149
156
  - lib/belt/cli/zip_artifact_builder.rb
150
157
  - lib/belt/configuration.rb
151
158
  - lib/belt/controllers/welcome_controller.rb
159
+ - lib/belt/docs/authentication.md
152
160
  - lib/belt/docs/backups.md
153
161
  - lib/belt/docs/console.md
154
162
  - lib/belt/docs/controllers.md
@@ -161,6 +169,7 @@ files:
161
169
  - lib/belt/docs/plugins.md
162
170
  - lib/belt/docs/routing.md
163
171
  - lib/belt/docs/structure.md
172
+ - lib/belt/errors.rb
164
173
  - lib/belt/helpers/cors_origin.rb
165
174
  - lib/belt/helpers/error_logging.rb
166
175
  - lib/belt/helpers/response.rb
@@ -207,6 +216,7 @@ files:
207
216
  - lib/templates/generate/auth/frontend/apiClient.js
208
217
  - lib/templates/generate/auth/frontend/auth.css
209
218
  - lib/templates/generate/auth/frontend/auth.js
219
+ - lib/templates/generate/auth/user_model.rb.erb
210
220
  - lib/templates/generate/controller.rb.erb
211
221
  - lib/templates/generate/model.rb.erb
212
222
  - lib/templates/module/dns.tf.erb