rhales 0.3.0 → 0.5.3

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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/.github/renovate.json5 +52 -0
  3. data/.github/workflows/ci.yml +123 -0
  4. data/.github/workflows/claude-code-review.yml +69 -0
  5. data/.github/workflows/claude.yml +49 -0
  6. data/.github/workflows/code-smells.yml +146 -0
  7. data/.github/workflows/ruby-lint.yml +78 -0
  8. data/.github/workflows/yardoc.yml +126 -0
  9. data/.gitignore +55 -0
  10. data/.pr_agent.toml +63 -0
  11. data/.pre-commit-config.yaml +89 -0
  12. data/.prettierignore +8 -0
  13. data/.prettierrc +38 -0
  14. data/.reek.yml +98 -0
  15. data/.rubocop.yml +428 -0
  16. data/.serena/.gitignore +3 -0
  17. data/.yardopts +56 -0
  18. data/CHANGELOG.md +44 -0
  19. data/CLAUDE.md +1 -2
  20. data/Gemfile +29 -0
  21. data/Gemfile.lock +189 -0
  22. data/README.md +706 -589
  23. data/Rakefile +46 -0
  24. data/debug_context.rb +25 -0
  25. data/demo/rhales-roda-demo/.gitignore +7 -0
  26. data/demo/rhales-roda-demo/Gemfile +32 -0
  27. data/demo/rhales-roda-demo/Gemfile.lock +151 -0
  28. data/demo/rhales-roda-demo/MAIL.md +405 -0
  29. data/demo/rhales-roda-demo/README.md +376 -0
  30. data/demo/rhales-roda-demo/RODA-TEMPLATE-ENGINES.md +192 -0
  31. data/demo/rhales-roda-demo/Rakefile +49 -0
  32. data/demo/rhales-roda-demo/app.rb +325 -0
  33. data/demo/rhales-roda-demo/bin/rackup +26 -0
  34. data/demo/rhales-roda-demo/config.ru +13 -0
  35. data/demo/rhales-roda-demo/db/migrate/001_create_rodauth_tables.rb +266 -0
  36. data/demo/rhales-roda-demo/db/migrate/002_create_rodauth_password_tables.rb +79 -0
  37. data/demo/rhales-roda-demo/db/migrate/003_add_admin_account.rb +68 -0
  38. data/demo/rhales-roda-demo/templates/change_login.rue +31 -0
  39. data/demo/rhales-roda-demo/templates/change_password.rue +36 -0
  40. data/demo/rhales-roda-demo/templates/close_account.rue +31 -0
  41. data/demo/rhales-roda-demo/templates/create_account.rue +40 -0
  42. data/demo/rhales-roda-demo/templates/dashboard.rue +150 -0
  43. data/demo/rhales-roda-demo/templates/home.rue +78 -0
  44. data/demo/rhales-roda-demo/templates/layouts/main.rue +168 -0
  45. data/demo/rhales-roda-demo/templates/login.rue +65 -0
  46. data/demo/rhales-roda-demo/templates/logout.rue +25 -0
  47. data/demo/rhales-roda-demo/templates/reset_password.rue +26 -0
  48. data/demo/rhales-roda-demo/templates/verify_account.rue +27 -0
  49. data/demo/rhales-roda-demo/test_full_output.rb +27 -0
  50. data/demo/rhales-roda-demo/test_simple.rb +24 -0
  51. data/docs/.gitignore +9 -0
  52. data/docs/architecture/data-flow.md +499 -0
  53. data/examples/dashboard-with-charts.rue +271 -0
  54. data/examples/form-with-validation.rue +180 -0
  55. data/examples/simple-page.rue +61 -0
  56. data/examples/vue.rue +136 -0
  57. data/generate-json-schemas.ts +158 -0
  58. data/json_schemer_migration_summary.md +172 -0
  59. data/lib/rhales/adapters/base_auth.rb +2 -0
  60. data/lib/rhales/adapters/base_request.rb +2 -0
  61. data/lib/rhales/adapters/base_session.rb +2 -0
  62. data/lib/rhales/adapters.rb +7 -0
  63. data/lib/rhales/configuration.rb +161 -1
  64. data/lib/rhales/core/context.rb +354 -0
  65. data/lib/rhales/{rue_document.rb → core/rue_document.rb} +59 -43
  66. data/lib/rhales/{template_engine.rb → core/template_engine.rb} +80 -33
  67. data/lib/rhales/core/view.rb +529 -0
  68. data/lib/rhales/{view_composition.rb → core/view_composition.rb} +81 -9
  69. data/lib/rhales/core.rb +9 -0
  70. data/lib/rhales/errors/hydration_collision_error.rb +2 -0
  71. data/lib/rhales/errors.rb +2 -0
  72. data/lib/rhales/hydration/earliest_injection_detector.rb +153 -0
  73. data/lib/rhales/hydration/hydration_data_aggregator.rb +487 -0
  74. data/lib/rhales/hydration/hydration_endpoint.rb +215 -0
  75. data/lib/rhales/hydration/hydration_injector.rb +175 -0
  76. data/lib/rhales/{hydration_registry.rb → hydration/hydration_registry.rb} +2 -0
  77. data/lib/rhales/hydration/hydrator.rb +102 -0
  78. data/lib/rhales/hydration/link_based_injection_detector.rb +195 -0
  79. data/lib/rhales/hydration/mount_point_detector.rb +109 -0
  80. data/lib/rhales/hydration/safe_injection_validator.rb +103 -0
  81. data/lib/rhales/hydration.rb +13 -0
  82. data/lib/rhales/{refinements → integrations/refinements}/require_refinements.rb +7 -13
  83. data/lib/rhales/{tilt.rb → integrations/tilt.rb} +26 -18
  84. data/lib/rhales/integrations.rb +6 -0
  85. data/lib/rhales/middleware/json_responder.rb +191 -0
  86. data/lib/rhales/middleware/schema_validator.rb +300 -0
  87. data/lib/rhales/middleware.rb +6 -0
  88. data/lib/rhales/parsers/handlebars_parser.rb +2 -0
  89. data/lib/rhales/parsers/rue_format_parser.rb +55 -36
  90. data/lib/rhales/parsers.rb +9 -0
  91. data/lib/rhales/{csp.rb → security/csp.rb} +27 -3
  92. data/lib/rhales/utils/json_serializer.rb +114 -0
  93. data/lib/rhales/utils/logging_helpers.rb +75 -0
  94. data/lib/rhales/utils/schema_extractor.rb +132 -0
  95. data/lib/rhales/utils/schema_generator.rb +194 -0
  96. data/lib/rhales/utils.rb +40 -0
  97. data/lib/rhales/version.rb +5 -1
  98. data/lib/rhales.rb +47 -19
  99. data/lib/tasks/rhales_schema.rake +197 -0
  100. data/package.json +10 -0
  101. data/pnpm-lock.yaml +345 -0
  102. data/pnpm-workspace.yaml +2 -0
  103. data/proofs/error_handling.rb +79 -0
  104. data/proofs/expanded_object_inheritance.rb +82 -0
  105. data/proofs/partial_context_scoping_fix.rb +168 -0
  106. data/proofs/ui_context_partial_inheritance.rb +236 -0
  107. data/rhales.gemspec +14 -6
  108. data/schema_vs_data_comparison.md +254 -0
  109. data/test_direct_access.rb +36 -0
  110. metadata +142 -18
  111. data/CLAUDE.locale.txt +0 -7
  112. data/lib/rhales/context.rb +0 -240
  113. data/lib/rhales/hydration_data_aggregator.rb +0 -220
  114. data/lib/rhales/hydrator.rb +0 -141
  115. data/lib/rhales/parsers/handlebars-grammar-review.txt +0 -39
  116. data/lib/rhales/view.rb +0 -412
