railspress-engine 1.2.0 → 1.3.0
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/README.md +1 -1
- data/app/assets/javascripts/railspress/admin.js +54 -0
- data/app/assets/stylesheets/railspress/admin/buttons.css +12 -0
- data/app/assets/stylesheets/railspress/admin/cards.css +8 -0
- data/app/assets/stylesheets/railspress/admin/forms.css +21 -0
- data/app/assets/stylesheets/railspress/admin/layout.css +88 -0
- data/app/assets/stylesheets/railspress/admin/responsive.css +15 -0
- data/app/controllers/railspress/admin/agent_bootstrap_keys_controller.rb +109 -0
- data/app/controllers/railspress/admin/api_keys_controller.rb +165 -0
- data/app/controllers/railspress/admin/base_controller.rb +61 -1
- data/app/controllers/railspress/api/v1/agent_key_exchanges_controller.rb +50 -0
- data/app/controllers/railspress/api/v1/base_controller.rb +52 -0
- data/app/controllers/railspress/api/v1/categories_controller.rb +89 -0
- data/app/controllers/railspress/api/v1/concerns/post_serialization.rb +130 -0
- data/app/controllers/railspress/api/v1/post_header_image_contexts_controller.rb +158 -0
- data/app/controllers/railspress/api/v1/post_header_image_focal_points_controller.rb +74 -0
- data/app/controllers/railspress/api/v1/post_header_images_controller.rb +58 -0
- data/app/controllers/railspress/api/v1/post_imports_controller.rb +118 -0
- data/app/controllers/railspress/api/v1/posts_controller.rb +127 -0
- data/app/controllers/railspress/api/v1/prime_controller.rb +78 -0
- data/app/controllers/railspress/api/v1/tags_controller.rb +85 -0
- data/app/helpers/railspress/admin_helper.rb +19 -0
- data/app/models/railspress/agent_bootstrap_key.rb +163 -0
- data/app/models/railspress/api_key.rb +157 -0
- data/app/models/railspress/post_export_processor.rb +16 -2
- data/app/views/railspress/admin/agent_bootstrap_keys/_form.html.erb +25 -0
- data/app/views/railspress/admin/agent_bootstrap_keys/new.html.erb +7 -0
- data/app/views/railspress/admin/agent_bootstrap_keys/reveal.html.erb +38 -0
- data/app/views/railspress/admin/api_keys/_form.html.erb +25 -0
- data/app/views/railspress/admin/api_keys/index.html.erb +142 -0
- data/app/views/railspress/admin/api_keys/new.html.erb +7 -0
- data/app/views/railspress/admin/api_keys/reveal.html.erb +40 -0
- data/app/views/railspress/admin/posts/_form.html.erb +1 -1
- data/app/views/railspress/admin/posts/_post_row.html.erb +1 -1
- data/app/views/railspress/admin/posts/show.html.erb +1 -1
- data/app/views/railspress/admin/shared/_copyable_textarea.html.erb +17 -0
- data/app/views/railspress/admin/shared/_sidebar.html.erb +46 -0
- data/config/routes.rb +33 -0
- data/db/migrate/20260415000001_create_railspress_api_keys.rb +40 -0
- data/db/migrate/20260415000002_create_railspress_agent_bootstrap_keys.rb +37 -0
- data/lib/generators/railspress/install/templates/initializer.rb +11 -0
- data/lib/railspress/version.rb +1 -1
- data/lib/railspress.rb +49 -1
- metadata +26 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<% content_for :title, "Agent Bootstrap Key Created" %>
|
|
2
|
+
|
|
3
|
+
<h1 class="rp-page-title rp-page-title--standalone">Agent Bootstrap Key Created</h1>
|
|
4
|
+
|
|
5
|
+
<div class="rp-card rp-card--padded">
|
|
6
|
+
<div class="rp-form rp-form--spacious">
|
|
7
|
+
<p class="rp-hint">
|
|
8
|
+
Copy this bootstrap key now. It is one-time use and will not be shown again.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<%= render "railspress/admin/shared/copyable_textarea",
|
|
12
|
+
id: "rp-bootstrap-key-token",
|
|
13
|
+
label: "Bootstrap Token",
|
|
14
|
+
value: @plain_bootstrap_token,
|
|
15
|
+
rows: 4,
|
|
16
|
+
hint: "Use this once at #{exchange_api_v1_agent_keys_path} to mint a real API key.",
|
|
17
|
+
button_text: "Copy Bootstrap Token" %>
|
|
18
|
+
|
|
19
|
+
<%= render "railspress/admin/shared/copyable_textarea",
|
|
20
|
+
id: "rp-bootstrap-quick-start",
|
|
21
|
+
label: "Bootstrap Quick Start",
|
|
22
|
+
value: @bootstrap_quick_start,
|
|
23
|
+
rows: 6,
|
|
24
|
+
button_text: "Copy Quick Start" %>
|
|
25
|
+
|
|
26
|
+
<%= render "railspress/admin/shared/copyable_textarea",
|
|
27
|
+
id: "rp-bootstrap-instructions",
|
|
28
|
+
label: "Agent Setup Instructions",
|
|
29
|
+
value: @bootstrap_instructions,
|
|
30
|
+
rows: 15,
|
|
31
|
+
button_text: "Copy Instructions" %>
|
|
32
|
+
|
|
33
|
+
<div class="rp-form-actions">
|
|
34
|
+
<%= link_to "Create Another Bootstrap Key", new_admin_agent_bootstrap_key_path, class: "rp-btn rp-btn--primary" %>
|
|
35
|
+
<%= link_to "Back to Agents & API", admin_api_keys_path, class: "rp-btn rp-btn--secondary" %>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<%= form_with model: [:admin, api_key], class: "rp-form rp-form--narrow" do |form| %>
|
|
2
|
+
<%= rp_form_errors(api_key) %>
|
|
3
|
+
|
|
4
|
+
<%= rp_hint("API keys currently grant full access to API resources that are exposed in this version.") %>
|
|
5
|
+
|
|
6
|
+
<%= rp_string_field(
|
|
7
|
+
form,
|
|
8
|
+
:name,
|
|
9
|
+
primary: true,
|
|
10
|
+
required: true,
|
|
11
|
+
label: "API Key Name",
|
|
12
|
+
placeholder: "e.g., Production Sync Worker",
|
|
13
|
+
hint: "Use a descriptive name so you can identify this key later."
|
|
14
|
+
) %>
|
|
15
|
+
|
|
16
|
+
<%= rp_datetime_field(
|
|
17
|
+
form,
|
|
18
|
+
:expires_at,
|
|
19
|
+
label: "Expiration",
|
|
20
|
+
hint: "Default is no expiration. Set a custom date/time if you want this key to expire.",
|
|
21
|
+
step: 60
|
|
22
|
+
) %>
|
|
23
|
+
|
|
24
|
+
<%= rp_form_actions(form, admin_api_keys_path, submit_text: "Create API Key") %>
|
|
25
|
+
<% end %>
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<% content_for :title, "Agents & API" %>
|
|
2
|
+
|
|
3
|
+
<%= rp_page_header "Agents & API",
|
|
4
|
+
"New Agent Key" => [new_admin_agent_bootstrap_key_path, class: "rp-btn rp-btn--secondary"],
|
|
5
|
+
"New API Key" => new_admin_api_key_path %>
|
|
6
|
+
|
|
7
|
+
<div class="rp-card rp-card--padded rp-card--section">
|
|
8
|
+
<div class="rp-section">
|
|
9
|
+
<div class="rp-section-header">
|
|
10
|
+
<h2 class="rp-section-title">Agent Setup (Generic)</h2>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div class="rp-form rp-form--spacious">
|
|
14
|
+
<p class="rp-hint">Use a short-lived bootstrap key in agent instructions. The agent exchanges it once for a real API key.</p>
|
|
15
|
+
|
|
16
|
+
<%= render "railspress/admin/shared/copyable_textarea",
|
|
17
|
+
id: "rp-generic-agent-quick-start",
|
|
18
|
+
label: "Bootstrap Quick Start",
|
|
19
|
+
value: @generic_agent_quick_start,
|
|
20
|
+
rows: 10,
|
|
21
|
+
button_text: "Copy Quick Start" %>
|
|
22
|
+
|
|
23
|
+
<%= render "railspress/admin/shared/copyable_textarea",
|
|
24
|
+
id: "rp-generic-agent-instructions",
|
|
25
|
+
label: "Agent Instructions",
|
|
26
|
+
value: @generic_agent_instructions,
|
|
27
|
+
rows: 10,
|
|
28
|
+
button_text: "Copy Instructions" %>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div class="rp-card rp-card--padded rp-card--section">
|
|
34
|
+
<div class="rp-section">
|
|
35
|
+
<div class="rp-section-header">
|
|
36
|
+
<h2 class="rp-section-title">API Keys</h2>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<% if @api_keys.any? %>
|
|
40
|
+
<div class="rp-table--responsive">
|
|
41
|
+
<table class="rp-table">
|
|
42
|
+
<thead>
|
|
43
|
+
<tr>
|
|
44
|
+
<th><%= rp_table_header("Name") %></th>
|
|
45
|
+
<th><%= rp_table_header("Prefix") %></th>
|
|
46
|
+
<th><%= rp_table_header("Status") %></th>
|
|
47
|
+
<th><%= rp_table_header("Last Used") %></th>
|
|
48
|
+
<th><%= rp_table_header("Expires") %></th>
|
|
49
|
+
<th class="rp-table-actions"><%= rp_table_header("Actions") %></th>
|
|
50
|
+
</tr>
|
|
51
|
+
</thead>
|
|
52
|
+
<tbody>
|
|
53
|
+
<% @api_keys.each do |api_key| %>
|
|
54
|
+
<% api_status_type = case api_key.status
|
|
55
|
+
when "active" then :published
|
|
56
|
+
when "expired" then :draft
|
|
57
|
+
else :failed
|
|
58
|
+
end %>
|
|
59
|
+
<tr>
|
|
60
|
+
<td class="rp-table-primary"><%= api_key.name %></td>
|
|
61
|
+
<td class="rp-table-secondary"><code><%= api_key.token_prefix %></code></td>
|
|
62
|
+
<td><%= rp_status_badge(api_key.status, type: api_status_type) %></td>
|
|
63
|
+
<td><%= api_key.last_used_at ? l(api_key.last_used_at, format: :short) : "Never" %></td>
|
|
64
|
+
<td><%= api_key.expires_at ? l(api_key.expires_at, format: :short) : "Never" %></td>
|
|
65
|
+
<td class="rp-table-actions">
|
|
66
|
+
<%= button_to "Rotate",
|
|
67
|
+
rotate_admin_api_key_path(api_key),
|
|
68
|
+
method: :post,
|
|
69
|
+
class: "rp-btn rp-btn--secondary rp-btn--sm" %>
|
|
70
|
+
<% unless api_key.revoked_at %>
|
|
71
|
+
<%= button_to "Revoke",
|
|
72
|
+
revoke_admin_api_key_path(api_key),
|
|
73
|
+
method: :post,
|
|
74
|
+
class: "rp-btn rp-btn--danger rp-btn--sm",
|
|
75
|
+
data: { turbo_confirm: "Revoke this API key?" } %>
|
|
76
|
+
<% end %>
|
|
77
|
+
</td>
|
|
78
|
+
</tr>
|
|
79
|
+
<% end %>
|
|
80
|
+
</tbody>
|
|
81
|
+
</table>
|
|
82
|
+
</div>
|
|
83
|
+
<% else %>
|
|
84
|
+
<%= rp_empty_state("No API keys yet.", link_text: "Create your first API key", link_path: new_admin_api_key_path) %>
|
|
85
|
+
<% end %>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<div class="rp-card rp-card--padded rp-card--section">
|
|
90
|
+
<div class="rp-section">
|
|
91
|
+
<div class="rp-section-header">
|
|
92
|
+
<h2 class="rp-section-title">Agent Bootstrap Keys</h2>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<% if @agent_bootstrap_keys.any? %>
|
|
96
|
+
<div class="rp-table--responsive">
|
|
97
|
+
<table class="rp-table">
|
|
98
|
+
<thead>
|
|
99
|
+
<tr>
|
|
100
|
+
<th><%= rp_table_header("Name") %></th>
|
|
101
|
+
<th><%= rp_table_header("Prefix") %></th>
|
|
102
|
+
<th><%= rp_table_header("Status") %></th>
|
|
103
|
+
<th><%= rp_table_header("Used") %></th>
|
|
104
|
+
<th><%= rp_table_header("Expires") %></th>
|
|
105
|
+
<th class="rp-table-actions"><%= rp_table_header("Actions") %></th>
|
|
106
|
+
</tr>
|
|
107
|
+
</thead>
|
|
108
|
+
<tbody>
|
|
109
|
+
<% @agent_bootstrap_keys.each do |bootstrap_key| %>
|
|
110
|
+
<% bootstrap_status_type = case bootstrap_key.status
|
|
111
|
+
when "active" then :published
|
|
112
|
+
when "used" then :completed
|
|
113
|
+
when "expired" then :draft
|
|
114
|
+
else :failed
|
|
115
|
+
end %>
|
|
116
|
+
<tr>
|
|
117
|
+
<td class="rp-table-primary"><%= bootstrap_key.name %></td>
|
|
118
|
+
<td class="rp-table-secondary"><code><%= bootstrap_key.token_prefix %></code></td>
|
|
119
|
+
<td><%= rp_status_badge(bootstrap_key.status, type: bootstrap_status_type) %></td>
|
|
120
|
+
<td><%= bootstrap_key.used_at ? l(bootstrap_key.used_at, format: :short) : "No" %></td>
|
|
121
|
+
<td><%= l(bootstrap_key.expires_at, format: :short) %></td>
|
|
122
|
+
<td class="rp-table-actions">
|
|
123
|
+
<% if bootstrap_key.active? %>
|
|
124
|
+
<%= button_to "Revoke",
|
|
125
|
+
revoke_admin_agent_bootstrap_key_path(bootstrap_key),
|
|
126
|
+
method: :post,
|
|
127
|
+
class: "rp-btn rp-btn--danger rp-btn--sm",
|
|
128
|
+
data: { turbo_confirm: "Revoke this bootstrap key?" } %>
|
|
129
|
+
<% else %>
|
|
130
|
+
<span class="rp-hint">No actions</span>
|
|
131
|
+
<% end %>
|
|
132
|
+
</td>
|
|
133
|
+
</tr>
|
|
134
|
+
<% end %>
|
|
135
|
+
</tbody>
|
|
136
|
+
</table>
|
|
137
|
+
</div>
|
|
138
|
+
<% else %>
|
|
139
|
+
<%= rp_empty_state("No bootstrap keys yet.", link_text: "Create your first bootstrap key", link_path: new_admin_agent_bootstrap_key_path) %>
|
|
140
|
+
<% end %>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<% content_for :title, "API Key Created" %>
|
|
2
|
+
|
|
3
|
+
<h1 class="rp-page-title rp-page-title--standalone">API Key Created</h1>
|
|
4
|
+
|
|
5
|
+
<div class="rp-card rp-card--padded">
|
|
6
|
+
<div class="rp-form rp-form--spacious">
|
|
7
|
+
<p class="rp-hint">
|
|
8
|
+
Copy this key now. For security reasons it will not be shown again.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<%= render "railspress/admin/shared/copyable_textarea",
|
|
12
|
+
id: "rp-api-key-token",
|
|
13
|
+
label: "Bearer Token",
|
|
14
|
+
value: @plain_token,
|
|
15
|
+
rows: 4,
|
|
16
|
+
hint: "Use this as Authorization: Bearer <token> (or save it to ~/.railspress_token for reuse).",
|
|
17
|
+
button_text: "Copy Token" %>
|
|
18
|
+
|
|
19
|
+
<%= render "railspress/admin/shared/copyable_textarea",
|
|
20
|
+
id: "rp-api-key-quick-start",
|
|
21
|
+
label: "API Quick Start",
|
|
22
|
+
value: @api_key_quick_start,
|
|
23
|
+
rows: 3,
|
|
24
|
+
hint: "This verifies connectivity and capabilities via /api/v1/prime.",
|
|
25
|
+
button_text: "Copy Quick Start" %>
|
|
26
|
+
|
|
27
|
+
<%= render "railspress/admin/shared/copyable_textarea",
|
|
28
|
+
id: "rp-api-key-instructions",
|
|
29
|
+
label: "API Key Instructions",
|
|
30
|
+
value: @api_key_instructions,
|
|
31
|
+
rows: 14,
|
|
32
|
+
hint: "Draft is default; publishing is supported when explicitly requested.",
|
|
33
|
+
button_text: "Copy Instructions" %>
|
|
34
|
+
|
|
35
|
+
<div class="rp-form-actions">
|
|
36
|
+
<%= link_to "Create Another API Key", new_admin_api_key_path, class: "rp-btn rp-btn--primary" %>
|
|
37
|
+
<%= link_to "Back to Agents & API", admin_api_keys_path, class: "rp-btn rp-btn--secondary" %>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
@@ -159,7 +159,7 @@
|
|
|
159
159
|
<% if authors_enabled? %>
|
|
160
160
|
<div class="rp-form-group">
|
|
161
161
|
<%= form.label :author_id, "Author", class: "rp-label" %>
|
|
162
|
-
<%= form.collection_select :author_id, available_authors, :id,
|
|
162
|
+
<%= form.collection_select :author_id, available_authors, :id, ->(author) { rp_author_display(author) },
|
|
163
163
|
{ prompt: "No author", selected: @post.author_id },
|
|
164
164
|
{ class: "rp-select" } %>
|
|
165
165
|
</div>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
</td>
|
|
12
12
|
<td><%= post.category&.name || "—" %></td>
|
|
13
13
|
<% if authors_enabled? %>
|
|
14
|
-
<td><%= post.author
|
|
14
|
+
<td><%= post.author ? rp_author_display(post.author) : "—" %></td>
|
|
15
15
|
<% end %>
|
|
16
16
|
<td>
|
|
17
17
|
<span class="rp-badge rp-badge--<%= post.display_status %>">
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<div>
|
|
3
3
|
<h1 class="rp-page-title"><%= @post.title %></h1>
|
|
4
4
|
<% if authors_enabled? && @post.author %>
|
|
5
|
-
<p class="rp-byline">by <%= @post.author
|
|
5
|
+
<p class="rp-byline">by <%= rp_author_display(@post.author) %></p>
|
|
6
6
|
<% end %>
|
|
7
7
|
</div>
|
|
8
8
|
<div class="rp-page-actions">
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<div class="rp-form-group">
|
|
2
|
+
<label class="rp-label"><%= label %></label>
|
|
3
|
+
<% textarea_classes = ["rp-input", "rp-input--mono", "rp-input--code", local_assigns[:textarea_class]].compact.join(" ") %>
|
|
4
|
+
<textarea id="<%= id %>"
|
|
5
|
+
class="<%= textarea_classes %>"
|
|
6
|
+
rows="<%= local_assigns.fetch(:rows, 4) %>"
|
|
7
|
+
readonly><%= value %></textarea>
|
|
8
|
+
<% if local_assigns[:hint].present? %>
|
|
9
|
+
<p class="rp-hint"><%= hint %></p>
|
|
10
|
+
<% end %>
|
|
11
|
+
<% button_variant = local_assigns.fetch(:button_class, "rp-btn--accent") %>
|
|
12
|
+
<button type="button"
|
|
13
|
+
class="rp-btn rp-btn--sm <%= button_variant %>"
|
|
14
|
+
data-copy-target="<%= id %>">
|
|
15
|
+
<%= local_assigns.fetch(:button_text, "Copy") %>
|
|
16
|
+
</button>
|
|
17
|
+
</div>
|
|
@@ -94,6 +94,20 @@
|
|
|
94
94
|
<% end %>
|
|
95
95
|
|
|
96
96
|
<div class="rp-nav-divider"></div>
|
|
97
|
+
<% if Railspress.api_enabled? %>
|
|
98
|
+
<%= link_to railspress.admin_api_keys_path,
|
|
99
|
+
class: "rp-nav-link #{controller_name == 'api_keys' ? 'rp-nav-link--active' : ''}" do %>
|
|
100
|
+
<svg class="rp-nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
101
|
+
<path d="M12 3 13.6 6.9 17.5 8.5 13.6 10.1 12 14 10.4 10.1 6.5 8.5 10.4 6.9z"/>
|
|
102
|
+
<path d="M19 13 19.8 14.9 21.7 15.7 19.8 16.5 19 18.4 18.2 16.5 16.3 15.7 18.2 14.9z"/>
|
|
103
|
+
<path d="M5 15 5.8 16.9 7.7 17.7 5.8 18.5 5 20.4 4.2 18.5 2.3 17.7 4.2 16.9z"/>
|
|
104
|
+
</svg>
|
|
105
|
+
<span class="rp-nav-text">Agents & API</span>
|
|
106
|
+
<% end %>
|
|
107
|
+
|
|
108
|
+
<div class="rp-nav-divider"></div>
|
|
109
|
+
<% end %>
|
|
110
|
+
|
|
97
111
|
<%= link_to "/", class: "rp-nav-link" do %>
|
|
98
112
|
<svg class="rp-nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
99
113
|
<circle cx="12" cy="12" r="10"/>
|
|
@@ -103,4 +117,36 @@
|
|
|
103
117
|
<span class="rp-nav-text">Main Site</span>
|
|
104
118
|
<% end %>
|
|
105
119
|
</nav>
|
|
120
|
+
|
|
121
|
+
<div class="rp-sidebar-footer">
|
|
122
|
+
<div class="rp-sidebar-version">v<%= Railspress::VERSION %></div>
|
|
123
|
+
|
|
124
|
+
<%= link_to "https://railspress.org/guide/", class: "rp-sidebar-footer-link", target: "_blank", rel: "noopener noreferrer", title: "Guide" do %>
|
|
125
|
+
<svg class="rp-sidebar-footer-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
126
|
+
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>
|
|
127
|
+
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5V4.5A2.5 2.5 0 0 1 6.5 2z"/>
|
|
128
|
+
</svg>
|
|
129
|
+
<span class="rp-sidebar-footer-text">Guide</span>
|
|
130
|
+
<% end %>
|
|
131
|
+
|
|
132
|
+
<%= link_to "https://railspress.org/docs/", class: "rp-sidebar-footer-link", target: "_blank", rel: "noopener noreferrer", title: "Docs" do %>
|
|
133
|
+
<svg class="rp-sidebar-footer-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
134
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
|
135
|
+
<polyline points="14 2 14 8 20 8"/>
|
|
136
|
+
</svg>
|
|
137
|
+
<span class="rp-sidebar-footer-text">Docs</span>
|
|
138
|
+
<% end %>
|
|
139
|
+
|
|
140
|
+
<%= link_to "https://github.com/aviflombaum/railspress-engine", class: "rp-sidebar-footer-link", target: "_blank", rel: "noopener noreferrer", title: "GitHub" do %>
|
|
141
|
+
<svg class="rp-sidebar-footer-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
142
|
+
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/>
|
|
143
|
+
</svg>
|
|
144
|
+
<span class="rp-sidebar-footer-text">GitHub</span>
|
|
145
|
+
<% end %>
|
|
146
|
+
|
|
147
|
+
<p class="rp-sidebar-credit">
|
|
148
|
+
Made with ❤️ by
|
|
149
|
+
<%= link_to "Avi.nyc", "https://avi.nyc", target: "_blank", rel: "noopener noreferrer", class: "rp-sidebar-credit-link" %>
|
|
150
|
+
</p>
|
|
151
|
+
</div>
|
|
106
152
|
</div>
|
data/config/routes.rb
CHANGED
|
@@ -4,6 +4,17 @@ Railspress::Engine.routes.draw do
|
|
|
4
4
|
root "dashboard#index"
|
|
5
5
|
resources :categories, except: [ :show ]
|
|
6
6
|
resources :tags, except: [ :show ]
|
|
7
|
+
resources :api_keys, only: [ :index, :new, :create ] do
|
|
8
|
+
member do
|
|
9
|
+
post :rotate
|
|
10
|
+
post :revoke
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
resources :agent_bootstrap_keys, only: [ :new, :create ] do
|
|
14
|
+
member do
|
|
15
|
+
post :revoke
|
|
16
|
+
end
|
|
17
|
+
end
|
|
7
18
|
|
|
8
19
|
# Content Element CMS (opt-in via config.enable_cms)
|
|
9
20
|
if Railspress.cms_enabled?
|
|
@@ -67,4 +78,26 @@ Railspress::Engine.routes.draw do
|
|
|
67
78
|
end
|
|
68
79
|
end
|
|
69
80
|
end
|
|
81
|
+
|
|
82
|
+
namespace :api do
|
|
83
|
+
namespace :v1 do
|
|
84
|
+
resource :prime, only: [ :show ], controller: "prime"
|
|
85
|
+
resources :agent_keys, only: [] do
|
|
86
|
+
collection do
|
|
87
|
+
post :exchange, to: "agent_key_exchanges#create"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
resources :post_imports, path: "posts/imports", only: [ :create, :show ]
|
|
91
|
+
resources :posts, only: [ :index, :show, :create, :update, :destroy ] do
|
|
92
|
+
resource :header_image, only: [ :show, :update, :destroy ], controller: "post_header_images" do
|
|
93
|
+
resource :focal_point, only: [ :show, :update ], controller: "post_header_image_focal_points"
|
|
94
|
+
resources :contexts, only: [ :index, :show, :update, :destroy ],
|
|
95
|
+
controller: "post_header_image_contexts",
|
|
96
|
+
param: :context
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
resources :categories, only: [ :index, :show, :create, :update, :destroy ]
|
|
100
|
+
resources :tags, only: [ :index, :show, :create, :update, :destroy ]
|
|
101
|
+
end
|
|
102
|
+
end
|
|
70
103
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateRailspressApiKeys < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :railspress_api_keys do |t|
|
|
6
|
+
t.string :name, null: false
|
|
7
|
+
t.string :global_uuid, null: false
|
|
8
|
+
t.string :token_prefix, null: false
|
|
9
|
+
t.string :token_digest, null: false
|
|
10
|
+
t.text :secret_ciphertext, null: false
|
|
11
|
+
t.datetime :expires_at
|
|
12
|
+
t.datetime :last_used_at
|
|
13
|
+
t.string :last_used_ip
|
|
14
|
+
t.datetime :revoked_at
|
|
15
|
+
t.string :revoke_reason
|
|
16
|
+
t.integer :rotated_from_id
|
|
17
|
+
|
|
18
|
+
t.string :owner_type
|
|
19
|
+
t.bigint :owner_id
|
|
20
|
+
|
|
21
|
+
t.string :created_by_type
|
|
22
|
+
t.bigint :created_by_id
|
|
23
|
+
t.string :rotated_by_type
|
|
24
|
+
t.bigint :rotated_by_id
|
|
25
|
+
t.string :revoked_by_type
|
|
26
|
+
t.bigint :revoked_by_id
|
|
27
|
+
|
|
28
|
+
t.timestamps
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
add_index :railspress_api_keys, :global_uuid, unique: true
|
|
32
|
+
add_index :railspress_api_keys, :token_prefix
|
|
33
|
+
add_index :railspress_api_keys, :token_digest, unique: true
|
|
34
|
+
add_index :railspress_api_keys, :rotated_from_id
|
|
35
|
+
add_index :railspress_api_keys, [ :owner_type, :owner_id ]
|
|
36
|
+
add_index :railspress_api_keys, [ :created_by_type, :created_by_id ]
|
|
37
|
+
add_index :railspress_api_keys, [ :rotated_by_type, :rotated_by_id ]
|
|
38
|
+
add_index :railspress_api_keys, [ :revoked_by_type, :revoked_by_id ]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateRailspressAgentBootstrapKeys < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :railspress_agent_bootstrap_keys do |t|
|
|
6
|
+
t.string :name, null: false
|
|
7
|
+
t.string :global_uuid, null: false
|
|
8
|
+
t.string :token_prefix, null: false
|
|
9
|
+
t.string :token_digest, null: false
|
|
10
|
+
t.text :secret_ciphertext, null: false
|
|
11
|
+
t.datetime :expires_at, null: false
|
|
12
|
+
t.datetime :used_at
|
|
13
|
+
t.string :used_ip
|
|
14
|
+
t.datetime :revoked_at
|
|
15
|
+
t.string :revoke_reason
|
|
16
|
+
t.bigint :exchanged_api_key_id
|
|
17
|
+
|
|
18
|
+
t.string :owner_type
|
|
19
|
+
t.bigint :owner_id
|
|
20
|
+
|
|
21
|
+
t.string :created_by_type
|
|
22
|
+
t.bigint :created_by_id
|
|
23
|
+
t.string :revoked_by_type
|
|
24
|
+
t.bigint :revoked_by_id
|
|
25
|
+
|
|
26
|
+
t.timestamps
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
add_index :railspress_agent_bootstrap_keys, :global_uuid, unique: true
|
|
30
|
+
add_index :railspress_agent_bootstrap_keys, :token_prefix
|
|
31
|
+
add_index :railspress_agent_bootstrap_keys, :token_digest, unique: true
|
|
32
|
+
add_index :railspress_agent_bootstrap_keys, :exchanged_api_key_id
|
|
33
|
+
add_index :railspress_agent_bootstrap_keys, [ :owner_type, :owner_id ], name: "idx_rp_agent_bootstrap_keys_owner"
|
|
34
|
+
add_index :railspress_agent_bootstrap_keys, [ :created_by_type, :created_by_id ], name: "idx_rp_agent_bootstrap_keys_created_by"
|
|
35
|
+
add_index :railspress_agent_bootstrap_keys, [ :revoked_by_type, :revoked_by_id ], name: "idx_rp_agent_bootstrap_keys_revoked_by"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -26,4 +26,15 @@ Railspress.configure do |config|
|
|
|
26
26
|
# config.inline_editing_check = ->(context) {
|
|
27
27
|
# context.controller.current_user&.admin?
|
|
28
28
|
# }
|
|
29
|
+
|
|
30
|
+
# === API (opt-in) ===
|
|
31
|
+
# Enables API endpoints under /railspress/api/v1 and admin API key management.
|
|
32
|
+
# Requires Active Record Encryption keys in your host app config.
|
|
33
|
+
# Uncomment to enable:
|
|
34
|
+
# config.enable_api
|
|
35
|
+
# config.current_api_actor_method = :current_user
|
|
36
|
+
# config.current_api_actor_proc = -> { Current.user }
|
|
37
|
+
# Optional: force a canonical public base URL in generated API instructions.
|
|
38
|
+
# Falls back to Rails.application.routes.default_url_options, then request host.
|
|
39
|
+
# config.public_base_url = "https://blog.example.com"
|
|
29
40
|
end
|
data/lib/railspress/version.rb
CHANGED
data/lib/railspress.rb
CHANGED
|
@@ -10,6 +10,9 @@ module Railspress
|
|
|
10
10
|
attr_accessor :author_class_name,
|
|
11
11
|
:current_author_method,
|
|
12
12
|
:current_author_proc,
|
|
13
|
+
:current_api_actor_method,
|
|
14
|
+
:current_api_actor_proc,
|
|
15
|
+
:public_base_url,
|
|
13
16
|
:author_scope,
|
|
14
17
|
:author_display_method,
|
|
15
18
|
:words_per_minute,
|
|
@@ -18,19 +21,23 @@ module Railspress
|
|
|
18
21
|
:post_image_variants,
|
|
19
22
|
:inline_editing_check
|
|
20
23
|
|
|
21
|
-
attr_reader :authors_enabled, :post_images_enabled, :focal_points_enabled, :cms_enabled, :image_contexts
|
|
24
|
+
attr_reader :authors_enabled, :post_images_enabled, :focal_points_enabled, :cms_enabled, :api_enabled, :image_contexts
|
|
22
25
|
|
|
23
26
|
def initialize
|
|
24
27
|
@authors_enabled = false
|
|
25
28
|
@post_images_enabled = false
|
|
26
29
|
@focal_points_enabled = false
|
|
27
30
|
@cms_enabled = false
|
|
31
|
+
@api_enabled = false
|
|
28
32
|
@image_contexts = default_image_contexts
|
|
29
33
|
@post_image_variants = {}
|
|
30
34
|
@inline_editing_check = nil
|
|
31
35
|
@author_class_name = "User"
|
|
32
36
|
@current_author_method = :current_user
|
|
33
37
|
@current_author_proc = nil
|
|
38
|
+
@current_api_actor_method = :current_user
|
|
39
|
+
@current_api_actor_proc = nil
|
|
40
|
+
@public_base_url = nil
|
|
34
41
|
@author_scope = nil
|
|
35
42
|
@author_display_method = :name
|
|
36
43
|
@words_per_minute = 200
|
|
@@ -58,6 +65,11 @@ module Railspress
|
|
|
58
65
|
@cms_enabled = true
|
|
59
66
|
end
|
|
60
67
|
|
|
68
|
+
# Declarative setter: config.enable_api
|
|
69
|
+
def enable_api
|
|
70
|
+
@api_enabled = true
|
|
71
|
+
end
|
|
72
|
+
|
|
61
73
|
# Validate configuration after the configure block completes.
|
|
62
74
|
# This keeps the configure block order-independent.
|
|
63
75
|
def validate!
|
|
@@ -212,6 +224,10 @@ module Railspress
|
|
|
212
224
|
configuration.image_contexts
|
|
213
225
|
end
|
|
214
226
|
|
|
227
|
+
def api_enabled?
|
|
228
|
+
configuration.api_enabled
|
|
229
|
+
end
|
|
230
|
+
|
|
215
231
|
def author_class
|
|
216
232
|
configuration.author_class_name.constantize
|
|
217
233
|
end
|
|
@@ -231,6 +247,26 @@ module Railspress
|
|
|
231
247
|
configuration.author_display_method
|
|
232
248
|
end
|
|
233
249
|
|
|
250
|
+
# Returns a safe display string for an author record.
|
|
251
|
+
# Falls back through common attribute names when the configured display
|
|
252
|
+
# method is missing on the model.
|
|
253
|
+
def author_display_for(author)
|
|
254
|
+
return nil unless author
|
|
255
|
+
|
|
256
|
+
configured_method = author_display_method
|
|
257
|
+
if configured_method.present? && author.respond_to?(configured_method)
|
|
258
|
+
value = author.public_send(configured_method)
|
|
259
|
+
return value if value.present?
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
fallback_method = [ :name, :full_name, :display_name, :email, :email_address ]
|
|
263
|
+
.find { |method| author.respond_to?(method) && author.public_send(method).present? }
|
|
264
|
+
|
|
265
|
+
return author.public_send(fallback_method) if fallback_method
|
|
266
|
+
|
|
267
|
+
"Author ##{author.id || "unknown"}"
|
|
268
|
+
end
|
|
269
|
+
|
|
234
270
|
def current_author_method
|
|
235
271
|
configuration.current_author_method
|
|
236
272
|
end
|
|
@@ -239,6 +275,18 @@ module Railspress
|
|
|
239
275
|
configuration.current_author_proc
|
|
240
276
|
end
|
|
241
277
|
|
|
278
|
+
def current_api_actor_method
|
|
279
|
+
configuration.current_api_actor_method
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def current_api_actor_proc
|
|
283
|
+
configuration.current_api_actor_proc
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def public_base_url
|
|
287
|
+
configuration.public_base_url
|
|
288
|
+
end
|
|
289
|
+
|
|
242
290
|
def words_per_minute
|
|
243
291
|
configuration.words_per_minute
|
|
244
292
|
end
|