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,79 @@
1
+ # demo/rhales-roda-demo/db/migrate/002_create_rodauth_password_tables.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ require 'rodauth/migrations'
6
+
7
+ Sequel.migration do
8
+ up do
9
+ primary_key_type = ENV['RODAUTH_SPEC_UUID'] && database_type == :postgres ? :uuid : :bigint
10
+
11
+ create_table(:account_password_hashes) do
12
+ foreign_key :id, :accounts, primary_key: true, type: primary_key_type
13
+ String :password_hash, null: false
14
+ end
15
+ Rodauth.create_database_authentication_functions(self, argon2: ENV['RODAUTH_NO_ARGON2'] != '1')
16
+ case database_type
17
+ when :postgres
18
+ user = get(Sequel.lit('current_user')).sub(/_password\z/, '')
19
+ run 'REVOKE ALL ON account_password_hashes FROM public'
20
+ run "REVOKE ALL ON FUNCTION rodauth_get_salt(#{primary_key_type}) FROM public"
21
+ run "REVOKE ALL ON FUNCTION rodauth_valid_password_hash(#{primary_key_type}, text) FROM public"
22
+ run "GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{user}"
23
+ run "GRANT SELECT(id) ON account_password_hashes TO #{user}"
24
+ run "GRANT EXECUTE ON FUNCTION rodauth_get_salt(#{primary_key_type}) TO #{user}"
25
+ run "GRANT EXECUTE ON FUNCTION rodauth_valid_password_hash(#{primary_key_type}, text) TO #{user}"
26
+ when :mysql
27
+ user = get(Sequel.lit('current_user')).sub(/_password@/, '@')
28
+ db_name = get(Sequel.function(:database))
29
+ run "GRANT EXECUTE ON #{db_name}.* TO #{user}"
30
+ run "GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{user}"
31
+ run "GRANT SELECT (id) ON account_password_hashes TO #{user}"
32
+ when :mssql
33
+ user = get(Sequel.function(:DB_NAME))
34
+ run "GRANT EXECUTE ON rodauth_get_salt TO #{user}"
35
+ run "GRANT EXECUTE ON rodauth_valid_password_hash TO #{user}"
36
+ run "GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{user}"
37
+ run "GRANT SELECT ON account_password_hashes(id) TO #{user}"
38
+ end
39
+
40
+ # Used by the disallow_password_reuse feature
41
+ create_table(:account_previous_password_hashes) do
42
+ primary_key :id, type: :Bignum
43
+ foreign_key :account_id, :accounts, type: primary_key_type
44
+ String :password_hash, null: false
45
+ end
46
+ Rodauth.create_database_previous_password_check_functions(self, argon2: ENV['RODAUTH_NO_ARGON2'] != '1')
47
+
48
+ case database_type
49
+ when :postgres
50
+ user = get(Sequel.lit('current_user')).sub(/_password\z/, '')
51
+ run 'REVOKE ALL ON account_previous_password_hashes FROM public'
52
+ run 'REVOKE ALL ON FUNCTION rodauth_get_previous_salt(int8) FROM public'
53
+ run 'REVOKE ALL ON FUNCTION rodauth_previous_password_hash_match(int8, text) FROM public'
54
+ run "GRANT INSERT, UPDATE, DELETE ON account_previous_password_hashes TO #{user}"
55
+ run "GRANT SELECT(id, account_id) ON account_previous_password_hashes TO #{user}"
56
+ run "GRANT USAGE ON account_previous_password_hashes_id_seq TO #{user}"
57
+ run "GRANT EXECUTE ON FUNCTION rodauth_get_previous_salt(int8) TO #{user}"
58
+ run "GRANT EXECUTE ON FUNCTION rodauth_previous_password_hash_match(int8, text) TO #{user}"
59
+ when :mysql
60
+ user = get(Sequel.lit('current_user')).sub(/_password@/, '@')
61
+ db_name = get(Sequel.function(:database))
62
+ run "GRANT EXECUTE ON #{db_name}.* TO #{user}"
63
+ run "GRANT INSERT, UPDATE, DELETE ON account_previous_password_hashes TO #{user}"
64
+ run "GRANT SELECT (id, account_id) ON account_previous_password_hashes TO #{user}"
65
+ when :mssql
66
+ user = get(Sequel.function(:DB_NAME))
67
+ run "GRANT EXECUTE ON rodauth_get_previous_salt TO #{user}"
68
+ run "GRANT EXECUTE ON rodauth_previous_password_hash_match TO #{user}"
69
+ run "GRANT INSERT, UPDATE, DELETE ON account_previous_password_hashes TO #{user}"
70
+ run "GRANT SELECT ON account_previous_password_hashes(id, account_id) TO #{user}"
71
+ end
72
+ end
73
+
74
+ down do
75
+ Rodauth.drop_database_previous_password_check_functions(self)
76
+ Rodauth.drop_database_authentication_functions(self)
77
+ drop_table(:account_previous_password_hashes, :account_password_hashes)
78
+ end
79
+ end
@@ -0,0 +1,68 @@
1
+ # demo/rhales-roda-demo/db/migrate/003_add_admin_account.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ require 'bcrypt'
6
+
7
+ Sequel.migration do
8
+ up do
9
+ # Ensure secrets table exists and get/create HMAC secret
10
+ #
11
+ # For the convenience of demo use only. Don't do this in production. It
12
+ # allows for consistent secret to be used between app restarts without
13
+ # having to manually generate and specify an environment variable.
14
+ create_table(:_demo_secrets) do
15
+ primary_key :id
16
+ String :name, unique: true, null: false
17
+ String :value, null: false
18
+ DateTime :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP
19
+ end
20
+
21
+ # Create demo account
22
+ unless from(:accounts).where(email: 'demo@example.com').first
23
+ demo_account_id = from(:accounts).insert(
24
+ email: 'demo@example.com',
25
+ status_id: 2, # Verified status
26
+ )
27
+
28
+ demo_password_hash = BCrypt::Password.create('demo123')
29
+ from(:account_password_hashes).insert(
30
+ id: demo_account_id,
31
+ password_hash: demo_password_hash,
32
+ )
33
+ end
34
+
35
+ # Create admin account
36
+ unless from(:accounts).where(email: 'admin@example.com').first
37
+ admin_account_id = from(:accounts).insert(
38
+ email: 'admin@example.com',
39
+ status_id: 2, # Verified status
40
+ )
41
+
42
+ admin_password_hash = BCrypt::Password.create('admin123')
43
+ from(:account_password_hashes).insert(
44
+ id: admin_account_id,
45
+ password_hash: admin_password_hash,
46
+ )
47
+ end
48
+ end
49
+
50
+ down do
51
+ p '[demo] Removing the following secrets:'
52
+ p from(:_demo_secrets)
53
+
54
+ drop_table(:_demo_secrets)
55
+
56
+ # Remove admin account and password hash
57
+ if admin_account = from(:accounts).where(email: 'admin@example.com').first
58
+ from(:account_password_hashes).where(id: admin_account[:id]).delete
59
+ from(:accounts).where(id: admin_account[:id]).delete
60
+ end
61
+
62
+ # Remove demo account and password hash
63
+ if demo_account = from(:accounts).where(email: 'demo@example.com').first
64
+ from(:account_password_hashes).where(id: demo_account[:id]).delete
65
+ from(:accounts).where(id: demo_account[:id]).delete
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,31 @@
1
+ <!-- demo/rhales-roda-demo/templates/change_login.rue -->
2
+
3
+ <schema lang="js-zod" version="2" envelope="SuccessEnvelope" layout="layouts/main">
4
+ const schema = z.object({
5
+ page_title: z.string(),
6
+ submit_text: z.string(),
7
+ zodauth: z.any()
8
+ });
9
+ </schema>
10
+
11
+ <template>
12
+ <div class="auth-form">
13
+ <h2>{{page_title}}</h2>
14
+
15
+ <form method="post">
16
+ <div class="form-group">
17
+ <label for="login">New Login:</label>
18
+ <input type="text" id="login" name="login" required />
19
+ </div>
20
+
21
+ <div class="form-group">
22
+ <label for="password">Current Password:</label>
23
+ <input type="password" id="password" name="password" required />
24
+ </div>
25
+
26
+ {{{rodauth.csrf_tag}}}
27
+
28
+ <button type="submit" class="btn btn-primary">{{submit_text}}</button>
29
+ </form>
30
+ </div>
31
+ </template>
@@ -0,0 +1,36 @@
1
+ <!-- demo/rhales-roda-demo/templates/change_password.rue -->
2
+
3
+ <schema lang="js-zod" version="2" envelope="SuccessEnvelope" layout="layouts/main">
4
+ const schema = z.object({
5
+ page_title: z.string(),
6
+ submit_text: z.string(),
7
+ zodauth: z.any()
8
+ });
9
+ </schema>
10
+
11
+ <template>
12
+ <div class="auth-form">
13
+ <h2>{{page_title}}</h2>
14
+
15
+ <form method="post">
16
+ <div class="form-group">
17
+ <label for="password">Current Password:</label>
18
+ <input type="password" id="password" name="password" required />
19
+ </div>
20
+
21
+ <div class="form-group">
22
+ <label for="new-password">New Password:</label>
23
+ <input type="password" id="new-password" name="new-password" required />
24
+ </div>
25
+
26
+ <div class="form-group">
27
+ <label for="password-confirm">Confirm New Password:</label>
28
+ <input type="password" id="password-confirm" name="password-confirm" required />
29
+ </div>
30
+
31
+ {{{rodauth.csrf_tag}}}
32
+
33
+ <button type="submit" class="btn btn-primary">{{submit_text}}</button>
34
+ </form>
35
+ </div>
36
+ </template>
@@ -0,0 +1,31 @@
1
+ <!-- demo/rhales-roda-demo/templates/close_account.rue -->
2
+
3
+ <schema lang="js-zod" version="2" envelope="SuccessEnvelope" layout="layouts/main">
4
+ const schema = z.object({
5
+ page_title: z.string(),
6
+ submit_text: z.string(),
7
+ warning_message: z.string(),
8
+ zodauth: z.any()
9
+ });
10
+ </schema>
11
+
12
+ <template>
13
+ <div class="auth-form">
14
+ <h2>{{page_title}}</h2>
15
+
16
+ <div class="alert alert-warning">
17
+ {{warning_message}}
18
+ </div>
19
+
20
+ <form method="post">
21
+ <div class="form-group">
22
+ <label for="password">Current Password:</label>
23
+ <input type="password" id="password" name="password" required />
24
+ </div>
25
+
26
+ {{{rodauth.csrf_tag}}}
27
+
28
+ <button type="submit" class="btn btn-danger">{{submit_text}}</button>
29
+ </form>
30
+ </div>
31
+ </template>
@@ -0,0 +1,40 @@
1
+ <!-- demo/rhales-roda-demo/templates/create_account.rue -->
2
+
3
+ <schema lang="js-zod" version="2" envelope="SuccessEnvelope" layout="layouts/main">
4
+ const schema = z.object({
5
+ title: z.string(),
6
+ fields: z.array(z.object({
7
+ name: z.string(),
8
+ type: z.string(),
9
+ label: z.string(),
10
+ placeholder: z.string()
11
+ })),
12
+ zodauth: z.any()
13
+ });
14
+ </schema>
15
+
16
+ <template>
17
+ <div class="card" style="max-width: 400px; margin: 0 auto;">
18
+ <h2>{{title}}</h2>
19
+
20
+ <form method="post" action="/register">
21
+ {{{rodauth.csrf_tag}}}
22
+
23
+ {{#each fields}}
24
+ <div class="form-group">
25
+ <label for="{{name}}">{{label}}</label>
26
+ <input type="{{type}}" name="{{name}}" id="{{name}}" placeholder="{{placeholder}}" required>
27
+ </div>
28
+ {{/each}}
29
+
30
+ <button type="submit" class="btn">Create Account</button>
31
+ <a href="/login" style="margin-left: 1rem;">Already have an account?</a>
32
+ </form>
33
+ </div>
34
+ </template>
35
+
36
+ <logic>
37
+ # Registration form using field iteration
38
+ # Demonstrates data-driven form generation
39
+ # CSRF protection included
40
+ </logic>
@@ -0,0 +1,150 @@
1
+ <!-- demo/rhales-roda-demo/templates/dashboard.rue -->
2
+
3
+ <schema lang="js-zod" version="2" envelope="SuccessEnvelope" window="userDashboard" layout="layouts/main">
4
+ const schema = z.object({
5
+ welcome_message: z.string(),
6
+ login_time: z.string(),
7
+ features: z.array(z.object({
8
+ title: z.string(),
9
+ description: z.string(),
10
+ icon: z.string()
11
+ })),
12
+ api_endpoints: z.object({
13
+ user: z.string(),
14
+ demo_data: z.string()
15
+ }),
16
+ authenticated: z.boolean()
17
+ });
18
+ </schema>
19
+
20
+ <template>
21
+ <div style="max-width: 800px; margin: 0 auto; padding: 2rem;">
22
+ <h1 style="color: #2c3e50; margin-bottom: 2rem;">🎯 {{welcome_message}}</h1>
23
+
24
+ <div style="background: #e8f5e8; padding: 1.5rem; border-radius: 8px; margin-bottom: 2rem;">
25
+ <h3>✅ Authentication Successful</h3>
26
+ <p><strong>Login Time:</strong> {{login_time}}</p>
27
+ <p>You're now viewing the authenticated section of the Rhales RSFC demo!</p>
28
+ </div>
29
+
30
+ <div style="background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 2rem;">
31
+ <h2>🎯 RSFC Demo Features</h2>
32
+ <p>This page demonstrates Ruby Single File Components with authentication boundaries:</p>
33
+
34
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; margin-top: 2rem;">
35
+ {{#each features}}
36
+ <div style="background: #f8f9fa; padding: 1.5rem; border-radius: 8px; text-align: center;">
37
+ <div style="font-size: 2rem; margin-bottom: 1rem;">{{icon}}</div>
38
+ <h3 style="margin-bottom: 1rem;">{{title}}</h3>
39
+ <p style="color: #666; font-size: 0.9rem;">{{description}}</p>
40
+ </div>
41
+ {{/each}}
42
+ </div>
43
+ </div>
44
+
45
+ <div style="background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 2rem;">
46
+ <h2>⚡ Interactive Demo</h2>
47
+ <p>Click the buttons below to see RSFC hydration and API integration in action:</p>
48
+
49
+ <div style="margin: 1.5rem 0;">
50
+ <button id="fetch-user" style="background: #007bff; color: white; padding: 0.75rem 1.5rem; border: none; border-radius: 4px; margin-right: 1rem; cursor: pointer;">
51
+ Fetch User Data
52
+ </button>
53
+ <button id="fetch-demo" style="background: #28a745; color: white; padding: 0.75rem 1.5rem; border: none; border-radius: 4px; margin-right: 1rem; cursor: pointer;">
54
+ Fetch Demo Data
55
+ </button>
56
+ <button id="show-hydrated" style="background: #17a2b8; color: white; padding: 0.75rem 1.5rem; border: none; border-radius: 4px; cursor: pointer;">
57
+ Show Hydrated Data
58
+ </button>
59
+ </div>
60
+
61
+ <div id="demo-result" style="margin-top: 1.5rem;"></div>
62
+ </div>
63
+
64
+ <div style="background: #fff3cd; border: 1px solid #ffeaa7; padding: 1.5rem; border-radius: 8px; margin-bottom: 2rem;">
65
+ <h3>💡 What's Happening Here?</h3>
66
+ <ul style="margin: 1rem 0; padding-left: 2rem;">
67
+ <li>The JSON data section above is populated with Ruby variables at render time</li>
68
+ <li>This data is then injected into the browser as <code>window.userDashboard</code></li>
69
+ <li>Client-side JavaScript can access this data without additional API calls</li>
70
+ <li>The API endpoints demonstrate fetching fresh data when needed</li>
71
+ <li>CSP nonces are used for security (with additional validation planned)</li>
72
+ <li><em>Note: v0.1.0 - Active development with security improvements in progress</em></li>
73
+ </ul>
74
+ </div>
75
+
76
+ <div style="text-align: center;">
77
+ <a href="/logout" style="background: #dc3545; color: white; padding: 0.75rem 2rem; text-decoration: none; border-radius: 4px; margin-right: 1rem;">Logout</a>
78
+ <a href="/" style="background: #6c757d; color: white; padding: 0.75rem 2rem; text-decoration: none; border-radius: 4px;">Back to Home</a>
79
+ </div>
80
+ </div>
81
+
82
+ <script>
83
+ document.addEventListener('DOMContentLoaded', function() {
84
+ // This data was hydrated from the server-side Ruby context
85
+ const dashboardData = window.userDashboard;
86
+
87
+ console.log('🎉 RSFC Hydration Success!', dashboardData);
88
+ console.log('🔐 Authenticated user viewing secure dashboard');
89
+
90
+ const resultDiv = document.getElementById('demo-result');
91
+
92
+ document.getElementById('fetch-user').addEventListener('click', async function() {
93
+ showLoading();
94
+
95
+ try {
96
+ const response = await fetch(dashboardData.api_endpoints.user);
97
+ const data = await response.json();
98
+
99
+ showResult('User API Data', data, '#e8f5e8');
100
+ } catch (error) {
101
+ showError('Failed to fetch user data: ' + error.message);
102
+ }
103
+ });
104
+
105
+ document.getElementById('fetch-demo').addEventListener('click', async function() {
106
+ showLoading();
107
+
108
+ try {
109
+ const response = await fetch(dashboardData.api_endpoints.demo_data);
110
+ const data = await response.json();
111
+
112
+ showResult('Demo API Data', data, '#e3f2fd');
113
+ } catch (error) {
114
+ showError('Failed to fetch demo data: ' + error.message);
115
+ }
116
+ });
117
+
118
+ document.getElementById('show-hydrated').addEventListener('click', function() {
119
+ showResult('Hydrated Dashboard Data', dashboardData, '#fff3cd');
120
+ });
121
+
122
+ function showLoading() {
123
+ resultDiv.innerHTML = '<p style="color: #666;">⏳ Loading...</p>';
124
+ }
125
+
126
+ function showResult(title, data, bgColor) {
127
+ resultDiv.innerHTML = `
128
+ <div style="background: ${bgColor}; padding: 1.5rem; border-radius: 8px; margin-top: 1rem;">
129
+ <h4>✅ ${title}:</h4>
130
+ <pre style="background: white; padding: 1rem; border-radius: 4px; overflow-x: auto; font-size: 0.9rem;">${JSON.stringify(data, null, 2)}</pre>
131
+ </div>
132
+ `;
133
+ }
134
+
135
+ function showError(message) {
136
+ resultDiv.innerHTML = `<p style="color: #dc3545; background: #f8d7da; padding: 1rem; border-radius: 4px;">❌ ${message}</p>`;
137
+ }
138
+ });
139
+ </script>
140
+ </template>
141
+
142
+ <logic>
143
+ # Dashboard demonstrates:
144
+ # - Secure authentication boundary (only shown when logged in)
145
+ # - Server-side data interpolation in JSON data section
146
+ # - Client-side hydration with window object
147
+ # - Multiple API integration examples
148
+ # - CSP nonce support for secure inline scripts
149
+ # - Interactive demo of RSFC features
150
+ </logic>
@@ -0,0 +1,78 @@
1
+ <!-- demo/rhales-roda-demo/templates/home.rue -->
2
+
3
+ <schema lang="js-zod" version="2" envelope="SuccessEnvelope" window="data" layout="layouts/main">
4
+ const schema = z.object({
5
+ page_type: z.string(),
6
+ features: z.array(z.object({
7
+ title: z.string(),
8
+ description: z.string(),
9
+ icon: z.string()
10
+ }))
11
+ });
12
+ </schema>
13
+
14
+ <template>
15
+ <div class="hero-section">
16
+ <h1 class="hero-title">Welcome to Rhales Demo</h1>
17
+ <p class="hero-subtitle">
18
+ Experience the power of Ruby Single File Components
19
+ </p>
20
+
21
+ {{#unless authenticated}}
22
+ <div class="demo-login">
23
+ <h3>Demo Login</h3>
24
+ <p>Use these credentials to see the authenticated experience:</p>
25
+
26
+ <div class="demo-accounts">
27
+ {{#each demo_accounts}}
28
+ <div class="demo-account">
29
+ <p><strong>{{role}} Account:</strong> {{email}} / {{password}}</p>
30
+ </div>
31
+ {{/each}}
32
+ </div>
33
+
34
+ <a href="/login" class="login-link">Login Now</a>
35
+ </div>
36
+ {{/unless}}
37
+
38
+ {{#if authenticated}}
39
+ <div class="auth-notice">
40
+ <p>✓ You're logged in! <a href="/">View Dashboard</a></p>
41
+ </div>
42
+ {{/if}}
43
+
44
+ <div class="features-section">
45
+ <h2>RSFC Features Demonstrated</h2>
46
+ <div class="features-grid">
47
+ {{#each features}}
48
+ <div class="feature-card">
49
+ <div class="feature-icon">{{icon}}</div>
50
+ <h3 class="feature-title">{{title}}</h3>
51
+ <p class="feature-description">{{description}}</p>
52
+ </div>
53
+ {{/each}}
54
+ </div>
55
+ </div>
56
+ </div>
57
+
58
+ <script nonce="{{app.nonce}}">
59
+ // Demonstrate client-side access to hydrated data
60
+ document.addEventListener('DOMContentLoaded', function() {
61
+ if (window.data) {
62
+ console.log('Rhales Demo hydrated with data:', window.data);
63
+
64
+ // Add a dynamic timestamp
65
+ const timestamp = document.createElement('p');
66
+ timestamp.className = 'timestamp';
67
+ timestamp.innerHTML = 'Page rendered at: ' + new Date().toLocaleString();
68
+ document.body.appendChild(timestamp);
69
+ }
70
+ });
71
+ </script>
72
+ </template>
73
+
74
+ <logic>
75
+ # Homepage showcases Rhales features with authentication demo
76
+ # Demonstrates conditional rendering based on auth state
77
+ # Shows demo credentials for easy testing
78
+ </logic>
@@ -0,0 +1,168 @@
1
+ <!-- demo/rhales-roda-demo/templates/layouts/main.rue -->
2
+
3
+ <schema lang="js-zod" version="2" envelope="SuccessEnvelope" window="layout">
4
+ const schema = z.object({
5
+ app_name: z.string(),
6
+ year: z.number()
7
+ });
8
+ </schema>
9
+
10
+ <template>
11
+ <!DOCTYPE html>
12
+ <html lang="en">
13
+ <head>
14
+ <meta charset="UTF-8">
15
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
16
+ <title>{{app_name}}</title>
17
+ <style nonce="{{app.nonce}}">
18
+ * { margin: 0; padding: 0; box-sizing: border-box; }
19
+ body {
20
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
21
+ line-height: 1.6;
22
+ color: #333;
23
+ background: #f5f5f5;
24
+ }
25
+ .container { max-width: 1200px; margin: 0 auto; padding: 0 20px; }
26
+ header {
27
+ background: #2c3e50;
28
+ color: white;
29
+ padding: 1rem 0;
30
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
31
+ }
32
+ nav { display: flex; justify-content: space-between; align-items: center; }
33
+ nav a { color: white; text-decoration: none; margin-left: 1.5rem; }
34
+ nav a:hover { text-decoration: underline; }
35
+ .logo { font-size: 1.5rem; font-weight: bold; }
36
+ main { min-height: calc(100vh - 140px); padding: 2rem 0; }
37
+ footer {
38
+ background: #34495e;
39
+ color: white;
40
+ text-align: center;
41
+ padding: 1rem 0;
42
+ }
43
+ .flash {
44
+ padding: 1rem;
45
+ margin: 1rem 0;
46
+ border-radius: 4px;
47
+ background: #d4edda;
48
+ color: #155724;
49
+ border: 1px solid #c3e6cb;
50
+ }
51
+ .flash.error {
52
+ background: #f8d7da;
53
+ color: #721c24;
54
+ border-color: #f5c6cb;
55
+ }
56
+ .btn {
57
+ display: inline-block;
58
+ padding: 0.5rem 1rem;
59
+ background: #3498db;
60
+ color: white;
61
+ text-decoration: none;
62
+ border: none;
63
+ border-radius: 4px;
64
+ cursor: pointer;
65
+ }
66
+ .btn:hover { background: #2980b9; }
67
+ .btn-danger { background: #e74c3c; }
68
+ .btn-danger:hover { background: #c0392b; }
69
+ .card {
70
+ background: white;
71
+ padding: 2rem;
72
+ border-radius: 8px;
73
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
74
+ margin-bottom: 1rem;
75
+ }
76
+ .form-group { margin-bottom: 1rem; }
77
+ .form-group label { display: block; margin-bottom: 0.5rem; font-weight: 500; }
78
+ .form-group input, .form-group textarea, .form-group select {
79
+ width: 100%;
80
+ padding: 0.5rem;
81
+ border: 1px solid #ddd;
82
+ border-radius: 4px;
83
+ font-size: 1rem;
84
+ }
85
+ .form-group input:focus, .form-group textarea:focus {
86
+ outline: none;
87
+ border-color: #3498db;
88
+ }
89
+ .stats {
90
+ display: grid;
91
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
92
+ gap: 1rem;
93
+ margin-bottom: 2rem;
94
+ }
95
+ .stat-card {
96
+ background: white;
97
+ padding: 1.5rem;
98
+ border-radius: 8px;
99
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
100
+ text-align: center;
101
+ }
102
+ .stat-card h3 { color: #666; font-size: 0.9rem; margin-bottom: 0.5rem; }
103
+ .stat-card .value { font-size: 2rem; font-weight: bold; color: #2c3e50; }
104
+
105
+ /* Home page styles */
106
+ .hero-section { text-align: center; padding: 4rem 0; }
107
+ .hero-title { font-size: 3rem; margin-bottom: 1rem; }
108
+ .hero-subtitle { font-size: 1.2rem; color: #666; margin-bottom: 2rem; }
109
+ .demo-login { background: #f8f9fa; padding: 2rem; border-radius: 8px; margin: 2rem auto; max-width: 400px; }
110
+ .demo-accounts { background: white; padding: 1rem; border-radius: 4px; margin: 1rem 0; }
111
+ .demo-account { margin-bottom: 0.5rem; }
112
+ .login-link { background: #007bff; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 4px; }
113
+ .auth-notice { background: #d4edda; color: #155724; padding: 1rem; border-radius: 4px; margin: 2rem auto; max-width: 400px; }
114
+ .features-section { margin: 3rem 0; }
115
+ .features-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 2rem; margin-top: 2rem; }
116
+ .feature-card { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center; }
117
+ .feature-icon { font-size: 3rem; margin-bottom: 1rem; }
118
+ .feature-title { margin-bottom: 1rem; }
119
+ .feature-description { color: #666; font-size: 0.9rem; }
120
+ .timestamp { font-size: 0.8rem; color: #999; }
121
+ </style>
122
+ </head>
123
+ <body>
124
+ <header>
125
+ <div class="container">
126
+ <nav>
127
+ <div class="logo">{{app_name}}</div>
128
+ <div>
129
+ {{#if authenticated}}
130
+ <a href="/">Dashboard</a>
131
+ <a href="/logout">Logout</a>
132
+ {{else}}
133
+ <a href="/">Home</a>
134
+ <a href="/login">Login</a>
135
+ <a href="/register">Register</a>
136
+ {{/if}}
137
+ </div>
138
+ </nav>
139
+ </div>
140
+ </header>
141
+
142
+ <main>
143
+ <div class="container">
144
+ {{#if flash_notice}}
145
+ <div class="flash">{{flash_notice}}</div>
146
+ {{/if}}
147
+ {{#if flash_error}}
148
+ <div class="flash error">{{flash_error}}</div>
149
+ {{/if}}
150
+
151
+ {{{content}}}
152
+ </div>
153
+ </main>
154
+
155
+ <footer>
156
+ <div class="container">
157
+ <p>&copy; {{year}} {{app_name}} - Powered by Rhales RSFC</p>
158
+ </div>
159
+ </footer>
160
+ </body>
161
+ </html>
162
+ </template>
163
+
164
+ <logic>
165
+ # Layout provides the main HTML structure for all pages
166
+ # Uses conditional rendering for navigation based on authentication state
167
+ # Includes flash message support and responsive design
168
+ </logic>