decidim-api 0.30.1 → 0.31.0.rc1
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/controllers/decidim/api/application_controller.rb +20 -0
- data/app/controllers/decidim/api/queries_controller.rb +33 -1
- data/app/controllers/decidim/api/sessions_controller.rb +61 -0
- data/app/models/decidim/api/api_user.rb +82 -0
- data/app/models/decidim/api/jwt_denylist.rb +11 -0
- data/app/packs/entrypoints/decidim_api_graphiql.js +2 -1
- data/app/presenters/decidim/api/api_user_presenter.rb +23 -0
- data/config/assets.rb +2 -2
- data/config/initializers/devise.rb +26 -0
- data/config/routes.rb +8 -0
- data/decidim-api.gemspec +2 -1
- data/docs/usage.md +98 -8
- data/lib/decidim/api/component_mutation_type.rb +19 -0
- data/lib/decidim/api/devise.rb +12 -0
- data/lib/decidim/api/engine.rb +2 -2
- data/lib/decidim/api/graphql_permissions.rb +125 -0
- data/lib/decidim/api/mutation_type.rb +10 -0
- data/lib/decidim/api/required_scopes.rb +31 -0
- data/lib/decidim/api/test/component_context.rb +17 -18
- data/lib/decidim/api/test/factories.rb +33 -0
- data/lib/decidim/api/test/mutation_context.rb +38 -0
- data/lib/decidim/api/test/shared_examples/amendable_interface_examples.rb +14 -0
- data/lib/decidim/api/test/shared_examples/amendable_proposals_interface_examples.rb +50 -0
- data/lib/decidim/api/test/shared_examples/attachable_interface_examples.rb +40 -0
- data/lib/decidim/api/test/shared_examples/authorable_interface_examples.rb +46 -0
- data/lib/decidim/api/test/shared_examples/categories_container_examples.rb +22 -0
- data/lib/decidim/api/test/shared_examples/categorizable_interface_examples.rb +27 -0
- data/lib/decidim/api/test/shared_examples/coauthorable_interface_examples.rb +77 -0
- data/lib/decidim/api/test/shared_examples/commentable_interface_examples.rb +13 -0
- data/lib/decidim/api/test/shared_examples/fingerprintable_interface_examples.rb +17 -0
- data/lib/decidim/api/test/shared_examples/followable_interface_examples.rb +13 -0
- data/lib/decidim/api/test/shared_examples/input_filter_examples.rb +77 -0
- data/lib/decidim/api/test/shared_examples/input_sort_examples.rb +126 -0
- data/lib/decidim/api/test/shared_examples/likeable_interface_examples.rb +22 -0
- data/lib/decidim/api/test/shared_examples/localizable_interface_examples.rb +29 -0
- data/lib/decidim/api/test/shared_examples/participatory_space_resourcable_interface_examples.rb +61 -0
- data/lib/decidim/api/test/shared_examples/referable_interface_examples.rb +13 -0
- data/lib/decidim/api/test/shared_examples/scopable_interface_examples.rb +19 -0
- data/lib/decidim/api/test/shared_examples/statistics_examples.rb +30 -16
- data/lib/decidim/api/test/shared_examples/taxonomizable_interface_examples.rb +20 -0
- data/lib/decidim/api/test/shared_examples/timestamps_interface_examples.rb +21 -0
- data/lib/decidim/api/test/shared_examples/traceable_interface_examples.rb +49 -0
- data/lib/decidim/api/test/type_context.rb +9 -1
- data/lib/decidim/api/test.rb +22 -0
- data/lib/decidim/api/types/base_mutation.rb +5 -1
- data/lib/decidim/api/types/base_object.rb +4 -69
- data/lib/decidim/api/types.rb +3 -0
- data/lib/decidim/api/version.rb +1 -1
- data/lib/decidim/api.rb +25 -5
- data/lib/devise/models/api_authenticatable.rb +30 -0
- data/lib/devise/strategies/api_authenticatable.rb +21 -0
- data/lib/warden/jwt_auth/decidim_overrides.rb +42 -0
- metadata +66 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ae9105250ff986390b7ea4387812c51f7f2f69cbf0b609e2146ccf64ce6b76fc
|
4
|
+
data.tar.gz: 47ac99a3cc8d3fefc0d1adb494f182a30c7131ccc81fdf2a8e7614fee8265062
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ea6328163dffd2c5b6d3d2cf1025017346a2264ebac6c5179897d6857329802aa345b6bd9e67635df91320ca9daa9c65a0e29e46d81bfccd160c80101343c208
|
7
|
+
data.tar.gz: 65c3fab8a6a6e163dd4d4d9b40d0fc8c7e00c85abaf462e9e95b9a0d02077d381f2a35bfceb03124faa7720a5674865fce76b9e03459cd073ec9068d267988b1
|
@@ -5,6 +5,8 @@ module Decidim
|
|
5
5
|
# Base controller for `decidim-api`. All other controllers inherit from this.
|
6
6
|
class ApplicationController < ::DecidimController
|
7
7
|
skip_before_action :verify_authenticity_token
|
8
|
+
before_action :ensure_api_authenticated!
|
9
|
+
|
8
10
|
include NeedsOrganization
|
9
11
|
include UseOrganizationTimeZone
|
10
12
|
include NeedsPermission
|
@@ -22,6 +24,24 @@ module Decidim
|
|
22
24
|
def permission_scope
|
23
25
|
:public
|
24
26
|
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def ensure_api_authenticated!
|
31
|
+
return unless Decidim::Api.force_api_authentication
|
32
|
+
return if user_signed_in?
|
33
|
+
|
34
|
+
respond_to do |format|
|
35
|
+
format.html do
|
36
|
+
flash[:warning] = t("actions.login_before_access", scope: "decidim.core")
|
37
|
+
store_location_for(:user, request.path)
|
38
|
+
redirect_to decidim.new_user_session_path
|
39
|
+
end
|
40
|
+
format.json do
|
41
|
+
render json: { error: "Access denied" }, status: :unauthorized
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
25
45
|
end
|
26
46
|
end
|
27
47
|
end
|
@@ -29,10 +29,42 @@ module Decidim
|
|
29
29
|
def context
|
30
30
|
{
|
31
31
|
current_organization:,
|
32
|
-
current_user:
|
32
|
+
current_user: api_user,
|
33
|
+
scopes: api_scopes
|
33
34
|
}
|
34
35
|
end
|
35
36
|
|
37
|
+
def api_user
|
38
|
+
@api_user = current_api_user || current_user
|
39
|
+
end
|
40
|
+
|
41
|
+
# Determines the scopes for the user for API requests.
|
42
|
+
#
|
43
|
+
# @return [Doorkeeper::OAuth::Scopes]
|
44
|
+
def api_scopes
|
45
|
+
if doorkeeper_token
|
46
|
+
doorkeeper_token.scopes
|
47
|
+
elsif api_user.present?
|
48
|
+
# In case a doorkeeper token is not available, we assume the user is
|
49
|
+
# either:
|
50
|
+
# - A regular authenticated user in Decidim using the API locally
|
51
|
+
# from within Decidim using the system with the cookie based
|
52
|
+
# authentication
|
53
|
+
# - A Decidim::Api::ApiUser authenticated through the `/api/sign_in`
|
54
|
+
# endpoint for machine-to-machine integrations using the system with
|
55
|
+
# the assigned JSON Web Token (JWT).
|
56
|
+
#
|
57
|
+
# In both of these cases we assume all scopes as the user does not
|
58
|
+
# request any specific scopes during the authentication process and
|
59
|
+
# the user would be anyways able to perform any actions they are
|
60
|
+
# normally allowed to perform within the regular user interface.
|
61
|
+
::Doorkeeper::OAuth::Scopes.from_array(::Doorkeeper.config.scopes.all)
|
62
|
+
else
|
63
|
+
# In case no user is present, we only allow the user to read the API.
|
64
|
+
::Doorkeeper::OAuth::Scopes.from_string("api:read")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
36
68
|
def prepare_variables(variables_param)
|
37
69
|
case variables_param
|
38
70
|
when String
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Api
|
5
|
+
class SessionsController < ::Devise::SessionsController
|
6
|
+
skip_before_action :verify_authenticity_token
|
7
|
+
|
8
|
+
respond_to :json
|
9
|
+
|
10
|
+
def failure
|
11
|
+
render json: resource_attributes(anonymous_user).merge(
|
12
|
+
"jwt_token" => nil,
|
13
|
+
"avatar" => nil
|
14
|
+
), status: :unauthorized
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def auth_options
|
20
|
+
{ scope: resource_name, recall: "#{controller_path}#failure" }
|
21
|
+
end
|
22
|
+
|
23
|
+
def after_sign_in_path_for(_resource_or_scope)
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def respond_with(resource, _opts = {})
|
28
|
+
serialized_user = resource_attributes(resource)
|
29
|
+
|
30
|
+
if request.env[::Warden::JWTAuth::Middleware::TokenDispatcher::ENV_KEY]
|
31
|
+
jwt_token = request.env[::Warden::JWTAuth::Hooks::PREPARED_TOKEN_ENV_KEY]
|
32
|
+
return failure if jwt_token.blank?
|
33
|
+
|
34
|
+
# Some systems (that is you Microsoft Power Automate (Flow)) may be
|
35
|
+
# parsing off the headers which makes it difficult for the API users
|
36
|
+
# to get the bearer token. This allows them to get it from the request
|
37
|
+
# body instead.
|
38
|
+
return render json: serialized_user.merge(
|
39
|
+
"jwt_token" => jwt_token,
|
40
|
+
"avatar" => nil
|
41
|
+
), status: :ok
|
42
|
+
end
|
43
|
+
|
44
|
+
# Since avatar can be ActiveStorage object now, it can cause infinite loops
|
45
|
+
render json: serialized_user.merge("avatar" => nil)
|
46
|
+
end
|
47
|
+
|
48
|
+
def respond_to_on_destroy
|
49
|
+
head :ok
|
50
|
+
end
|
51
|
+
|
52
|
+
def resource_attributes(resource)
|
53
|
+
resource.attributes.slice("id", "name", "nickname")
|
54
|
+
end
|
55
|
+
|
56
|
+
def anonymous_user
|
57
|
+
Decidim::Api::ApiUser.new(api_key: params.dig("api_user", "key"))
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Api
|
5
|
+
class ApiUser < UserBaseEntity
|
6
|
+
include Decidim::Traceable
|
7
|
+
|
8
|
+
attribute :tos_agreement, :boolean, default: true
|
9
|
+
|
10
|
+
devise :api_authenticatable, :jwt_authenticatable, jwt_revocation_strategy: JwtDenylist
|
11
|
+
|
12
|
+
validates :api_key, :name, uniqueness: { case_sensitive: true, scope: :organization }
|
13
|
+
|
14
|
+
def presenter
|
15
|
+
Decidim::Api::ApiUserPresenter.new(self)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.log_presenter_class_for(_log)
|
19
|
+
Decidim::AdminLog::UserPresenter
|
20
|
+
end
|
21
|
+
|
22
|
+
# Checks if the user has the given `role` or not.
|
23
|
+
#
|
24
|
+
# role - a String or a Symbol that represents the role that is being
|
25
|
+
# checked
|
26
|
+
#
|
27
|
+
# Returns a boolean.
|
28
|
+
def role?(role)
|
29
|
+
roles.include?(role.to_s)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Public: Returns the active role of the user
|
33
|
+
def active_role
|
34
|
+
admin ? "admin" : roles.first
|
35
|
+
end
|
36
|
+
|
37
|
+
# Public: returns the user's name or the default one
|
38
|
+
def name
|
39
|
+
super || I18n.t("decidim.anonymous_user")
|
40
|
+
end
|
41
|
+
|
42
|
+
# Check if the user account has been deleted or not
|
43
|
+
def deleted?
|
44
|
+
deleted_at.present?
|
45
|
+
end
|
46
|
+
|
47
|
+
# Public: whether the user has been officialized or not
|
48
|
+
def officialized?
|
49
|
+
!officialized_at.nil?
|
50
|
+
end
|
51
|
+
|
52
|
+
def confirmed?
|
53
|
+
true
|
54
|
+
end
|
55
|
+
|
56
|
+
def follows?(followable)
|
57
|
+
Decidim::Follow.where(user: self, followable: followable).any?
|
58
|
+
end
|
59
|
+
|
60
|
+
# Public: whether the user accepts direct messages from another
|
61
|
+
def accepts_conversation?(_user)
|
62
|
+
false
|
63
|
+
end
|
64
|
+
|
65
|
+
def unread_conversations
|
66
|
+
Decidim::Messaging::Conversation.unread_by(self)
|
67
|
+
end
|
68
|
+
|
69
|
+
def tos_accepted?
|
70
|
+
true
|
71
|
+
end
|
72
|
+
|
73
|
+
def admin_terms_accepted?
|
74
|
+
true
|
75
|
+
end
|
76
|
+
|
77
|
+
def needs_password_update?
|
78
|
+
false
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -9,7 +9,7 @@ import React from "react";
|
|
9
9
|
import { createRoot } from "react-dom/client";
|
10
10
|
|
11
11
|
import { GraphiQL } from "graphiql"; // eslint-disable-line no-unused-vars
|
12
|
-
import Configuration from "src/decidim/configuration"
|
12
|
+
import Configuration from "src/decidim/refactor/implementation/configuration"
|
13
13
|
|
14
14
|
window.Decidim = window.Decidim || {};
|
15
15
|
window.Decidim.config = new Configuration();
|
@@ -89,6 +89,7 @@ const graphQLFetcher = function (graphQLParams) {
|
|
89
89
|
});
|
90
90
|
};
|
91
91
|
|
92
|
+
// We do not have turbo or decidim_core when this file is loaded, and we need to leave it with old "DOMContentLoaded"
|
92
93
|
window.addEventListener("DOMContentLoaded", () => {
|
93
94
|
const container = document.getElementById("graphiql-container");
|
94
95
|
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Api
|
5
|
+
class ApiUserPresenter < Decidim::UserPresenter
|
6
|
+
def deleted?
|
7
|
+
false
|
8
|
+
end
|
9
|
+
|
10
|
+
def badge
|
11
|
+
"verified-badge"
|
12
|
+
end
|
13
|
+
|
14
|
+
def can_be_contacted?
|
15
|
+
false
|
16
|
+
end
|
17
|
+
|
18
|
+
def can_follow?
|
19
|
+
false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/config/assets.rb
CHANGED
@@ -2,8 +2,8 @@
|
|
2
2
|
|
3
3
|
base_path = File.expand_path("..", __dir__)
|
4
4
|
|
5
|
-
Decidim::
|
6
|
-
Decidim::
|
5
|
+
Decidim::Shakapacker.register_path("#{base_path}/app/packs")
|
6
|
+
Decidim::Shakapacker.register_entrypoints(
|
7
7
|
decidim_api_docs: "#{base_path}/app/packs/entrypoints/decidim_api_docs.js",
|
8
8
|
decidim_api_graphiql: "#{base_path}/app/packs/entrypoints/decidim_api_graphiql.js"
|
9
9
|
)
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Devise.jwt do |jwt|
|
4
|
+
# In order to be compatible with the JWT authentication, we need to set these
|
5
|
+
# configurations. JWT secret is being used by the devise-jwt to sign the
|
6
|
+
# tokens, once the user authenticated. The token signature ensures the
|
7
|
+
# validity of the token and that the user has not tampered with it. If the
|
8
|
+
# secret is not set correctly, the API authentication does not work.
|
9
|
+
#
|
10
|
+
# Note that the `dispatch_requests` and `revocation_requests` paths are the
|
11
|
+
# full paths because we do not want the JWT tokens to be dispatched or revoked
|
12
|
+
# during normal Decidim user sign ins or sign outs. This also requires a small
|
13
|
+
# override to `Warden::JWTAuth` which is defined at
|
14
|
+
# `decidim-api/lib/warden/jwt_auth/decidim_overrides.rb`.
|
15
|
+
jwt.secret = Decidim::Env.new("DECIDIM_API_JWT_SECRET").value
|
16
|
+
next unless jwt.secret
|
17
|
+
|
18
|
+
jwt.dispatch_requests = [
|
19
|
+
["POST", %r{^/api/sign_in$}]
|
20
|
+
]
|
21
|
+
jwt.revocation_requests = [
|
22
|
+
["DELETE", %r{^/api/sign_out$}]
|
23
|
+
]
|
24
|
+
jwt.expiration_time = Decidim::Api.jwt_expires_in.minutes.to_i
|
25
|
+
jwt.aud_header = "X_JWT_AUD"
|
26
|
+
end
|
data/config/routes.rb
CHANGED
@@ -6,4 +6,12 @@ Decidim::Api::Engine.routes.draw do
|
|
6
6
|
get "/docs/*path", to: "documentation#show"
|
7
7
|
get "/", to: redirect("/api/docs")
|
8
8
|
post "/" => "queries#create", :as => :root
|
9
|
+
|
10
|
+
devise_for :api_users,
|
11
|
+
class_name: "Decidim::Api::ApiUser",
|
12
|
+
module: "decidim/api",
|
13
|
+
path: "/",
|
14
|
+
router_name: :decidim_api,
|
15
|
+
controllers: { sessions: "decidim/api/sessions" },
|
16
|
+
only: :sessions
|
9
17
|
end
|
data/decidim-api.gemspec
CHANGED
@@ -33,7 +33,8 @@ Gem::Specification.new do |s|
|
|
33
33
|
end
|
34
34
|
|
35
35
|
s.add_dependency "decidim-core", Decidim::Api.version
|
36
|
-
s.add_dependency "
|
36
|
+
s.add_dependency "devise-jwt", "~> 0.12.1"
|
37
|
+
s.add_dependency "graphql", "~> 2.4.0", ">= 2.4.17"
|
37
38
|
s.add_dependency "graphql-docs", "~> 5.0"
|
38
39
|
s.add_dependency "rack-cors", "~> 1.0"
|
39
40
|
|
data/docs/usage.md
CHANGED
@@ -12,7 +12,7 @@ Typically (although some particular installations may change that) you will find
|
|
12
12
|
* `URL/api/docs` This documentation, every Decidim site should provide one.
|
13
13
|
* `URL/api/graphiql` [GraphiQL](https://github.com/graphql/graphiql) is a in-browser IDE for exploring GraphQL APIs. Some Decidim installations may choose to remove access to this tool. In that case you can use a [standalone version](https://electronjs.org/apps/graphiql) and use any `URL/api` as the endpoint
|
14
14
|
|
15
|
-
### Using the GraphQL
|
15
|
+
### Using the GraphQL API
|
16
16
|
|
17
17
|
The GraphQL format is a JSON formatted text that is specified in a query. Response is a JSON object as well. For details about specification check the official [GraphQL site](https://graphql.org/learn/).
|
18
18
|
|
@@ -54,6 +54,96 @@ The most practical way to experiment with GraphQL, however, is just to use the i
|
|
54
54
|
|
55
55
|
From now on, we will skip the "query" keyword for the purpose of readability. You can skip it too if you are using GraphiQL, if you are querying directly (by using CURL for instance) you will need to include it.
|
56
56
|
|
57
|
+
### Signing in to the API
|
58
|
+
|
59
|
+
In case you want to use the API as a sign in user to perform mutations representing a user in Decidim, you have two available options for such integrations through the system administration panel:
|
60
|
+
|
61
|
+
1. Creating an OAuth application and implementing the OAuth authentication flow for the users of your application. Use this option for participant-facing applications where the participants represent themselves in Decidim through the API.
|
62
|
+
2. Creating API credentials and signing in to the API with these credentials to perform the operations as a signed in machine user. Use this option for machine-to-machine automations where there is no real end user interacting with Decidim.
|
63
|
+
|
64
|
+
If you only want to test the GraphQL queries as a signed in user, you can use the normal Decidim authentication functionality to sign in and then use the GraphiQL IDE to perform these queries as a signed in user.
|
65
|
+
|
66
|
+
#### OAuth flow for participant-facing applications
|
67
|
+
|
68
|
+
Participant-facing applications where the participants need to interact with Decidim through GraphQL mutations can be integrated using OAuth applications. In order to configure such integration capability from the system administration panel, create a new OAuth application and provide the necessary details for your integration. Note that the "application type" for such applications would typically be "Public". For more information regarding the application types, refer to [RFC 6749 Section 2.1. (OAuth client types)](https://datatracker.ietf.org/doc/html/rfc6749#section-2.1).
|
69
|
+
|
70
|
+
In order to use the OAuth access tokens to represent the user through the API, please select the following scopes as "Available" scopes for the application:
|
71
|
+
|
72
|
+
* `user` - Authenticated users have the ability to represent a logged in user in Decidim
|
73
|
+
* `api:read` - Authenticated users have the ability to read data from the API
|
74
|
+
* `api:write` - Authenticated users have the ability to write data through the API (in case your external application needs to perform mutations over the API on behalf of the user)
|
75
|
+
|
76
|
+
Once configured, you can now use any OAuth authentication library to perform the OAuth authentication flow with your application users and receive an access token to utilize the Decidim API representing the signed in user. Please note that with public OAuth clients especially (and recommended also for confidential clients), you have to use [PKCE](https://datatracker.ietf.org/doc/html/rfc7636) with the authorization flow.
|
77
|
+
|
78
|
+
Once the OAuth application is created, you can authenticate against it with the following steps:
|
79
|
+
|
80
|
+
1. Send the user to perform an OAuth authorization request at Decidim with the required API scopes (`user`, `api:read` and `api:write` if you want to perform mutations over the API). Along with the authorization request, also send the additional parameters required by PKCE (`code_challenge` and `code_challenge_method`).
|
81
|
+
2. Receive an OAuth authorization code back to your application's configured redirect URI.
|
82
|
+
3. Utilizing the received authorization code, request an OAuth access token from the OAuth token endpoint. Along with the token request, also send the additional parameter required by PKCE `code_verifier`.
|
83
|
+
4. The issued token is a JSON Web Token (JWT) when the authorization request contains the defined scopes. This token can be now used to represent the user in further calls to the API by passing the token with its type (`Bearer`) within the HTTP Authorization header with the request to the API.
|
84
|
+
|
85
|
+
When doing the requests to the API, you also need to pass the OAuth client ID within the `X-Jwt-Aud` header of the requests in order for the token to be recognized as a valid token for the issued client. Passing the bearer token to the `Authorization` header and the OAuth client ID to the `X-Jwt-Aud` header, you can send the following HTTP request to the API to validate that the token works and the user is recognized as signed in:
|
86
|
+
|
87
|
+
```http
|
88
|
+
POST /api HTTP/1.1
|
89
|
+
Accept: application/json
|
90
|
+
Authorization: Bearer token
|
91
|
+
Content-Length: 53
|
92
|
+
Content-Type: application/json
|
93
|
+
Host: DOMAIN
|
94
|
+
X-Jwt-Aud: OAUTH_CLIENT_ID
|
95
|
+
|
96
|
+
{"query":"{ session { user { id name nickname } } }"}
|
97
|
+
```
|
98
|
+
|
99
|
+
You should see the user details in the response in case the token is valid and you have configured the API correctly. If the response does not contain the user details, please refer to the Decidim configuration documentation.
|
100
|
+
|
101
|
+
Once the interaction with the API is completed, it is recommended to revoke the tokens, which is similar to the user signing out of the application. This can be done utilizing the OAuth revocation endpoint provided by Decidim. After the token is revoked, it is no longer valid and the user has to perform a re-authorization the next time they want to utilize the API.
|
102
|
+
|
103
|
+
In case you need tokens with a longer life span, you can either look into the Decidim documentation to extend the validity period of the access tokens or enable refresh tokens for the OAuth application when configuring it. However, note that tokens with longer lifespan can weaken the security of your system and make your application users vulnerable to security threats. Such use cases should be carefully planned and the security concerns should be addressed seriously.
|
104
|
+
|
105
|
+
#### API credentials flow for machine-to-machine automations
|
106
|
+
|
107
|
+
The API credentials represent an administrative user in Decidim that performs administrative tasks on behalf of the end users. This type of integration flows should never live on devices that the participants have access to. These types of integrations are meant for different types of automations, such as transferring proposal answers or meeting reports back to Decidim from an external system automatically, e.g. once a day.
|
108
|
+
|
109
|
+
Note that these credentials are highly sensitive and have elevated permissions, so take good care of the system security where you are planning to store these credentials. If these credentials end up in participants' hands, the whole system is compromised and no longer secure. You should always primarily create OAuth integrations where the end users will manually perform the authorization for the application to perform actions on behalf of them.
|
110
|
+
|
111
|
+
Once you have validated that this is the correct way for your integration to operate, you can create the API credentials from the system administration panel. You will receive an API key and API secret after creating the credentials. These credentials should be also manually rotated on a regular basis to prevent unauthorized access to the system with these credentials in case they are leaked. The credentials have to be manually rotated in order to prevent external applications breaking because they cannot rotate the credentials themselves and they are typically statically configured for these applications.
|
112
|
+
|
113
|
+
Given you have issued the API key and API secret, you can now send a sign in request to the API using these credentials as follows:
|
114
|
+
|
115
|
+
```bash
|
116
|
+
curl -s -i -H "Content-type: application/x-www-form-urlencoded" \
|
117
|
+
-d "api_user[key]=PASTE_API_KEY_HERE" \
|
118
|
+
-d "api_user[secret]=PASTE_API_SECRET_HERE" \
|
119
|
+
-X POST https://DOMAIN/api/sign_in | grep 'Authorization' | cut -d ' ' -f2-
|
120
|
+
```
|
121
|
+
|
122
|
+
After running this command, you should see the following string in the console, where `token` is replaced with the access token:
|
123
|
+
|
124
|
+
```bash
|
125
|
+
Bearer token
|
126
|
+
```
|
127
|
+
|
128
|
+
This string is passed to the following requests within the HTTP `Authorization` header to represent the user during API calls. You can use the following example query to test it out and confirm that signing in works as expected:
|
129
|
+
|
130
|
+
```bash
|
131
|
+
curl -w "\n" -H "Content-Type: application/json" \
|
132
|
+
-H "Authorization: Bearer token" \
|
133
|
+
-d '{"query":"{ session { user { id name nickname } } }"}' \
|
134
|
+
-X POST https://DOMAIN/api
|
135
|
+
```
|
136
|
+
|
137
|
+
You should see the user details in the response in case the token is valid and you have configured the API correctly. If the response does not contain the user details, please refer to the Decidim configuration documentation.
|
138
|
+
|
139
|
+
Once the API interaction is done, you should always make an HTTP DELETE request to `/api/sign_out` with the same token in order to revoke the token from further access as follows:
|
140
|
+
|
141
|
+
```bash
|
142
|
+
curl -s -o /dev/null -w "HTTP %{http_code}\n" \
|
143
|
+
-H "Authorization: Bearer token" \
|
144
|
+
-X DELETE http://DOMAIN/api/sign_out
|
145
|
+
```
|
146
|
+
|
57
147
|
### Usage limits
|
58
148
|
|
59
149
|
Decidim is just a Rails application, meaning that any particular installation may implement custom limits in order to access the API (and the application in general).
|
@@ -404,11 +494,11 @@ Consider this query:
|
|
404
494
|
translation(locale: "en")
|
405
495
|
}
|
406
496
|
... on Proposals {
|
407
|
-
proposals(order: {
|
497
|
+
proposals(order: {likeCount: "desc"}, first: 2) {
|
408
498
|
edges {
|
409
499
|
node {
|
410
500
|
id
|
411
|
-
|
501
|
+
likes {
|
412
502
|
name
|
413
503
|
}
|
414
504
|
}
|
@@ -437,7 +527,7 @@ The response:
|
|
437
527
|
{
|
438
528
|
"node": {
|
439
529
|
"id": "35",
|
440
|
-
"
|
530
|
+
"likes": [
|
441
531
|
{
|
442
532
|
"name": "Ms. Johnathon Schaefer"
|
443
533
|
},
|
@@ -468,7 +558,7 @@ The response:
|
|
468
558
|
{
|
469
559
|
"node": {
|
470
560
|
"id": "33",
|
471
|
-
"
|
561
|
+
"likes": [
|
472
562
|
{
|
473
563
|
"name": "Spring Brakus"
|
474
564
|
},
|
@@ -529,7 +619,7 @@ Example:
|
|
529
619
|
edges {
|
530
620
|
node {
|
531
621
|
id
|
532
|
-
|
622
|
+
likes {
|
533
623
|
name
|
534
624
|
}
|
535
625
|
}
|
@@ -564,13 +654,13 @@ Being the response:
|
|
564
654
|
{
|
565
655
|
"node": {
|
566
656
|
"id": "32",
|
567
|
-
"
|
657
|
+
"likes": []
|
568
658
|
}
|
569
659
|
},
|
570
660
|
{
|
571
661
|
"node": {
|
572
662
|
"id": "31",
|
573
|
-
"
|
663
|
+
"likes": [
|
574
664
|
{
|
575
665
|
"name": "Mr. Nicolas Raynor"
|
576
666
|
},
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Api
|
5
|
+
class ComponentMutationType < GraphQL::Schema::Union
|
6
|
+
description "A component mutation."
|
7
|
+
|
8
|
+
possible_types(*Decidim::MutationRegistry.instance.mutation_types)
|
9
|
+
|
10
|
+
def self.resolve_type(obj, _ctx)
|
11
|
+
mod = obj.manifest_name.camelize
|
12
|
+
"Decidim::#{mod}::#{mod}MutationType".constantize
|
13
|
+
rescue NameError
|
14
|
+
Rails.logger.warn("Mutation type not found for #{mod}: #{e.message}")
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "devise/models/api_authenticatable"
|
4
|
+
require "devise/strategies/api_authenticatable"
|
5
|
+
|
6
|
+
Devise.add_module(
|
7
|
+
:api_authenticatable,
|
8
|
+
model: true,
|
9
|
+
strategy: true,
|
10
|
+
controller: :sessions,
|
11
|
+
route: { session: [nil, :destroy] }
|
12
|
+
)
|
data/lib/decidim/api/engine.rb
CHANGED
@@ -27,7 +27,7 @@ module Decidim
|
|
27
27
|
app.config.middleware.insert_before 0, Rack::Cors do
|
28
28
|
allow do
|
29
29
|
origins "*"
|
30
|
-
resource "/api", headers: :any, methods: [:post, :options]
|
30
|
+
resource "/api/*", headers: :any, methods: [:post, :options]
|
31
31
|
end
|
32
32
|
end
|
33
33
|
end
|
@@ -41,7 +41,7 @@ module Decidim
|
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
|
-
initializer "decidim_api.
|
44
|
+
initializer "decidim_api.shakapacker.assets_path" do
|
45
45
|
Decidim.register_assets_path File.expand_path("app/packs", root)
|
46
46
|
end
|
47
47
|
end
|