postnhost 0.2.0 → 0.2.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/CHANGELOG.md +8 -0
- data/README.md +16 -0
- data/app/controllers/postnhost/api/base_controller.rb +6 -1
- data/app/controllers/postnhost/api/v1/mcp_controller.rb +4 -0
- data/app/controllers/postnhost/articles_controller.rb +5 -6
- data/app/controllers/postnhost/oauth/authorizations_controller.rb +50 -0
- data/app/controllers/postnhost/oauth/metadata_controller.rb +25 -0
- data/app/controllers/postnhost/oauth/public_controller.rb +16 -0
- data/app/controllers/postnhost/oauth/registrations_controller.rb +29 -0
- data/app/controllers/postnhost/oauth/tokens_controller.rb +19 -0
- data/app/controllers/postnhost/sessions_controller.rb +2 -1
- data/app/models/postnhost/api_token.rb +2 -2
- data/app/models/postnhost/oauth_client.rb +29 -0
- data/app/models/postnhost/oauth_grant.rb +32 -0
- data/app/models/postnhost/user.rb +1 -0
- data/app/views/postnhost/api_tokens/index.html.erb +2 -2
- data/app/views/postnhost/oauth/authorizations/new.html.erb +18 -0
- data/config/routes.rb +9 -0
- data/db/migrate/20261025120000_add_mcp_oauth.rb +23 -0
- data/lib/postnhost/engine.rb +7 -0
- data/lib/postnhost/oauth_discovery.rb +22 -0
- data/lib/postnhost/version.rb +1 -1
- metadata +11 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eeff5b8e47da4271aa4b1b5240c98fa2e5fcf89ef59ea86f775c41c7fbf3c64e
|
|
4
|
+
data.tar.gz: 8784beb69048bba221cfbda8306a57eb1366b470996b02b4325bb2dec8feaacc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8317f464ba035086b326e0537c157f8957ad250080201d32af7e74955e0909e546f59e13bf2dfcbdd648e4476be75a3bbf6dea0fcee4df3ed90a47fb04c718d0
|
|
7
|
+
data.tar.gz: db9ff2f49ee7727d9e8156288de193e01992eef8f0b1cf06aa59fe9a41339b46bc8d5370d9c1cf72f45edceaeac8953bc8dcfd36b717c8194090b443a199dac1
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to PostnHost are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.2.1] - 2026-11-03
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Show all users' articles in the CMS article list and suggestion picker, including articles whose creator has been deleted.
|
|
10
|
+
- Preserve shared article suggestions when the editor displays validation errors.
|
|
11
|
+
|
|
5
12
|
## [0.2.0] - 2026-10-24
|
|
6
13
|
|
|
7
14
|
### Added
|
|
@@ -56,6 +63,7 @@ First open source release of PostnHost.
|
|
|
56
63
|
|
|
57
64
|
Authored by Kirill Shevchenko (Ruby/Rails engineering) and Maxim Sova (technical SEO expertise).
|
|
58
65
|
|
|
66
|
+
[0.2.1]: https://github.com/postnhost/postnhost/compare/v0.2.0...v0.2.1
|
|
59
67
|
[0.2.0]: https://github.com/postnhost/postnhost/compare/v0.1.1...v0.2.0
|
|
60
68
|
[0.1.1]: https://github.com/postnhost/postnhost/compare/v0.1.0...v0.1.1
|
|
61
69
|
[0.1.0]: https://github.com/postnhost/postnhost/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -306,6 +306,22 @@ and publishing workflows.
|
|
|
306
306
|
Discover commands at `/api/v1/commands`, OpenAPI at `/api/v1/openapi`, and
|
|
307
307
|
connect MCP clients to `/api/v1/mcp`, relative to the engine mount path.
|
|
308
308
|
|
|
309
|
+
MCP clients can authenticate through OAuth: add the endpoint URL, sign in to the
|
|
310
|
+
CMS in your browser, and approve the connection. OAuth grants full CMS access
|
|
311
|
+
with a single `mcp` scope. Access tokens do not expire; revoke individual
|
|
312
|
+
connections under **API & MCP**. No refresh tokens or copied secrets are needed.
|
|
313
|
+
Existing manually created bearer tokens remain supported.
|
|
314
|
+
|
|
315
|
+
The engine provides client registration, authorization-code exchange with S256
|
|
316
|
+
PKCE, and discovery metadata. Authorization codes expire after five minutes and
|
|
317
|
+
can only be used once. OAuth access tokens are bound to the MCP endpoint.
|
|
318
|
+
After upgrading, install and run the new engine migration:
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
bin/rails postnhost:install:migrations
|
|
322
|
+
bin/rails db:migrate
|
|
323
|
+
```
|
|
324
|
+
|
|
309
325
|
## Routes
|
|
310
326
|
|
|
311
327
|
The engine mounts at `/blog` by default. You can change this in `config/routes.rb`:
|
|
@@ -14,13 +14,18 @@ module Postnhost
|
|
|
14
14
|
def authenticate_token!
|
|
15
15
|
scheme, secret = request.authorization.to_s.split(" ", 2)
|
|
16
16
|
token = ApiToken.authenticate(secret) if scheme&.casecmp?("Bearer")
|
|
17
|
+
token = nil if token&.resource.present? && token.resource != request.base_url + request.path
|
|
17
18
|
@api_user = token&.user
|
|
18
19
|
return if api_user
|
|
19
20
|
|
|
20
|
-
response.set_header("WWW-Authenticate",
|
|
21
|
+
response.set_header("WWW-Authenticate", bearer_challenge)
|
|
21
22
|
render json: { errors: ["A valid bearer token is required"] }, status: :unauthorized
|
|
22
23
|
end
|
|
23
24
|
|
|
25
|
+
def bearer_challenge
|
|
26
|
+
'Bearer realm="PostnHost"'
|
|
27
|
+
end
|
|
28
|
+
|
|
24
29
|
def prevent_caching
|
|
25
30
|
response.set_header("Cache-Control", "no-store")
|
|
26
31
|
end
|
|
@@ -23,6 +23,10 @@ module Postnhost
|
|
|
23
23
|
|
|
24
24
|
private
|
|
25
25
|
|
|
26
|
+
def bearer_challenge
|
|
27
|
+
%(Bearer resource_metadata="#{postnhost.oauth_resource_metadata_url}", scope="mcp")
|
|
28
|
+
end
|
|
29
|
+
|
|
26
30
|
def validate_transport!
|
|
27
31
|
origin = request.headers["Origin"]
|
|
28
32
|
if origin.present? && origin != request.base_url
|
|
@@ -11,10 +11,9 @@ module Postnhost
|
|
|
11
11
|
@status = Postnhost::Article.normalize_status(params[:status])
|
|
12
12
|
@sort = Postnhost::Article.normalize_sort(params[:sort])
|
|
13
13
|
|
|
14
|
-
articles =
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.sorted_by(@sort)
|
|
14
|
+
articles = Postnhost::Article.includes(:language, :article_snapshot)
|
|
15
|
+
.with_status(@status)
|
|
16
|
+
.sorted_by(@sort)
|
|
18
17
|
|
|
19
18
|
@pagy, @articles = pagy(:offset, articles, limit: PAGE_SIZE)
|
|
20
19
|
end
|
|
@@ -30,7 +29,7 @@ module Postnhost
|
|
|
30
29
|
@categories = Postnhost::Category.all
|
|
31
30
|
@users = Postnhost::User.order(:name, :email)
|
|
32
31
|
@languages = Postnhost::Language.order(default: :desc, name: :asc)
|
|
33
|
-
@articles =
|
|
32
|
+
@articles = Postnhost::Article.where.not(id: @article.id).order(:title)
|
|
34
33
|
end
|
|
35
34
|
|
|
36
35
|
def update
|
|
@@ -45,7 +44,7 @@ module Postnhost
|
|
|
45
44
|
@categories = Postnhost::Category.all
|
|
46
45
|
@users = Postnhost::User.order(:name, :email)
|
|
47
46
|
@languages = Postnhost::Language.order(default: :desc, name: :asc)
|
|
48
|
-
@articles =
|
|
47
|
+
@articles = Postnhost::Article.where.not(id: @article.id).order(:title)
|
|
49
48
|
render :edit, status: :unprocessable_content
|
|
50
49
|
end
|
|
51
50
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Postnhost
|
|
2
|
+
module Oauth
|
|
3
|
+
class AuthorizationsController < ApplicationController
|
|
4
|
+
protect_from_forgery with: :exception
|
|
5
|
+
before_action :prevent_caching
|
|
6
|
+
before_action :validate_request!
|
|
7
|
+
before_action :require_oauth_user!
|
|
8
|
+
layout "postnhost/product"
|
|
9
|
+
|
|
10
|
+
def new; end
|
|
11
|
+
|
|
12
|
+
def create
|
|
13
|
+
result = if params[:decision] == "approve"
|
|
14
|
+
{ code: OauthGrant.issue!(oauth_client: @client, user: current_user,
|
|
15
|
+
**@oauth_request.slice(:redirect_uri, :resource, :code_challenge).symbolize_keys) }
|
|
16
|
+
else
|
|
17
|
+
{ error: "access_denied" }
|
|
18
|
+
end
|
|
19
|
+
uri = URI.parse(@oauth_request[:redirect_uri])
|
|
20
|
+
uri.query = [uri.query.presence, { **result, state: @oauth_request[:state] }.to_query].compact.join("&")
|
|
21
|
+
redirect_to uri.to_s, allow_other_host: true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def prevent_caching
|
|
27
|
+
response.set_header("Cache-Control", "no-store")
|
|
28
|
+
response.set_header("Referrer-Policy", "no-referrer")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def validate_request!
|
|
32
|
+
@oauth_request = params.permit(:client_id, :redirect_uri, :response_type, :scope, :state,
|
|
33
|
+
:code_challenge, :code_challenge_method, :resource).to_h.with_indifferent_access
|
|
34
|
+
@client = OauthClient.find_by(client_id: @oauth_request[:client_id])
|
|
35
|
+
valid = @client&.redirect_uris&.include?(@oauth_request[:redirect_uri]) &&
|
|
36
|
+
@oauth_request[:response_type] == "code" && @oauth_request[:code_challenge_method] == "S256" &&
|
|
37
|
+
@oauth_request[:code_challenge].to_s.match?(/\A[A-Za-z0-9_-]{43}\z/) &&
|
|
38
|
+
@oauth_request[:scope].to_s.in?(["", "mcp"]) && @oauth_request[:resource] == postnhost.api_v1_mcp_url
|
|
39
|
+
render plain: "Invalid OAuth authorization request", status: :bad_request unless valid
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def require_oauth_user!
|
|
43
|
+
return if user_signed_in?
|
|
44
|
+
|
|
45
|
+
session[:oauth_return_to] = postnhost.oauth_authorize_path(@oauth_request)
|
|
46
|
+
authenticate_user!
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Postnhost
|
|
2
|
+
module Oauth
|
|
3
|
+
class MetadataController < PublicController
|
|
4
|
+
def resource
|
|
5
|
+
render json: {
|
|
6
|
+
resource: postnhost.api_v1_mcp_url,
|
|
7
|
+
authorization_servers: [request.base_url + request.script_name],
|
|
8
|
+
scopes_supported: ["mcp"], bearer_methods_supported: ["header"]
|
|
9
|
+
}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def server
|
|
13
|
+
render json: {
|
|
14
|
+
issuer: request.base_url + request.script_name,
|
|
15
|
+
authorization_endpoint: postnhost.oauth_authorize_url,
|
|
16
|
+
token_endpoint: postnhost.oauth_token_url,
|
|
17
|
+
registration_endpoint: postnhost.oauth_register_url,
|
|
18
|
+
response_types_supported: ["code"], grant_types_supported: ["authorization_code"],
|
|
19
|
+
token_endpoint_auth_methods_supported: ["none"],
|
|
20
|
+
code_challenge_methods_supported: ["S256"], scopes_supported: ["mcp"]
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Postnhost
|
|
2
|
+
module Oauth
|
|
3
|
+
# OAuth protocol endpoints do not use CMS sessions for authentication.
|
|
4
|
+
# rubocop:disable-next Rails/ApplicationController
|
|
5
|
+
class PublicController < ActionController::Base
|
|
6
|
+
skip_forgery_protection
|
|
7
|
+
before_action :prevent_caching
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def prevent_caching
|
|
12
|
+
response.set_header("Cache-Control", "no-store")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Postnhost
|
|
2
|
+
module Oauth
|
|
3
|
+
class RegistrationsController < PublicController
|
|
4
|
+
def create
|
|
5
|
+
unless params[:token_endpoint_auth_method].to_s.in?(["", "none"]) &&
|
|
6
|
+
supported_grants? &&
|
|
7
|
+
(params[:response_types].nil? || params[:response_types] == ["code"])
|
|
8
|
+
render json: { error: "invalid_client_metadata" }, status: :bad_request
|
|
9
|
+
return
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
client = OauthClient.create!(name: params[:client_name].presence || "MCP client", redirect_uris: params[:redirect_uris])
|
|
13
|
+
render json: {
|
|
14
|
+
client_id: client.client_id, client_name: client.name, redirect_uris: client.redirect_uris,
|
|
15
|
+
token_endpoint_auth_method: "none", grant_types: ["authorization_code"], response_types: ["code"]
|
|
16
|
+
}, status: :created
|
|
17
|
+
rescue ActiveRecord::RecordInvalid
|
|
18
|
+
render json: { error: "invalid_client_metadata" }, status: :bad_request
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def supported_grants?
|
|
24
|
+
grants = params[:grant_types] || ["authorization_code"]
|
|
25
|
+
grants.is_a?(Array) && grants.include?("authorization_code") && (grants - %w[authorization_code refresh_token]).empty?
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Postnhost
|
|
2
|
+
module Oauth
|
|
3
|
+
class TokensController < PublicController
|
|
4
|
+
def create
|
|
5
|
+
unless params[:grant_type] == "authorization_code"
|
|
6
|
+
render json: { error: "unsupported_grant_type" }, status: :bad_request
|
|
7
|
+
return
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
secret = OauthGrant.exchange(**%i[code client_id redirect_uri resource code_verifier].index_with { |key| params[key].to_s })
|
|
11
|
+
if secret
|
|
12
|
+
render json: { access_token: secret, token_type: "Bearer", scope: "mcp" }
|
|
13
|
+
else
|
|
14
|
+
render json: { error: "invalid_grant" }, status: :bad_request
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -9,8 +9,9 @@ module Postnhost
|
|
|
9
9
|
user = Postnhost::User.find_by(email: session_params[:email].to_s.downcase.strip)
|
|
10
10
|
|
|
11
11
|
if user&.authenticate(session_params[:password].to_s)
|
|
12
|
+
oauth_return_to = session[:oauth_return_to]
|
|
12
13
|
sign_in(user)
|
|
13
|
-
redirect_to articles_path
|
|
14
|
+
redirect_to oauth_return_to || articles_path
|
|
14
15
|
else
|
|
15
16
|
flash.now[:alert] = "Invalid email or password."
|
|
16
17
|
render :new, status: :unprocessable_content
|
|
@@ -7,9 +7,9 @@ module Postnhost
|
|
|
7
7
|
validates :name, :token_digest, presence: true
|
|
8
8
|
validates :token_digest, uniqueness: true
|
|
9
9
|
|
|
10
|
-
def self.issue!(user:, name:)
|
|
10
|
+
def self.issue!(user:, name:, resource: nil)
|
|
11
11
|
secret = "pnh_#{SecureRandom.urlsafe_base64(32)}"
|
|
12
|
-
token = create!(user:, name:, token_digest: Digest::SHA256.hexdigest(secret))
|
|
12
|
+
token = create!(user:, name:, resource:, token_digest: Digest::SHA256.hexdigest(secret))
|
|
13
13
|
[token, secret]
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
|
|
3
|
+
module Postnhost
|
|
4
|
+
class OauthClient < ApplicationRecord
|
|
5
|
+
has_many :oauth_grants, class_name: "Postnhost::OauthGrant", dependent: :destroy
|
|
6
|
+
has_secure_token :client_id, length: 32
|
|
7
|
+
|
|
8
|
+
validates :name, presence: true, length: { maximum: 100 }
|
|
9
|
+
validate :safe_redirect_uris
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def safe_redirect_uris
|
|
14
|
+
return if redirect_uris.is_a?(Array) && redirect_uris.size.between?(1, 10) && redirect_uris.all? { |value| safe_redirect_uri?(value) }
|
|
15
|
+
|
|
16
|
+
errors.add(:redirect_uris, "must contain HTTPS URLs or HTTP loopback URLs")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def safe_redirect_uri?(value)
|
|
20
|
+
return false unless value.is_a?(String) && value.length <= 2048
|
|
21
|
+
|
|
22
|
+
uri = URI.parse(value)
|
|
23
|
+
uri.host.present? && uri.userinfo.nil? && uri.fragment.nil? &&
|
|
24
|
+
(uri.scheme == "https" || (uri.scheme == "http" && %w[localhost 127.0.0.1 [::1]].include?(uri.host)))
|
|
25
|
+
rescue URI::InvalidURIError
|
|
26
|
+
false
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require "base64"
|
|
2
|
+
require "digest"
|
|
3
|
+
|
|
4
|
+
module Postnhost
|
|
5
|
+
class OauthGrant < ApplicationRecord
|
|
6
|
+
belongs_to :oauth_client, class_name: "Postnhost::OauthClient"
|
|
7
|
+
belongs_to :user, class_name: "Postnhost::User"
|
|
8
|
+
|
|
9
|
+
def self.issue!(**attributes)
|
|
10
|
+
code = SecureRandom.urlsafe_base64(32)
|
|
11
|
+
create!(**attributes, code_digest: Digest::SHA256.hexdigest(code), expires_at: 5.minutes.from_now)
|
|
12
|
+
code
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.exchange(code:, client_id:, redirect_uri:, resource:, code_verifier:)
|
|
16
|
+
return unless code_verifier.match?(/\A[A-Za-z0-9._~-]{43,128}\z/)
|
|
17
|
+
|
|
18
|
+
transaction do
|
|
19
|
+
grant = lock.find_by(code_digest: Digest::SHA256.hexdigest(code))
|
|
20
|
+
return unless grant&.expires_at&.future? && grant.oauth_client.client_id == client_id &&
|
|
21
|
+
grant.redirect_uri == redirect_uri && grant.resource == resource
|
|
22
|
+
|
|
23
|
+
challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(code_verifier), padding: false)
|
|
24
|
+
return unless ActiveSupport::SecurityUtils.secure_compare(grant.code_challenge, challenge)
|
|
25
|
+
|
|
26
|
+
token = ApiToken.issue!(user: grant.user, name: grant.oauth_client.name, resource: grant.resource)
|
|
27
|
+
grant.destroy!
|
|
28
|
+
token.last
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
<section class="<%= form_card_classes %>" aria-labelledby="connection-title">
|
|
36
36
|
<div>
|
|
37
37
|
<h2 id="connection-title" class="text-lg font-medium text-gray-900">Connection details</h2>
|
|
38
|
-
<p class="mt-2 text-sm text-gray-500">
|
|
38
|
+
<p class="mt-2 text-sm text-gray-500">Connect an MCP client by signing in through your browser, or use a token for the management API.</p>
|
|
39
39
|
</div>
|
|
40
40
|
<dl class="space-y-5">
|
|
41
41
|
<div>
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
<div>
|
|
46
46
|
<dt class="text-sm font-medium text-gray-700">MCP · v1</dt>
|
|
47
47
|
<dd class="mt-2 rounded-xl bg-gray-50 p-3 text-xs text-gray-900 break-all"><code><%= api_v1_mcp_url %></code></dd>
|
|
48
|
-
<dd class="mt-2 text-xs text-gray-500">Connect using Streamable HTTP.</dd>
|
|
48
|
+
<dd class="mt-2 text-xs text-gray-500">Connect using Streamable HTTP and OAuth. Access remains active until revoked.</dd>
|
|
49
49
|
</div>
|
|
50
50
|
<div>
|
|
51
51
|
<dt class="text-sm font-medium text-gray-700">Authorization header</dt>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<% content_for :title, "Connect MCP" %>
|
|
2
|
+
|
|
3
|
+
<section class="max-w-xl space-y-6" aria-labelledby="oauth-title">
|
|
4
|
+
<h1 id="oauth-title" class="text-xl font-medium text-gray-900">Connect <%= @client.name %>?</h1>
|
|
5
|
+
<p class="text-gray-700">This application will be able to read and manage your CMS content and settings, including publishing and deleting content.</p>
|
|
6
|
+
<p class="text-sm text-gray-500">Access remains active until you revoke it in API & MCP.</p>
|
|
7
|
+
<dl class="space-y-2">
|
|
8
|
+
<dt class="text-sm font-medium text-gray-700">Application callback</dt>
|
|
9
|
+
<dd class="text-sm text-gray-700 break-all"><%= @oauth_request[:redirect_uri] %></dd>
|
|
10
|
+
</dl>
|
|
11
|
+
<%= form_with url: oauth_authorize_path, data: { turbo: false }, class: "flex gap-3" do |form| %>
|
|
12
|
+
<% @oauth_request.each do |key, value| %>
|
|
13
|
+
<%= hidden_field_tag key, value %>
|
|
14
|
+
<% end %>
|
|
15
|
+
<%= form.button "Connect", name: "decision", value: "approve", class: button_classes %>
|
|
16
|
+
<%= form.button "Cancel", name: "decision", value: "deny", class: button_classes(variant: :secondary) %>
|
|
17
|
+
<% end %>
|
|
18
|
+
</section>
|
data/config/routes.rb
CHANGED
|
@@ -33,6 +33,15 @@ Postnhost::Engine.routes.draw do
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
# Admin routes
|
|
36
|
+
namespace :oauth do
|
|
37
|
+
get "resource_metadata", to: "metadata#resource"
|
|
38
|
+
get "server_metadata", to: "metadata#server"
|
|
39
|
+
get "authorize", to: "authorizations#new"
|
|
40
|
+
post "authorize", to: "authorizations#create"
|
|
41
|
+
post "register", to: "registrations#create"
|
|
42
|
+
post "token", to: "tokens#create"
|
|
43
|
+
end
|
|
44
|
+
|
|
36
45
|
resources :api_tokens, only: %i[index create destroy]
|
|
37
46
|
resource :onboarding, only: %i[show update], controller: "onboarding"
|
|
38
47
|
resource :session, only: %i[new create destroy], controller: "sessions"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class AddMcpOauth < ActiveRecord::Migration[7.2]
|
|
2
|
+
def change
|
|
3
|
+
create_table :postnhost_oauth_clients do |t|
|
|
4
|
+
t.string :client_id, null: false
|
|
5
|
+
t.string :name, null: false
|
|
6
|
+
t.json :redirect_uris, null: false
|
|
7
|
+
t.timestamps
|
|
8
|
+
end
|
|
9
|
+
add_index :postnhost_oauth_clients, :client_id, unique: true
|
|
10
|
+
|
|
11
|
+
create_table :postnhost_oauth_grants do |t|
|
|
12
|
+
t.references :oauth_client, null: false, foreign_key: { to_table: :postnhost_oauth_clients }
|
|
13
|
+
t.references :user, null: false, foreign_key: { to_table: :postnhost_users }
|
|
14
|
+
t.string :code_digest, null: false
|
|
15
|
+
t.string :code_challenge, null: false
|
|
16
|
+
t.string :redirect_uri, null: false
|
|
17
|
+
t.string :resource, null: false
|
|
18
|
+
t.datetime :expires_at, null: false
|
|
19
|
+
end
|
|
20
|
+
add_index :postnhost_oauth_grants, :code_digest, unique: true
|
|
21
|
+
add_column :postnhost_api_tokens, :resource, :string
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/postnhost/engine.rb
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
require_relative "oauth_discovery"
|
|
2
|
+
|
|
1
3
|
module Postnhost
|
|
2
4
|
class Engine < ::Rails::Engine
|
|
3
5
|
isolate_namespace Postnhost
|
|
4
6
|
|
|
7
|
+
initializer "postnhost.oauth" do |app|
|
|
8
|
+
app.middleware.use Postnhost::OauthDiscovery
|
|
9
|
+
app.config.filter_parameters += %i[code code_verifier access_token]
|
|
10
|
+
end
|
|
11
|
+
|
|
5
12
|
# Initialize configuration before other initializers
|
|
6
13
|
initializer "postnhost.configuration", before: :load_config_initializers do
|
|
7
14
|
Postnhost.configure
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Postnhost
|
|
2
|
+
# Well-known discovery lives at the origin even when the engine is mounted at /blog.
|
|
3
|
+
class OauthDiscovery
|
|
4
|
+
def initialize(app)
|
|
5
|
+
@app = app
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def call(env)
|
|
9
|
+
path = env["PATH_INFO"]
|
|
10
|
+
if env["REQUEST_METHOD"] == "GET" && path.match?(%r{\A/\.well-known/oauth-authorization-server(?:/.*)?\z})
|
|
11
|
+
status, headers, body = @app.call(env)
|
|
12
|
+
return [status, headers, body] unless status == 404
|
|
13
|
+
|
|
14
|
+
body.close if body.respond_to?(:close)
|
|
15
|
+
mount = path.delete_prefix("/.well-known/oauth-authorization-server")
|
|
16
|
+
return @app.call(env.merge("PATH_INFO" => "#{mount}/oauth/server_metadata"))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
@app.call(env)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/postnhost/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: postnhost
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kirill Shevchenko
|
|
@@ -338,6 +338,11 @@ files:
|
|
|
338
338
|
- app/controllers/postnhost/category_variants_controller.rb
|
|
339
339
|
- app/controllers/postnhost/languages_controller.rb
|
|
340
340
|
- app/controllers/postnhost/management_preview_controller.rb
|
|
341
|
+
- app/controllers/postnhost/oauth/authorizations_controller.rb
|
|
342
|
+
- app/controllers/postnhost/oauth/metadata_controller.rb
|
|
343
|
+
- app/controllers/postnhost/oauth/public_controller.rb
|
|
344
|
+
- app/controllers/postnhost/oauth/registrations_controller.rb
|
|
345
|
+
- app/controllers/postnhost/oauth/tokens_controller.rb
|
|
341
346
|
- app/controllers/postnhost/onboarding_controller.rb
|
|
342
347
|
- app/controllers/postnhost/page_variants_controller.rb
|
|
343
348
|
- app/controllers/postnhost/pages/images_controller.rb
|
|
@@ -416,6 +421,8 @@ files:
|
|
|
416
421
|
- app/models/postnhost/language.rb
|
|
417
422
|
- app/models/postnhost/navigation.rb
|
|
418
423
|
- app/models/postnhost/navigation_item.rb
|
|
424
|
+
- app/models/postnhost/oauth_client.rb
|
|
425
|
+
- app/models/postnhost/oauth_grant.rb
|
|
419
426
|
- app/models/postnhost/page.rb
|
|
420
427
|
- app/models/postnhost/page_variant.rb
|
|
421
428
|
- app/models/postnhost/public_site_revision.rb
|
|
@@ -537,6 +544,7 @@ files:
|
|
|
537
544
|
- app/views/postnhost/languages/index.html.erb
|
|
538
545
|
- app/views/postnhost/languages/new.html.erb
|
|
539
546
|
- app/views/postnhost/languages/show.html.erb
|
|
547
|
+
- app/views/postnhost/oauth/authorizations/new.html.erb
|
|
540
548
|
- app/views/postnhost/onboarding/_post_install_checklist.html.erb
|
|
541
549
|
- app/views/postnhost/onboarding/_step_1.html.erb
|
|
542
550
|
- app/views/postnhost/onboarding/_step_2.html.erb
|
|
@@ -690,6 +698,7 @@ files:
|
|
|
690
698
|
- db/migrate/20260709090000_create_snapshots.rb
|
|
691
699
|
- db/migrate/20260726130000_create_site_scripts.rb
|
|
692
700
|
- db/migrate/20261008120000_create_api_tokens.rb
|
|
701
|
+
- db/migrate/20261025120000_add_mcp_oauth.rb
|
|
693
702
|
- lib/generators/postnhost/install/install_generator.rb
|
|
694
703
|
- lib/generators/postnhost/install/templates/carrierwave.rb
|
|
695
704
|
- lib/generators/postnhost/install/templates/postnhost.rb
|
|
@@ -704,6 +713,7 @@ files:
|
|
|
704
713
|
- lib/postnhost/css_build.rb
|
|
705
714
|
- lib/postnhost/css_scope.rb
|
|
706
715
|
- lib/postnhost/engine.rb
|
|
716
|
+
- lib/postnhost/oauth_discovery.rb
|
|
707
717
|
- lib/postnhost/sample_data.rb
|
|
708
718
|
- lib/postnhost/settings/i18n_overrides.rb
|
|
709
719
|
- lib/postnhost/settings/i18n_patch.rb
|