rhales 0.4.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 -1
- data/Gemfile +29 -0
- data/Gemfile.lock +189 -0
- data/README.md +686 -868
- 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 +47 -0
- data/lib/rhales/core/context.rb +354 -0
- data/lib/rhales/{rue_document.rb → core/rue_document.rb} +56 -38
- data/lib/rhales/{template_engine.rb → core/template_engine.rb} +66 -59
- data/lib/rhales/{view.rb → core/view.rb} +112 -135
- data/lib/rhales/{view_composition.rb → core/view_composition.rb} +78 -8
- 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/{earliest_injection_detector.rb → hydration/earliest_injection_detector.rb} +4 -0
- data/lib/rhales/hydration/hydration_data_aggregator.rb +487 -0
- data/lib/rhales/{hydration_endpoint.rb → hydration/hydration_endpoint.rb} +16 -12
- data/lib/rhales/{hydration_injector.rb → hydration/hydration_injector.rb} +4 -0
- data/lib/rhales/{hydration_registry.rb → hydration/hydration_registry.rb} +2 -0
- data/lib/rhales/hydration/hydrator.rb +102 -0
- data/lib/rhales/{link_based_injection_detector.rb → hydration/link_based_injection_detector.rb} +4 -0
- data/lib/rhales/{mount_point_detector.rb → hydration/mount_point_detector.rb} +4 -0
- data/lib/rhales/{safe_injection_validator.rb → hydration/safe_injection_validator.rb} +4 -0
- data/lib/rhales/hydration.rb +13 -0
- data/lib/rhales/{refinements → integrations/refinements}/require_refinements.rb +3 -1
- data/lib/rhales/{tilt.rb → integrations/tilt.rb} +22 -15
- 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 +9 -7
- 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 +3 -1
- data/lib/rhales.rb +41 -24
- 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 +141 -23
- data/CLAUDE.locale.txt +0 -7
- data/lib/rhales/context.rb +0 -239
- data/lib/rhales/hydration_data_aggregator.rb +0 -221
- data/lib/rhales/hydrator.rb +0 -141
- data/lib/rhales/parsers/handlebars-grammar-review.txt +0 -39
|
@@ -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>© {{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>
|