railspress-engine 1.2.1 → 1.3.1
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/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/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 +14 -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 +16 -0
- data/lib/railspress/engine.rb +12 -0
- data/lib/railspress/version.rb +1 -1
- data/lib/railspress.rb +73 -1
- metadata +26 -1
|
@@ -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"/>
|
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
|
|
@@ -8,6 +8,7 @@ Railspress.configure do |config|
|
|
|
8
8
|
# config.enable_authors
|
|
9
9
|
# config.author_class_name = "User"
|
|
10
10
|
# config.current_author_method = :current_user
|
|
11
|
+
# config.current_author_proc = -> { Current.user }
|
|
11
12
|
|
|
12
13
|
# === CMS Content Elements (opt-in) ===
|
|
13
14
|
# Adds content groups, content elements, and the cms_element/cms_value
|
|
@@ -26,4 +27,19 @@ Railspress.configure do |config|
|
|
|
26
27
|
# config.inline_editing_check = ->(context) {
|
|
27
28
|
# context.controller.current_user&.admin?
|
|
28
29
|
# }
|
|
30
|
+
|
|
31
|
+
# === API (opt-in) ===
|
|
32
|
+
# Enables API endpoints under /railspress/api/v1 and admin API key management.
|
|
33
|
+
# Requires Active Record Encryption keys in your host app config.
|
|
34
|
+
# Uncomment to enable:
|
|
35
|
+
# config.enable_api
|
|
36
|
+
# Optional: include a host auth concern into Railspress::Admin::BaseController.
|
|
37
|
+
# Define your concern in app/controllers/concerns/railspress_admin_auth.rb
|
|
38
|
+
# and set this to its constant name (String or Symbol).
|
|
39
|
+
# config.admin_auth_concern = "RailspressAdminAuth"
|
|
40
|
+
# config.current_api_actor_method = :current_user
|
|
41
|
+
# config.current_api_actor_proc = -> { Current.user if Current.user&.admin? }
|
|
42
|
+
# Optional: force a canonical public base URL in generated API instructions.
|
|
43
|
+
# Falls back to Rails.application.routes.default_url_options, then request host.
|
|
44
|
+
# config.public_base_url = "https://blog.example.com"
|
|
29
45
|
end
|
data/lib/railspress/engine.rb
CHANGED
|
@@ -41,6 +41,18 @@ module Railspress
|
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
# Allow host apps to inject their auth concern into the admin base controller.
|
|
45
|
+
# This keeps host auth logic in app/controllers concerns while Railspress handles reload-safe wiring.
|
|
46
|
+
initializer "railspress.admin_auth_concern" do |app|
|
|
47
|
+
app.config.to_prepare do
|
|
48
|
+
concern = Railspress.resolved_admin_auth_concern
|
|
49
|
+
next unless concern
|
|
50
|
+
next if Railspress::Admin::BaseController < concern
|
|
51
|
+
|
|
52
|
+
Railspress::Admin::BaseController.include(concern)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
44
56
|
# Configure importmap for Stimulus controllers
|
|
45
57
|
initializer "railspress.importmap", before: "importmap" do |app|
|
|
46
58
|
if app.respond_to?(:importmap)
|
data/lib/railspress/version.rb
CHANGED
data/lib/railspress.rb
CHANGED
|
@@ -10,6 +10,10 @@ 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
|
+
:admin_auth_concern,
|
|
16
|
+
:public_base_url,
|
|
13
17
|
:author_scope,
|
|
14
18
|
:author_display_method,
|
|
15
19
|
:words_per_minute,
|
|
@@ -18,19 +22,24 @@ module Railspress
|
|
|
18
22
|
:post_image_variants,
|
|
19
23
|
:inline_editing_check
|
|
20
24
|
|
|
21
|
-
attr_reader :authors_enabled, :post_images_enabled, :focal_points_enabled, :cms_enabled, :image_contexts
|
|
25
|
+
attr_reader :authors_enabled, :post_images_enabled, :focal_points_enabled, :cms_enabled, :api_enabled, :image_contexts
|
|
22
26
|
|
|
23
27
|
def initialize
|
|
24
28
|
@authors_enabled = false
|
|
25
29
|
@post_images_enabled = false
|
|
26
30
|
@focal_points_enabled = false
|
|
27
31
|
@cms_enabled = false
|
|
32
|
+
@api_enabled = false
|
|
28
33
|
@image_contexts = default_image_contexts
|
|
29
34
|
@post_image_variants = {}
|
|
30
35
|
@inline_editing_check = nil
|
|
31
36
|
@author_class_name = "User"
|
|
32
37
|
@current_author_method = :current_user
|
|
33
38
|
@current_author_proc = nil
|
|
39
|
+
@current_api_actor_method = :current_user
|
|
40
|
+
@current_api_actor_proc = nil
|
|
41
|
+
@admin_auth_concern = nil
|
|
42
|
+
@public_base_url = nil
|
|
34
43
|
@author_scope = nil
|
|
35
44
|
@author_display_method = :name
|
|
36
45
|
@words_per_minute = 200
|
|
@@ -58,6 +67,11 @@ module Railspress
|
|
|
58
67
|
@cms_enabled = true
|
|
59
68
|
end
|
|
60
69
|
|
|
70
|
+
# Declarative setter: config.enable_api
|
|
71
|
+
def enable_api
|
|
72
|
+
@api_enabled = true
|
|
73
|
+
end
|
|
74
|
+
|
|
61
75
|
# Validate configuration after the configure block completes.
|
|
62
76
|
# This keeps the configure block order-independent.
|
|
63
77
|
def validate!
|
|
@@ -212,6 +226,10 @@ module Railspress
|
|
|
212
226
|
configuration.image_contexts
|
|
213
227
|
end
|
|
214
228
|
|
|
229
|
+
def api_enabled?
|
|
230
|
+
configuration.api_enabled
|
|
231
|
+
end
|
|
232
|
+
|
|
215
233
|
def author_class
|
|
216
234
|
configuration.author_class_name.constantize
|
|
217
235
|
end
|
|
@@ -231,6 +249,26 @@ module Railspress
|
|
|
231
249
|
configuration.author_display_method
|
|
232
250
|
end
|
|
233
251
|
|
|
252
|
+
# Returns a safe display string for an author record.
|
|
253
|
+
# Falls back through common attribute names when the configured display
|
|
254
|
+
# method is missing on the model.
|
|
255
|
+
def author_display_for(author)
|
|
256
|
+
return nil unless author
|
|
257
|
+
|
|
258
|
+
configured_method = author_display_method
|
|
259
|
+
if configured_method.present? && author.respond_to?(configured_method)
|
|
260
|
+
value = author.public_send(configured_method)
|
|
261
|
+
return value if value.present?
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
fallback_method = [ :name, :full_name, :display_name, :email, :email_address ]
|
|
265
|
+
.find { |method| author.respond_to?(method) && author.public_send(method).present? }
|
|
266
|
+
|
|
267
|
+
return author.public_send(fallback_method) if fallback_method
|
|
268
|
+
|
|
269
|
+
"Author ##{author.id || "unknown"}"
|
|
270
|
+
end
|
|
271
|
+
|
|
234
272
|
def current_author_method
|
|
235
273
|
configuration.current_author_method
|
|
236
274
|
end
|
|
@@ -239,6 +277,40 @@ module Railspress
|
|
|
239
277
|
configuration.current_author_proc
|
|
240
278
|
end
|
|
241
279
|
|
|
280
|
+
def current_api_actor_method
|
|
281
|
+
configuration.current_api_actor_method
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def current_api_actor_proc
|
|
285
|
+
configuration.current_api_actor_proc
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def admin_auth_concern
|
|
289
|
+
configuration.admin_auth_concern
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def resolved_admin_auth_concern
|
|
293
|
+
concern = admin_auth_concern
|
|
294
|
+
return nil if concern.blank?
|
|
295
|
+
|
|
296
|
+
module_value = case concern
|
|
297
|
+
when Module then concern
|
|
298
|
+
when String, Symbol
|
|
299
|
+
concern_name = concern.to_s
|
|
300
|
+
concern_name.safe_constantize || concern_name.camelize.safe_constantize
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
unless module_value.is_a?(Module)
|
|
304
|
+
raise ConfigurationError, "admin_auth_concern must resolve to a Module. Got: #{concern.inspect}"
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
module_value
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def public_base_url
|
|
311
|
+
configuration.public_base_url
|
|
312
|
+
end
|
|
313
|
+
|
|
242
314
|
def words_per_minute
|
|
243
315
|
configuration.words_per_minute
|
|
244
316
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: railspress-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Avi Flombaum
|
|
@@ -157,6 +157,8 @@ files:
|
|
|
157
157
|
- app/assets/stylesheets/railspress/admin/utilities.css
|
|
158
158
|
- app/assets/stylesheets/railspress/admin/variables.css
|
|
159
159
|
- app/assets/stylesheets/railspress/application.css
|
|
160
|
+
- app/controllers/railspress/admin/agent_bootstrap_keys_controller.rb
|
|
161
|
+
- app/controllers/railspress/admin/api_keys_controller.rb
|
|
160
162
|
- app/controllers/railspress/admin/base_controller.rb
|
|
161
163
|
- app/controllers/railspress/admin/categories_controller.rb
|
|
162
164
|
- app/controllers/railspress/admin/cms_transfers_controller.rb
|
|
@@ -171,6 +173,17 @@ files:
|
|
|
171
173
|
- app/controllers/railspress/admin/posts_controller.rb
|
|
172
174
|
- app/controllers/railspress/admin/prototypes_controller.rb
|
|
173
175
|
- app/controllers/railspress/admin/tags_controller.rb
|
|
176
|
+
- app/controllers/railspress/api/v1/agent_key_exchanges_controller.rb
|
|
177
|
+
- app/controllers/railspress/api/v1/base_controller.rb
|
|
178
|
+
- app/controllers/railspress/api/v1/categories_controller.rb
|
|
179
|
+
- app/controllers/railspress/api/v1/concerns/post_serialization.rb
|
|
180
|
+
- app/controllers/railspress/api/v1/post_header_image_contexts_controller.rb
|
|
181
|
+
- app/controllers/railspress/api/v1/post_header_image_focal_points_controller.rb
|
|
182
|
+
- app/controllers/railspress/api/v1/post_header_images_controller.rb
|
|
183
|
+
- app/controllers/railspress/api/v1/post_imports_controller.rb
|
|
184
|
+
- app/controllers/railspress/api/v1/posts_controller.rb
|
|
185
|
+
- app/controllers/railspress/api/v1/prime_controller.rb
|
|
186
|
+
- app/controllers/railspress/api/v1/tags_controller.rb
|
|
174
187
|
- app/controllers/railspress/application_controller.rb
|
|
175
188
|
- app/helpers/railspress/admin_helper.rb
|
|
176
189
|
- app/helpers/railspress/application_helper.rb
|
|
@@ -190,6 +203,8 @@ files:
|
|
|
190
203
|
- app/models/concerns/railspress/has_focal_point.rb
|
|
191
204
|
- app/models/concerns/railspress/soft_deletable.rb
|
|
192
205
|
- app/models/concerns/railspress/taggable.rb
|
|
206
|
+
- app/models/railspress/agent_bootstrap_key.rb
|
|
207
|
+
- app/models/railspress/api_key.rb
|
|
193
208
|
- app/models/railspress/application_record.rb
|
|
194
209
|
- app/models/railspress/category.rb
|
|
195
210
|
- app/models/railspress/content_element.rb
|
|
@@ -210,6 +225,13 @@ files:
|
|
|
210
225
|
- app/views/layouts/action_text/contents/_content.html.erb
|
|
211
226
|
- app/views/layouts/railspress/admin.html.erb
|
|
212
227
|
- app/views/layouts/railspress/application.html.erb
|
|
228
|
+
- app/views/railspress/admin/agent_bootstrap_keys/_form.html.erb
|
|
229
|
+
- app/views/railspress/admin/agent_bootstrap_keys/new.html.erb
|
|
230
|
+
- app/views/railspress/admin/agent_bootstrap_keys/reveal.html.erb
|
|
231
|
+
- app/views/railspress/admin/api_keys/_form.html.erb
|
|
232
|
+
- app/views/railspress/admin/api_keys/index.html.erb
|
|
233
|
+
- app/views/railspress/admin/api_keys/new.html.erb
|
|
234
|
+
- app/views/railspress/admin/api_keys/reveal.html.erb
|
|
213
235
|
- app/views/railspress/admin/categories/_form.html.erb
|
|
214
236
|
- app/views/railspress/admin/categories/edit.html.erb
|
|
215
237
|
- app/views/railspress/admin/categories/index.html.erb
|
|
@@ -244,6 +266,7 @@ files:
|
|
|
244
266
|
- app/views/railspress/admin/posts/new.html.erb
|
|
245
267
|
- app/views/railspress/admin/posts/show.html.erb
|
|
246
268
|
- app/views/railspress/admin/prototypes/image_section.html.erb
|
|
269
|
+
- app/views/railspress/admin/shared/_copyable_textarea.html.erb
|
|
247
270
|
- app/views/railspress/admin/shared/_dropzone.html.erb
|
|
248
271
|
- app/views/railspress/admin/shared/_flash.html.erb
|
|
249
272
|
- app/views/railspress/admin/shared/_focal_point_editor.html.erb
|
|
@@ -275,6 +298,8 @@ files:
|
|
|
275
298
|
- db/migrate/20260207000001_add_unique_index_to_content_elements.rb
|
|
276
299
|
- db/migrate/20260211112812_add_image_hint_to_railspress_content_elements.rb
|
|
277
300
|
- db/migrate/20260211154040_add_required_to_railspress_content_elements.rb
|
|
301
|
+
- db/migrate/20260415000001_create_railspress_api_keys.rb
|
|
302
|
+
- db/migrate/20260415000002_create_railspress_agent_bootstrap_keys.rb
|
|
278
303
|
- lib/generators/railspress/entity/entity_generator.rb
|
|
279
304
|
- lib/generators/railspress/entity/templates/migration.rb.tt
|
|
280
305
|
- lib/generators/railspress/entity/templates/model.rb.tt
|