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.
- checksums.yaml +4 -4
- data/.github/renovate.json5 +52 -0
- data/.github/workflows/ci.yml +123 -0
- data/.github/workflows/claude-code-review.yml +69 -0
- data/.github/workflows/claude.yml +49 -0
- data/.github/workflows/code-smells.yml +146 -0
- data/.github/workflows/ruby-lint.yml +78 -0
- data/.github/workflows/yardoc.yml +126 -0
- data/.gitignore +55 -0
- data/.pr_agent.toml +63 -0
- data/.pre-commit-config.yaml +89 -0
- data/.prettierignore +8 -0
- data/.prettierrc +38 -0
- data/.reek.yml +98 -0
- data/.rubocop.yml +428 -0
- data/.serena/.gitignore +3 -0
- data/.yardopts +56 -0
- data/CHANGELOG.md +44 -0
- data/CLAUDE.md +1 -2
- data/Gemfile +29 -0
- data/Gemfile.lock +189 -0
- data/README.md +706 -589
- data/Rakefile +46 -0
- data/debug_context.rb +25 -0
- data/demo/rhales-roda-demo/.gitignore +7 -0
- data/demo/rhales-roda-demo/Gemfile +32 -0
- data/demo/rhales-roda-demo/Gemfile.lock +151 -0
- data/demo/rhales-roda-demo/MAIL.md +405 -0
- data/demo/rhales-roda-demo/README.md +376 -0
- data/demo/rhales-roda-demo/RODA-TEMPLATE-ENGINES.md +192 -0
- data/demo/rhales-roda-demo/Rakefile +49 -0
- data/demo/rhales-roda-demo/app.rb +325 -0
- data/demo/rhales-roda-demo/bin/rackup +26 -0
- data/demo/rhales-roda-demo/config.ru +13 -0
- data/demo/rhales-roda-demo/db/migrate/001_create_rodauth_tables.rb +266 -0
- data/demo/rhales-roda-demo/db/migrate/002_create_rodauth_password_tables.rb +79 -0
- data/demo/rhales-roda-demo/db/migrate/003_add_admin_account.rb +68 -0
- data/demo/rhales-roda-demo/templates/change_login.rue +31 -0
- data/demo/rhales-roda-demo/templates/change_password.rue +36 -0
- data/demo/rhales-roda-demo/templates/close_account.rue +31 -0
- data/demo/rhales-roda-demo/templates/create_account.rue +40 -0
- data/demo/rhales-roda-demo/templates/dashboard.rue +150 -0
- data/demo/rhales-roda-demo/templates/home.rue +78 -0
- data/demo/rhales-roda-demo/templates/layouts/main.rue +168 -0
- data/demo/rhales-roda-demo/templates/login.rue +65 -0
- data/demo/rhales-roda-demo/templates/logout.rue +25 -0
- data/demo/rhales-roda-demo/templates/reset_password.rue +26 -0
- data/demo/rhales-roda-demo/templates/verify_account.rue +27 -0
- data/demo/rhales-roda-demo/test_full_output.rb +27 -0
- data/demo/rhales-roda-demo/test_simple.rb +24 -0
- data/docs/.gitignore +9 -0
- data/docs/architecture/data-flow.md +499 -0
- data/examples/dashboard-with-charts.rue +271 -0
- data/examples/form-with-validation.rue +180 -0
- data/examples/simple-page.rue +61 -0
- data/examples/vue.rue +136 -0
- data/generate-json-schemas.ts +158 -0
- data/json_schemer_migration_summary.md +172 -0
- data/lib/rhales/adapters/base_auth.rb +2 -0
- data/lib/rhales/adapters/base_request.rb +2 -0
- data/lib/rhales/adapters/base_session.rb +2 -0
- data/lib/rhales/adapters.rb +7 -0
- data/lib/rhales/configuration.rb +161 -1
- data/lib/rhales/core/context.rb +354 -0
- data/lib/rhales/{rue_document.rb → core/rue_document.rb} +59 -43
- data/lib/rhales/{template_engine.rb → core/template_engine.rb} +80 -33
- data/lib/rhales/core/view.rb +529 -0
- data/lib/rhales/{view_composition.rb → core/view_composition.rb} +81 -9
- data/lib/rhales/core.rb +9 -0
- data/lib/rhales/errors/hydration_collision_error.rb +2 -0
- data/lib/rhales/errors.rb +2 -0
- data/lib/rhales/hydration/earliest_injection_detector.rb +153 -0
- data/lib/rhales/hydration/hydration_data_aggregator.rb +487 -0
- data/lib/rhales/hydration/hydration_endpoint.rb +215 -0
- data/lib/rhales/hydration/hydration_injector.rb +175 -0
- data/lib/rhales/{hydration_registry.rb → hydration/hydration_registry.rb} +2 -0
- data/lib/rhales/hydration/hydrator.rb +102 -0
- data/lib/rhales/hydration/link_based_injection_detector.rb +195 -0
- data/lib/rhales/hydration/mount_point_detector.rb +109 -0
- data/lib/rhales/hydration/safe_injection_validator.rb +103 -0
- data/lib/rhales/hydration.rb +13 -0
- data/lib/rhales/{refinements → integrations/refinements}/require_refinements.rb +7 -13
- data/lib/rhales/{tilt.rb → integrations/tilt.rb} +26 -18
- data/lib/rhales/integrations.rb +6 -0
- data/lib/rhales/middleware/json_responder.rb +191 -0
- data/lib/rhales/middleware/schema_validator.rb +300 -0
- data/lib/rhales/middleware.rb +6 -0
- data/lib/rhales/parsers/handlebars_parser.rb +2 -0
- data/lib/rhales/parsers/rue_format_parser.rb +55 -36
- data/lib/rhales/parsers.rb +9 -0
- data/lib/rhales/{csp.rb → security/csp.rb} +27 -3
- data/lib/rhales/utils/json_serializer.rb +114 -0
- data/lib/rhales/utils/logging_helpers.rb +75 -0
- data/lib/rhales/utils/schema_extractor.rb +132 -0
- data/lib/rhales/utils/schema_generator.rb +194 -0
- data/lib/rhales/utils.rb +40 -0
- data/lib/rhales/version.rb +5 -1
- data/lib/rhales.rb +47 -19
- data/lib/tasks/rhales_schema.rake +197 -0
- data/package.json +10 -0
- data/pnpm-lock.yaml +345 -0
- data/pnpm-workspace.yaml +2 -0
- data/proofs/error_handling.rb +79 -0
- data/proofs/expanded_object_inheritance.rb +82 -0
- data/proofs/partial_context_scoping_fix.rb +168 -0
- data/proofs/ui_context_partial_inheritance.rb +236 -0
- data/rhales.gemspec +14 -6
- data/schema_vs_data_comparison.md +254 -0
- data/test_direct_access.rb +36 -0
- metadata +142 -18
- data/CLAUDE.locale.txt +0 -7
- data/lib/rhales/context.rb +0 -240
- data/lib/rhales/hydration_data_aggregator.rb +0 -220
- data/lib/rhales/hydrator.rb +0 -141
- data/lib/rhales/parsers/handlebars-grammar-review.txt +0 -39
- 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
|