@@ -0,0 +1,325 @@
1
+ # demo/rhales-roda-demo/app.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ require 'logger'
6
+ require 'roda'
7
+ require 'sequel'
8
+ require 'securerandom'
9
+ require 'bcrypt'
10
+ require 'rack/session'
11
+
12
+ # Add the lib directory to the load path
13
+ $:.unshift(File.expand_path('../../lib', __dir__))
14
+ require 'rhales'
15
+ require 'rhales/integrations/tilt'
16
+ require 'mail'
17
+
18
+ Mail.defaults do
19
+ delivery_method :smtp, {
20
+ address: 'localhost',
21
+ port: 1025,
22
+ domain: 'localhost.localdomain',
23
+ enable_starttls_auto: false,
24
+ }
25
+ end
26
+
27
+ class RhalesDemo < Roda
28
+ # Demo accounts for testing - matches migration seed data
29
+ DEMO_ACCOUNTS = [
30
+ {
31
+ email: 'demo@example.com',
32
+ password: 'demo123',
33
+ role: 'user',
34
+ },
35
+ {
36
+ email: 'admin@example.com',
37
+ password: 'admin123',
38
+ role: 'admin',
39
+ },
40
+ ].freeze
41
+
42
+ # Database setup - use file-based SQLite for persistence
43
+ DB = Sequel.sqlite(File.join(__dir__, 'db', 'demo.db'))
44
+
45
+ DB.extension :date_arithmetic
46
+
47
+ logger = Logger.new($stdout)
48
+
49
+ class << self
50
+ def get_secret
51
+ secret = DB[:_demo_secrets].get(:value) # `get` automatically gets the first row
52
+
53
+ if secret.nil?
54
+ secret = SecureRandom.hex(64)
55
+ DB[:_demo_secrets].insert_conflict.insert(
56
+ name: 'migration-default',
57
+ value: secret,
58
+ )
59
+ end
60
+
61
+ secret
62
+ end
63
+ end
64
+
65
+ # Run migrations if needed
66
+ Sequel.extension :migration
67
+ Sequel::Migrator.run(DB, File.join(__dir__, 'db', 'migrate'))
68
+
69
+ secret_value = RhalesDemo.get_secret
70
+ logger.info("[demo] Secret value: #{secret_value}")
71
+
72
+ opts[:root] = File.dirname(__FILE__)
73
+
74
+ # Configure Rhales with Tilt integration
75
+ Rhales.configure do |config|
76
+ config.template_paths = [File.join(opts[:root], 'templates')]
77
+ config.cache_templates = false
78
+ end
79
+
80
+ # Use Roda's render plugin with Rhales engine
81
+ # Note: Layout handling is done by Rhales ViewComposition, not Roda
82
+ plugin :render,
83
+ engine: 'rue',
84
+ views: File.join(opts[:root], 'templates'),
85
+ layout: true
86
+
87
+ plugin :flash
88
+ plugin :sessions, secret: secret_value, key: 'rhales-demo.session'
89
+ plugin :route_csrf
90
+
91
+ # Simple Rodauth configuration
92
+ plugin :rodauth do
93
+ db DB
94
+
95
+ # Used for HMAC operations in various Rodauth features like password reset
96
+ # tokens, email verification, etc. If it changes, existing tokens become
97
+ # invalid (users lose pending password resets, etc).
98
+ # e.g. SecureRandom.hex(64)
99
+ hmac_secret ENV['RODAUTH_HMAC_SECRET'] || secret_value
100
+
101
+ enable :change_login, :change_password, :close_account, :create_account,
102
+ :lockout, :login, :logout, :remember, :reset_password, :verify_account,
103
+ :otp_modify_email, :otp_lockout_email, :recovery_codes, :sms_codes,
104
+ :disallow_password_reuse, :password_grace_period, :active_sessions,
105
+ :verify_login_change, :change_password_notify, :confirm_password,
106
+ :email_auth, :disallow_common_passwords
107
+
108
+ login_redirect '/'
109
+ logout_redirect '/'
110
+ create_account_redirect '/'
111
+
112
+ # Set custom routes to match our templates
113
+ create_account_route 'register'
114
+
115
+ # Skip status checks for demo simplicity
116
+ skip_status_checks? true
117
+
118
+ # Use email as login - param name should match form field
119
+ login_param 'login'
120
+ login_confirm_param 'login'
121
+
122
+ # The following hooks are kept to document their availability and naming.
123
+ # They can be implemented with custom logic as needed.
124
+ # before_login
125
+ # before_create_account
126
+ # after_login_failure
127
+ # after_create_account
128
+ # login_error_flash
129
+ # create_account_error_flash
130
+ # account_from_login
131
+ # password_match?
132
+
133
+ # AVAILABLE VARIABLES FOR ALL RODAUTH VIEWS:
134
+ # Global (auto-injected by plugin):
135
+ # - rodauth.* : Full Rodauth object (csrf_tag, logged_in?, etc.)
136
+ # - flash_notice : Success message from flash[:notice]
137
+ # - flash_error : Error message from flash[:error]
138
+ # - current_path : Current URL path
139
+ # - request_method : HTTP method
140
+ # - demo_accounts : Demo credentials array
141
+ #
142
+ # View-specific variables available via rodauth object:
143
+ # - login.rue: rodauth.login, rodauth.login_error_flash
144
+ # - create_account.rue: rodauth.login_confirm, rodauth.create_account_error_flash
145
+ # - verify_account.rue: rodauth.verify_account_key_value
146
+ # - change_login.rue: rodauth.login, rodauth.login_confirm
147
+ # - change_password.rue: rodauth.new_password_param, rodauth.password_confirm_param
148
+ # - reset_password.rue: rodauth.reset_password_key_value
149
+ # - close_account.rue: (requires current password confirmation)
150
+ # - logout.rue: (minimal data, typically POST-only)
151
+ #
152
+ # Additional enabled features (templates auto-created if needed):
153
+ # - lockout.rue: rodauth.lockout_error_flash (account lockout after failed attempts)
154
+ # - remember.rue: rodauth.remember_param (remember login checkbox)
155
+ # - verify_login_change.rue: rodauth.verify_login_change_key_value (email change verification)
156
+ # - change_password_notify.rue: (notification after password change)
157
+ # - confirm_password.rue: rodauth.password_param (password confirmation for sensitive operations)
158
+ # - email_auth.rue: rodauth.email_auth_key_value (passwordless email authentication)
159
+ # - recovery_codes.rue: rodauth.recovery_codes (backup 2FA codes)
160
+ # - sms_codes.rue: rodauth.sms_phone, rodauth.sms_code (SMS 2FA)
161
+ # - otp_modify_email.rue: rodauth.otp_* (TOTP setup/modification)
162
+ # - active_sessions.rue: rodauth.active_sessions (manage multiple login sessions)
163
+ #
164
+ # Templates automatically discovered in templates/ directory:
165
+ # login.rue, create_account.rue, logout.rue, verify_account.rue,
166
+ # change_login.rue, change_password.rue, reset_password.rue, close_account.rue
167
+ end
168
+
169
+ # Simple auth helper - uses Rodauth's session management
170
+ def current_user
171
+ return nil unless rodauth.logged_in?
172
+
173
+ @current_user ||= DB[:accounts].where(id: rodauth.session_value).first
174
+ end
175
+
176
+ def roda_secret
177
+ @roda_secret ||= RhalesDemo.get_secret
178
+ end
179
+
180
+ def logged_in?
181
+ rodauth.logged_in?
182
+ end
183
+
184
+ # Set CSP header using upstream Rhales functionality
185
+ def set_csp_header
186
+ # Get CSP header from request env (set by Rhales view rendering)
187
+ csp_header = request.env['csp_header']
188
+ response.headers['Content-Security-Policy'] = csp_header if csp_header
189
+ end
190
+
191
+ route do |r|
192
+ r.rodauth
193
+
194
+ # Home route - shows different content based on auth state
195
+ r.root do
196
+ result = if logged_in?
197
+ locals = {
198
+ 'welcome_message' => "Welcome back, #{current_user[:email]}!",
199
+ 'login_time' => Time.now.strftime('%Y-%m-%d %H:%M:%S'),
200
+ # Dashboard specific props
201
+ 'features' => [
202
+ {
203
+ 'title' => 'Authenticated Access',
204
+ 'description' => 'Only visible when logged in',
205
+ 'icon' => '🔐',
206
+ },
207
+ {
208
+ 'title' => 'Session Management',
209
+ 'description' => 'Powered by Rodauth',
210
+ 'icon' => '👤',
211
+ },
212
+ {
213
+ 'title' => 'API Integration',
214
+ 'description' => 'Fetch data with hydrated endpoints',
215
+ 'icon' => '⚡',
216
+ },
217
+ ],
218
+ 'api_endpoints' => {
219
+ 'user' => '/api/user',
220
+ 'demo_data' => '/api/demo-data',
221
+ },
222
+ }
223
+ view('dashboard',
224
+ locals: template_locals(locals),
225
+ layout: false,
226
+ )
227
+ else
228
+ # Home page props
229
+ locals = {
230
+ 'page_type' => 'home',
231
+ 'features' => [
232
+ {
233
+ 'title' => 'Single File Components',
234
+ 'description' => 'Combine templates, data, and logic in one file',
235
+ 'icon' => '📦',
236
+ },
237
+ {
238
+ 'title' => 'Type-Safe Hydration',
239
+ 'description' => 'Zod schemas ensure contract safety',
240
+ 'icon' => '🛡️',
241
+ },
242
+ {
243
+ 'title' => 'Security First',
244
+ 'description' => 'CSP nonces and HTML escaping by default',
245
+ 'icon' => '🔒',
246
+ },
247
+ {
248
+ 'title' => 'Framework Agnostic',
249
+ 'description' => 'Works with Roda, Sinatra, Rails, and more',
250
+ 'icon' => '🔧',
251
+ },
252
+ ],
253
+ }
254
+ view('home', locals: template_locals(locals), layout: false)
255
+ end
256
+
257
+ # Set CSP header after view rendering
258
+ set_csp_header
259
+ result
260
+ end
261
+
262
+ # Simple API endpoint for RSFC hydration demo
263
+ r.get 'api/user' do
264
+ response['Content-Type'] = 'application/json'
265
+
266
+ if logged_in?
267
+ {
268
+ authenticated: true,
269
+ user: current_user,
270
+ server_time: Time.now.iso8601,
271
+ }.to_json
272
+ else
273
+ { authenticated: false }.to_json
274
+ end
275
+ end
276
+
277
+ # Demo data endpoint
278
+ r.get 'api/demo-data' do
279
+ response['Content-Type'] = 'application/json'
280
+ {
281
+ message: 'This data was loaded dynamically via JavaScript!',
282
+ timestamp: Time.now.to_i,
283
+ random_number: rand(1000),
284
+ }.to_json
285
+ end
286
+ end
287
+
288
+ # Helper method to provide common template data
289
+ # Returns hash with :client_data and :server_data keys for Rhales v2.0+
290
+ def template_locals(additional_locals = {})
291
+ # Separate client (serialized to browser) from server (template-only) data
292
+ client_defaults = {
293
+ # Authentication state
294
+ 'authenticated' => respond_to?(:logged_in?) ? logged_in? : false,
295
+
296
+ # Demo accounts for login page
297
+ 'demo_accounts' => DEMO_ACCOUNTS,
298
+ }
299
+
300
+ server_defaults = {
301
+ # Layout props (required by layouts/main.rue)
302
+ 'app_name' => 'Rhales Demo',
303
+ 'year' => Time.now.year,
304
+
305
+ # Flash messages (already handled by Tilt, but keeping for consistency)
306
+ 'flash_notice' => respond_to?(:flash) ? flash['notice'] : nil,
307
+ 'flash_error' => respond_to?(:flash) ? flash['error'] : nil,
308
+ }
309
+
310
+ # Merge additional locals
311
+ client_data = client_defaults.merge(additional_locals.fetch('client_data', {}))
312
+ server_data = server_defaults.merge(additional_locals.fetch('server_data', {}))
313
+
314
+ # Also merge any top-level keys into client for backward compatibility
315
+ additional_locals.each do |key, value|
316
+ next if key == 'client_data' || key == 'server_data'
317
+ client_data[key] = value
318
+ end
319
+
320
+ {
321
+ client_data: client_data,
322
+ server_data: server_data,
323
+ }
324
+ end
325
+ end
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ #
4
+ # This file was generated by Bundler.
5
+ #
6
+ # The application 'rackup' is installed as part of a gem, and
7
+ # this file is here to facilitate running it.
8
+ #
9
+
10
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
11
+
12
+ bundle_binstub = File.expand_path("bundle", __dir__)
13
+
14
+ if File.file?(bundle_binstub)
15
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
16
+ load(bundle_binstub)
17
+ else
18
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
19
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
20
+ end
21
+ end
22
+
23
+ require "rubygems"
24
+ require "bundler/setup"
25
+
26
+ load Gem.bin_path("rackup", "rackup")
@@ -0,0 +1,13 @@
1
+ require_relative 'app'
2
+
3
+ # Add the lib directory to the load path for middleware
4
+ $:.unshift(File.expand_path('../../lib', __dir__))
5
+ require 'rhales/middleware/json_responder'
6
+
7
+ # Enable JSON responses for API clients
8
+ # When Accept: application/json header is present, return hydration data as JSON
9
+ use Rhales::Middleware::JsonResponder,
10
+ enabled: true,
11
+ include_metadata: ENV['RACK_ENV'] == 'development'
12
+
13
+ run RhalesDemo.freeze.app
@@ -0,0 +1,266 @@
1
+ # demo/rhales-roda-demo/db/migrate/001_create_rodauth_tables.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ Sequel.migration do
6
+ up do
7
+ primary_key_type = ENV['RODAUTH_SPEC_UUID'] && database_type == :postgres ? :uuid : :bigint
8
+
9
+ extension :date_arithmetic
10
+
11
+ # Used by the account verification and close account features
12
+ create_table(:account_statuses) do
13
+ Integer :id, primary_key: true
14
+ String :name, null: false, unique: true
15
+ end
16
+ from(:account_statuses).import([:id, :name], [[1, 'Unverified'], [2, 'Verified'], [3, 'Closed']])
17
+
18
+ db = self
19
+ create_table(:accounts) do
20
+ if primary_key_type == :uuid
21
+ uuid :id, primary_key: true, default: Sequel.function(:gen_random_uuid)
22
+ else
23
+ primary_key :id, type: :Bignum
24
+ end
25
+ foreign_key :status_id, :account_statuses, null: false, default: 1
26
+ if db.database_type == :postgres
27
+ citext :email, null: false
28
+ constraint :valid_email, email: /^[^,;@ \r\n]+@[^,@; \r\n]+\.[^,@; \r\n]+$/
29
+ else
30
+ String :email, null: false
31
+ end
32
+ if db.supports_partial_indexes?
33
+ index :email, unique: true, where: { status_id: [1, 2] }
34
+ else
35
+ index :email, unique: true
36
+ end
37
+ end
38
+
39
+ deadline_opts = proc do |days|
40
+ if database_type == :mysql
41
+ { null: false }
42
+ else
43
+ { null: false, default: Sequel.date_add(Sequel::CURRENT_TIMESTAMP, days: days) }
44
+ end
45
+ end
46
+
47
+ # Used by the audit logging feature
48
+ json_type = case database_type
49
+ when :postgres
50
+ :jsonb
51
+ when :sqlite, :mysql
52
+ :json
53
+ else
54
+ String
55
+ end
56
+ create_table(:account_authentication_audit_logs) do
57
+ if primary_key_type == :uuid
58
+ uuid :id, primary_key: true, default: Sequel.function(:gen_random_uuid)
59
+ else
60
+ primary_key :id, type: :Bignum
61
+ end
62
+ foreign_key :account_id, :accounts, null: false, type: primary_key_type
63
+ DateTime :at, null: false, default: Sequel::CURRENT_TIMESTAMP
64
+ String :message, null: false
65
+ column :metadata, json_type
66
+ index [:account_id, :at], name: :audit_account_at_idx
67
+ index :at, name: :audit_at_idx
68
+ end
69
+
70
+ # Used by the password reset feature
71
+ create_table(:account_password_reset_keys) do
72
+ foreign_key :id, :accounts, primary_key: true, type: primary_key_type
73
+ String :key, null: false
74
+ DateTime :deadline, deadline_opts[1]
75
+ DateTime :email_last_sent, null: false, default: Sequel::CURRENT_TIMESTAMP
76
+ end
77
+
78
+ # Used by the jwt refresh feature
79
+ create_table(:account_jwt_refresh_keys) do
80
+ if primary_key_type == :uuid
81
+ uuid :id, primary_key: true, default: Sequel.function(:gen_random_uuid)
82
+ else
83
+ primary_key :id, type: :Bignum
84
+ end
85
+ foreign_key :account_id, :accounts, null: false, type: primary_key_type
86
+ String :key, null: false
87
+ DateTime :deadline, deadline_opts[1]
88
+ index :account_id, name: :account_jwt_rk_account_id_idx
89
+ end
90
+
91
+ # Used by the account verification feature
92
+ create_table(:account_verification_keys) do
93
+ foreign_key :id, :accounts, primary_key: true, type: primary_key_type
94
+ String :key, null: false
95
+ DateTime :requested_at, null: false, default: Sequel::CURRENT_TIMESTAMP
96
+ DateTime :email_last_sent, null: false, default: Sequel::CURRENT_TIMESTAMP
97
+ end
98
+
99
+ # Used by the verify login change feature
100
+ create_table(:account_login_change_keys) do
101
+ foreign_key :id, :accounts, primary_key: true, type: primary_key_type
102
+ String :key, null: false
103
+ String :login, null: false
104
+ DateTime :deadline, deadline_opts[1]
105
+ end
106
+
107
+ # Used by the remember me feature
108
+ create_table(:account_remember_keys) do
109
+ foreign_key :id, :accounts, primary_key: true, type: primary_key_type
110
+ String :key, null: false
111
+ DateTime :deadline, deadline_opts[14]
112
+ end
113
+
114
+ # Used by the lockout feature
115
+ create_table(:account_login_failures) do
116
+ foreign_key :id, :accounts, primary_key: true, type: primary_key_type
117
+ Integer :number, null: false, default: 1
118
+ end
119
+ create_table(:account_lockouts) do
120
+ foreign_key :id, :accounts, primary_key: true, type: primary_key_type
121
+ String :key, null: false
122
+ DateTime :deadline, deadline_opts[1]
123
+ DateTime :email_last_sent
124
+ end
125
+
126
+ # Used by the email auth feature
127
+ create_table(:account_email_auth_keys) do
128
+ foreign_key :id, :accounts, primary_key: true, type: primary_key_type
129
+ String :key, null: false
130
+ DateTime :deadline, deadline_opts[1]
131
+ DateTime :email_last_sent, null: false, default: Sequel::CURRENT_TIMESTAMP
132
+ end
133
+
134
+ # Used by the password expiration feature
135
+ create_table(:account_password_change_times) do
136
+ foreign_key :id, :accounts, primary_key: true, type: primary_key_type
137
+ DateTime :changed_at, null: false, default: Sequel::CURRENT_TIMESTAMP
138
+ end
139
+
140
+ # Used by the account expiration feature
141
+ create_table(:account_activity_times) do
142
+ foreign_key :id, :accounts, primary_key: true, type: primary_key_type
143
+ DateTime :last_activity_at, null: false
144
+ DateTime :last_login_at, null: false
145
+ DateTime :expired_at
146
+ end
147
+
148
+ # Used by the single session feature
149
+ create_table(:account_session_keys) do
150
+ foreign_key :id, :accounts, primary_key: true, type: primary_key_type
151
+ String :key, null: false
152
+ end
153
+
154
+ # Used by the active sessions feature
155
+ create_table(:account_active_session_keys) do
156
+ foreign_key :account_id, :accounts, type: primary_key_type
157
+ String :session_id
158
+ Time :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP
159
+ Time :last_use, null: false, default: Sequel::CURRENT_TIMESTAMP
160
+ primary_key [:account_id, :session_id]
161
+ end
162
+
163
+ # Used by the webauthn feature
164
+ create_table(:account_webauthn_user_ids) do
165
+ foreign_key :id, :accounts, primary_key: true, type: primary_key_type
166
+ String :webauthn_id, null: false
167
+ end
168
+ create_table(:account_webauthn_keys) do
169
+ foreign_key :account_id, :accounts, type: primary_key_type
170
+ String :webauthn_id
171
+ String :public_key, null: false
172
+ Integer :sign_count, null: false
173
+ Time :last_use, null: false, default: Sequel::CURRENT_TIMESTAMP
174
+ primary_key [:account_id, :webauthn_id]
175
+ end
176
+
177
+ # Used by the otp feature
178
+ create_table(:account_otp_keys) do
179
+ foreign_key :id, :accounts, primary_key: true, type: primary_key_type
180
+ String :key, null: false
181
+ Integer :num_failures, null: false, default: 0
182
+ Time :last_use, null: false, default: Sequel::CURRENT_TIMESTAMP
183
+ end
184
+
185
+ # Used by the otp_unlock feature
186
+ create_table(:account_otp_unlocks) do
187
+ foreign_key :id, :accounts, primary_key: true, type: primary_key_type
188
+ Integer :num_successes, null: false, default: 1
189
+ Time :next_auth_attempt_after, null: false, default: Sequel::CURRENT_TIMESTAMP
190
+ end
191
+
192
+ # Used by the recovery codes feature
193
+ create_table(:account_recovery_codes) do
194
+ foreign_key :id, :accounts, type: primary_key_type
195
+ String :code
196
+ primary_key [:id, :code]
197
+ end
198
+
199
+ # Used by the sms codes feature
200
+ create_table(:account_sms_codes) do
201
+ foreign_key :id, :accounts, primary_key: true, type: primary_key_type
202
+ String :phone_number, null: false
203
+ Integer :num_failures
204
+ String :code
205
+ DateTime :code_issued_at, null: false, default: Sequel::CURRENT_TIMESTAMP
206
+ end
207
+
208
+ case database_type
209
+ when :postgres
210
+ user = get(Sequel.lit('current_user')) + '_password'
211
+ run "GRANT REFERENCES ON accounts TO #{user}"
212
+ when :mysql, :mssql
213
+ user = if database_type == :mysql
214
+ get(Sequel.lit('current_user')).sub(/_password@/, '@')
215
+ else
216
+ get(Sequel.function(:DB_NAME))
217
+ end
218
+ run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_statuses TO #{user}"
219
+ run "GRANT SELECT, INSERT, UPDATE, DELETE ON accounts TO #{user}"
220
+ run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_authentication_audit_logs TO #{user}"
221
+ run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_password_reset_keys TO #{user}"
222
+ run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_jwt_refresh_keys TO #{user}"
223
+ run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_verification_keys TO #{user}"
224
+ run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_login_change_keys TO #{user}"
225
+ run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_remember_keys TO #{user}"
226
+ run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_login_failures TO #{user}"
227
+ run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_email_auth_keys TO #{user}"
228
+ run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_lockouts TO #{user}"
229
+ run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_password_change_times TO #{user}"
230
+ run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_activity_times TO #{user}"
231
+ run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_session_keys TO #{user}"
232
+ run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_active_session_keys TO #{user}"
233
+ run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_webauthn_user_ids TO #{user}"
234
+ run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_webauthn_keys TO #{user}"
235
+ run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_otp_keys TO #{user}"
236
+ run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_otp_unlocks TO #{user}"
237
+ run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_recovery_codes TO #{user}"
238
+ run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_sms_codes TO #{user}"
239
+ end
240
+ end
241
+
242
+ down do
243
+ drop_table(:account_sms_codes,
244
+ :account_recovery_codes,
245
+ :account_otp_unlocks,
246
+ :account_otp_keys,
247
+ :account_webauthn_keys,
248
+ :account_webauthn_user_ids,
249
+ :account_active_session_keys,
250
+ :account_session_keys,
251
+ :account_activity_times,
252
+ :account_password_change_times,
253
+ :account_email_auth_keys,
254
+ :account_lockouts,
255
+ :account_login_failures,
256
+ :account_remember_keys,
257
+ :account_login_change_keys,
258
+ :account_verification_keys,
259
+ :account_jwt_refresh_keys,
260
+ :account_password_reset_keys,
261
+ :account_authentication_audit_logs,
262
+ :accounts,
263
+ :account_statuses,
264
+ )
265
+ end
266
+ end