decidim-api 0.30.2 → 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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/decidim/api/application_controller.rb +20 -0
  3. data/app/controllers/decidim/api/queries_controller.rb +33 -1
  4. data/app/controllers/decidim/api/sessions_controller.rb +61 -0
  5. data/app/models/decidim/api/api_user.rb +82 -0
  6. data/app/models/decidim/api/jwt_denylist.rb +11 -0
  7. data/app/packs/entrypoints/decidim_api_graphiql.js +2 -1
  8. data/app/presenters/decidim/api/api_user_presenter.rb +23 -0
  9. data/config/assets.rb +2 -2
  10. data/config/initializers/devise.rb +26 -0
  11. data/config/routes.rb +8 -0
  12. data/decidim-api.gemspec +2 -1
  13. data/docs/usage.md +98 -8
  14. data/lib/decidim/api/component_mutation_type.rb +19 -0
  15. data/lib/decidim/api/devise.rb +12 -0
  16. data/lib/decidim/api/engine.rb +2 -2
  17. data/lib/decidim/api/graphql_permissions.rb +125 -0
  18. data/lib/decidim/api/mutation_type.rb +10 -0
  19. data/lib/decidim/api/required_scopes.rb +31 -0
  20. data/lib/decidim/api/test/component_context.rb +17 -18
  21. data/lib/decidim/api/test/factories.rb +33 -0
  22. data/lib/decidim/api/test/mutation_context.rb +38 -0
  23. data/lib/decidim/api/test/shared_examples/amendable_interface_examples.rb +14 -0
  24. data/lib/decidim/api/test/shared_examples/amendable_proposals_interface_examples.rb +50 -0
  25. data/lib/decidim/api/test/shared_examples/attachable_interface_examples.rb +40 -0
  26. data/lib/decidim/api/test/shared_examples/authorable_interface_examples.rb +46 -0
  27. data/lib/decidim/api/test/shared_examples/categories_container_examples.rb +22 -0
  28. data/lib/decidim/api/test/shared_examples/categorizable_interface_examples.rb +27 -0
  29. data/lib/decidim/api/test/shared_examples/coauthorable_interface_examples.rb +77 -0
  30. data/lib/decidim/api/test/shared_examples/commentable_interface_examples.rb +13 -0
  31. data/lib/decidim/api/test/shared_examples/fingerprintable_interface_examples.rb +17 -0
  32. data/lib/decidim/api/test/shared_examples/followable_interface_examples.rb +13 -0
  33. data/lib/decidim/api/test/shared_examples/input_filter_examples.rb +77 -0
  34. data/lib/decidim/api/test/shared_examples/input_sort_examples.rb +126 -0
  35. data/lib/decidim/api/test/shared_examples/likeable_interface_examples.rb +22 -0
  36. data/lib/decidim/api/test/shared_examples/localizable_interface_examples.rb +29 -0
  37. data/lib/decidim/api/test/shared_examples/participatory_space_resourcable_interface_examples.rb +61 -0
  38. data/lib/decidim/api/test/shared_examples/referable_interface_examples.rb +13 -0
  39. data/lib/decidim/api/test/shared_examples/scopable_interface_examples.rb +19 -0
  40. data/lib/decidim/api/test/shared_examples/statistics_examples.rb +30 -16
  41. data/lib/decidim/api/test/shared_examples/taxonomizable_interface_examples.rb +20 -0
  42. data/lib/decidim/api/test/shared_examples/timestamps_interface_examples.rb +21 -0
  43. data/lib/decidim/api/test/shared_examples/traceable_interface_examples.rb +49 -0
  44. data/lib/decidim/api/test/type_context.rb +9 -1
  45. data/lib/decidim/api/test.rb +22 -0
  46. data/lib/decidim/api/types/base_mutation.rb +5 -1
  47. data/lib/decidim/api/types/base_object.rb +4 -69
  48. data/lib/decidim/api/types.rb +3 -0
  49. data/lib/decidim/api/version.rb +1 -1
  50. data/lib/decidim/api.rb +25 -5
  51. data/lib/devise/models/api_authenticatable.rb +30 -0
  52. data/lib/devise/strategies/api_authenticatable.rb +21 -0
  53. data/lib/warden/jwt_auth/decidim_overrides.rb +42 -0
  54. metadata +66 -12
@@ -4,77 +4,12 @@ module Decidim
4
4
  module Api
5
5
  module Types
6
6
  class BaseObject < GraphQL::Schema::Object
7
- field_class Types::BaseField
8
-
9
- def self.authorized?(object, context)
10
- chain = []
11
-
12
- subject = determine_subject_name(object)
13
- context[subject] = object
14
-
15
- chain.unshift(allowed_to?(:read, :participatory_space, object, context)) if object.respond_to?(:participatory_space)
16
- chain.unshift(allowed_to?(:read, :component, object, context)) if object.respond_to?(:component) && object.component.present?
17
-
18
- super && chain.all?
19
- end
20
-
21
- def self.determine_subject_name(object)
22
- object.class.name.split("::").last.underscore.to_sym
23
- end
24
-
25
- # This is a simplified adaptation of allowed_to? from NeedsPermission concern
26
- # @param action [Symbol] The action performed. Most cases the action is :read
27
- # @param subject [Object] The name of the subject. Ex: :participatory_space, :component, or object
28
- # @param object [ActiveModel::Base] The object that is being represented.
29
- # @param context [GraphQL::Query::Context] The GraphQL context
30
- #
31
- # @return Boolean
32
- def self.allowed_to?(action, subject, object, context)
33
- unless subject.is_a?(::Symbol)
34
- subject = determine_subject_name(object)
35
- context[subject] = object
36
- end
7
+ include Decidim::Api::RequiredScopes
8
+ include Decidim::Api::GraphqlPermissions
37
9
 
38
- permission_action = Decidim::PermissionAction.new(scope: :public, action:, subject:)
39
-
40
- permission_chain(object).inject(permission_action) do |current_permission_action, permission_class|
41
- permission_class.new(
42
- context[:current_user],
43
- current_permission_action,
44
- local_context(object, context)
45
- ).permissions
46
- end.allowed?
47
- end
48
-
49
- # Injects into context object current_participatory_space and current_component keys as they are needed
50
- #
51
- # @param object [ActiveModel::Base] The object that is being represented.
52
- # @param context [GraphQL::Query::Context] The GraphQL context
53
- #
54
- # @return Hash
55
- def self.local_context(object, context)
56
- context[:current_participatory_space] = object.participatory_space if object.respond_to?(:participatory_space)
57
- context[:current_component] = object.component if object.respond_to?(:component) && object.component.present?
58
-
59
- context.to_h
60
- end
61
-
62
- # Creates the permission chain arrau that contains all the permission classes required to authorize a certain resource
63
- # We are using unshift as we need the Admin and base permissions to be last in the chain
64
- # @param object [ActiveModel::Base] The object that is being represented.
65
- #
66
- # @return [Decidim::DefaultPermissions]
67
- def self.permission_chain(object)
68
- permissions = [
69
- Decidim::Admin::Permissions,
70
- Decidim::Permissions
71
- ]
72
-
73
- permissions.unshift(object.participatory_space.manifest.permissions_class) if object.respond_to?(:participatory_space)
74
- permissions.unshift(object.component.manifest.permissions_class) if object.respond_to?(:component) && object.component.present?
10
+ field_class Types::BaseField
75
11
 
76
- permissions
77
- end
12
+ required_scopes "api:read"
78
13
  end
79
14
  end
80
15
  end
@@ -5,6 +5,9 @@ module Decidim
5
5
  autoload :QueryType, "decidim/api/query_type"
6
6
  autoload :MutationType, "decidim/api/mutation_type"
7
7
  autoload :Schema, "decidim/api/schema"
8
+ autoload :RequiredScopes, "decidim/api/required_scopes"
9
+ autoload :GraphqlPermissions, "decidim/api/graphql_permissions"
10
+ autoload :ComponentMutationType, "decidim/api/component_mutation_type"
8
11
 
9
12
  module Types
10
13
  autoload :BaseArgument, "decidim/api/types/base_argument"
@@ -4,7 +4,7 @@ module Decidim
4
4
  # This holds the decidim-api version.
5
5
  module Api
6
6
  def self.version
7
- "0.30.2"
7
+ "0.31.0.rc1"
8
8
  end
9
9
  end
10
10
  end
data/lib/decidim/api.rb CHANGED
@@ -1,7 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "devise/jwt"
4
+ require "warden/jwt_auth/decidim_overrides"
5
+ require "decidim/env"
3
6
  require "decidim/api/engine"
4
7
  require "decidim/api/types"
8
+ require "decidim/api/devise"
5
9
 
6
10
  module Decidim
7
11
  # This module holds all business logic related to exposing a Public API for
@@ -11,27 +15,43 @@ module Decidim
11
15
 
12
16
  # defines the schema max_per_page to configure GraphQL pagination
13
17
  config_accessor :schema_max_per_page do
14
- 50
18
+ Decidim::Env.new("API_SCHEMA_MAX_PER_PAGE", 50).to_i
15
19
  end
16
20
 
17
21
  # defines the schema max_complexity to configure GraphQL query complexity
18
22
  config_accessor :schema_max_complexity do
19
- 5000
23
+ Decidim::Env.new("API_SCHEMA_MAX_COMPLEXITY", 5000).to_i
20
24
  end
21
25
 
22
26
  # defines the schema max_depth to configure GraphQL query max_depth
23
27
  config_accessor :schema_max_depth do
24
- 15
28
+ Decidim::Env.new("API_SCHEMA_MAX_DEPTH", 15).to_i
25
29
  end
26
30
 
27
31
  config_accessor :disclose_system_version do
28
- %w(1 true yes).include?(ENV.fetch("DECIDIM_API_DISCLOSE_SYSTEM_VERSION", nil))
32
+ Decidim::Env.new("DECIDIM_API_DISCLOSE_SYSTEM_VERSION").present?
33
+ end
34
+
35
+ # Public Setting that can make the API authentication necessary in order to
36
+ # access it.
37
+ config_accessor :force_api_authentication do
38
+ Decidim::Env.new("DECIDIM_API_FORCE_API_AUTHENTICATION", nil).present?
39
+ end
40
+
41
+ # The expiration time of the JWT tokens, after which issued token will
42
+ # expire. Recommended to match the value of
43
+ # `DECIDIM_OAUTH_ACCESS_TOKEN_EXPIRES_IN`.
44
+ config_accessor :jwt_expires_in do
45
+ Decidim::Env.new(
46
+ "DECIDIM_API_JWT_EXPIRES_IN",
47
+ Decidim::Env.new("DECIDIM_OAUTH_ACCESS_TOKEN_EXPIRES_IN", "120").value
48
+ ).to_i
29
49
  end
30
50
 
31
51
  # This declares all the types an interface or union can resolve to. This needs
32
52
  # to be done in order to be able to have them found. This is a shortcoming of
33
53
  # graphql-ruby and the way it deals with loading types, in combination with
34
- # rail's infamous autoloading.
54
+ # rail's infamous auto-loading.
35
55
  def self.orphan_types
36
56
  Decidim.component_manifests.map(&:query_type).map(&:constantize).uniq +
37
57
  Decidim.participatory_space_manifests.map(&:query_type).map(&:constantize).uniq +
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Devise
4
+ module Models
5
+ module ApiAuthenticatable
6
+ extend ActiveSupport::Concern
7
+
8
+ def api_secret=(new_secret)
9
+ self.encrypted_password = ::Devise::Encryptor.digest(self.class, new_secret)
10
+ end
11
+
12
+ # Verifies whether a secret (ie from sign in) matches the user's secret.
13
+ def valid_api_secret?(secret)
14
+ Devise::Encryptor.compare(self.class, encrypted_password, secret)
15
+ end
16
+
17
+ module ClassMethods
18
+ Devise::Models.config(self, :pepper, :stretches)
19
+
20
+ def authentication_keys
21
+ [:key, :secret]
22
+ end
23
+
24
+ def find_for_api_authentication(conditions)
25
+ find_for_authentication(conditions)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "devise/strategies/authenticatable"
4
+
5
+ module Devise
6
+ module Strategies
7
+ class ApiAuthenticatable < Authenticatable
8
+ def authenticate!
9
+ key = authentication_hash[:key]
10
+ secret = authentication_hash[:secret]
11
+
12
+ resource = mapping.to.find_for_api_authentication(api_key: key)
13
+ validation_status = validate(resource) { resource.valid_api_secret?(secret) }
14
+
15
+ success!(resource) if validation_status
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ Warden::Strategies.add(:api_authenticatable, Devise::Strategies::ApiAuthenticatable)
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Warden
4
+ module JWTAuth
5
+ # This module adds some overrides to Warde::JWTAuth due to some issues with
6
+ # how it needs to be setup.
7
+ module EnvHelper
8
+ # Returns ORIGINAL_FULLPATH, REQUEST_PATH or REQUEST_URI environment
9
+ # variable, which is different from the original version returning the
10
+ # PATH_INFO environment variable.
11
+ #
12
+ # This is overridden because the `PATH_INFO` string only includes the
13
+ # route under the engine where it is defined, i.e. `/sign_in` and
14
+ # `/sign_out` which are defined under the `Decidim::Api`.
15
+ #
16
+ # Instead, we want this method to return the full path, i.e.
17
+ # `/api/sign_in` and `/api/sign_out` in order to map these routes
18
+ # correctly as the JWT token dispatch and revocation routes. Otherwise the
19
+ # token would be dispatched and revoked also with normal user sign in and
20
+ # sign out requests under the `Decidim::Core` engine which we do not want.
21
+ #
22
+ # This affects the Devise::JWT / Warden::JWTAuth configuration that is
23
+ # defined at `decidim-api/config/initializers/devise.rb` (as
24
+ # `config.jwt`).
25
+ #
26
+ # The return value is only used by Warden::JWTAuth to check when the token
27
+ # should be dispatched or revoked for the user, nothing else.
28
+ #
29
+ # Note that this behaves slightly differently during controller testing
30
+ # and when the application is actually running under Rack. The end result
31
+ # is the same but some of the environment variables may be missing during
32
+ # controller testing (e.g. `REQUEST_PATH` is not defined under that
33
+ # situation).
34
+ #
35
+ # @param env [Hash] Rack env
36
+ # @return [String]
37
+ def self.path_info(env)
38
+ env["ORIGINAL_FULLPATH"] || env["REQUEST_PATH"] || env["REQUEST_URI"] || ""
39
+ end
40
+ end
41
+ end
42
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: decidim-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.30.2
4
+ version: 0.31.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josep Jaume Rey Peroy
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2025-09-23 00:00:00.000000000 Z
13
+ date: 2025-09-25 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: decidim-core
@@ -18,14 +18,28 @@ dependencies:
18
18
  requirements:
19
19
  - - '='
20
20
  - !ruby/object:Gem::Version
21
- version: 0.30.2
21
+ version: 0.31.0.rc1
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
25
25
  requirements:
26
26
  - - '='
27
27
  - !ruby/object:Gem::Version
28
- version: 0.30.2
28
+ version: 0.31.0.rc1
29
+ - !ruby/object:Gem::Dependency
30
+ name: devise-jwt
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - "~>"
34
+ - !ruby/object:Gem::Version
35
+ version: 0.12.1
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - "~>"
41
+ - !ruby/object:Gem::Version
42
+ version: 0.12.1
29
43
  - !ruby/object:Gem::Dependency
30
44
  name: graphql
31
45
  requirement: !ruby/object:Gem::Requirement
@@ -33,6 +47,9 @@ dependencies:
33
47
  - - "~>"
34
48
  - !ruby/object:Gem::Version
35
49
  version: 2.4.0
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 2.4.17
36
53
  type: :runtime
37
54
  prerelease: false
38
55
  version_requirements: !ruby/object:Gem::Requirement
@@ -40,6 +57,9 @@ dependencies:
40
57
  - - "~>"
41
58
  - !ruby/object:Gem::Version
42
59
  version: 2.4.0
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 2.4.17
43
63
  - !ruby/object:Gem::Dependency
44
64
  name: graphql-docs
45
65
  requirement: !ruby/object:Gem::Requirement
@@ -74,56 +94,56 @@ dependencies:
74
94
  requirements:
75
95
  - - '='
76
96
  - !ruby/object:Gem::Version
77
- version: 0.30.2
97
+ version: 0.31.0.rc1
78
98
  type: :development
79
99
  prerelease: false
80
100
  version_requirements: !ruby/object:Gem::Requirement
81
101
  requirements:
82
102
  - - '='
83
103
  - !ruby/object:Gem::Version
84
- version: 0.30.2
104
+ version: 0.31.0.rc1
85
105
  - !ruby/object:Gem::Dependency
86
106
  name: decidim-comments
87
107
  requirement: !ruby/object:Gem::Requirement
88
108
  requirements:
89
109
  - - '='
90
110
  - !ruby/object:Gem::Version
91
- version: 0.30.2
111
+ version: 0.31.0.rc1
92
112
  type: :development
93
113
  prerelease: false
94
114
  version_requirements: !ruby/object:Gem::Requirement
95
115
  requirements:
96
116
  - - '='
97
117
  - !ruby/object:Gem::Version
98
- version: 0.30.2
118
+ version: 0.31.0.rc1
99
119
  - !ruby/object:Gem::Dependency
100
120
  name: decidim-dev
101
121
  requirement: !ruby/object:Gem::Requirement
102
122
  requirements:
103
123
  - - '='
104
124
  - !ruby/object:Gem::Version
105
- version: 0.30.2
125
+ version: 0.31.0.rc1
106
126
  type: :development
107
127
  prerelease: false
108
128
  version_requirements: !ruby/object:Gem::Requirement
109
129
  requirements:
110
130
  - - '='
111
131
  - !ruby/object:Gem::Version
112
- version: 0.30.2
132
+ version: 0.31.0.rc1
113
133
  - !ruby/object:Gem::Dependency
114
134
  name: decidim-participatory_processes
115
135
  requirement: !ruby/object:Gem::Requirement
116
136
  requirements:
117
137
  - - '='
118
138
  - !ruby/object:Gem::Version
119
- version: 0.30.2
139
+ version: 0.31.0.rc1
120
140
  type: :development
121
141
  prerelease: false
122
142
  version_requirements: !ruby/object:Gem::Requirement
123
143
  requirements:
124
144
  - - '='
125
145
  - !ruby/object:Gem::Version
126
- version: 0.30.2
146
+ version: 0.31.0.rc1
127
147
  description: API engine for decidim
128
148
  email:
129
149
  - josepjaume@gmail.com
@@ -139,28 +159,59 @@ files:
139
159
  - app/controllers/decidim/api/documentation_controller.rb
140
160
  - app/controllers/decidim/api/graphiql_controller.rb
141
161
  - app/controllers/decidim/api/queries_controller.rb
162
+ - app/controllers/decidim/api/sessions_controller.rb
163
+ - app/models/decidim/api/api_user.rb
164
+ - app/models/decidim/api/jwt_denylist.rb
142
165
  - app/packs/entrypoints/decidim_api_docs.js
143
166
  - app/packs/entrypoints/decidim_api_docs.scss
144
167
  - app/packs/entrypoints/decidim_api_graphiql.js
145
168
  - app/packs/entrypoints/decidim_api_graphiql.scss
169
+ - app/presenters/decidim/api/api_user_presenter.rb
146
170
  - app/views/decidim/api/documentation/graphql_docs_template.html.erb
147
171
  - app/views/decidim/api/documentation/show.html.erb
148
172
  - app/views/decidim/api/graphiql/show.html.erb
149
173
  - app/views/layouts/decidim/api/documentation.html.erb
150
174
  - config/assets.rb
175
+ - config/initializers/devise.rb
151
176
  - config/routes.rb
152
177
  - decidim-api.gemspec
153
178
  - docs/usage.md
154
179
  - lib/decidim/api.rb
180
+ - lib/decidim/api/component_mutation_type.rb
181
+ - lib/decidim/api/devise.rb
155
182
  - lib/decidim/api/engine.rb
156
183
  - lib/decidim/api/graphiql-initial-query.txt
157
184
  - lib/decidim/api/graphiql/config.rb
185
+ - lib/decidim/api/graphql_permissions.rb
158
186
  - lib/decidim/api/mutation_type.rb
159
187
  - lib/decidim/api/query_type.rb
188
+ - lib/decidim/api/required_scopes.rb
160
189
  - lib/decidim/api/schema.rb
161
190
  - lib/decidim/api/test.rb
162
191
  - lib/decidim/api/test/component_context.rb
192
+ - lib/decidim/api/test/factories.rb
193
+ - lib/decidim/api/test/mutation_context.rb
194
+ - lib/decidim/api/test/shared_examples/amendable_interface_examples.rb
195
+ - lib/decidim/api/test/shared_examples/amendable_proposals_interface_examples.rb
196
+ - lib/decidim/api/test/shared_examples/attachable_interface_examples.rb
197
+ - lib/decidim/api/test/shared_examples/authorable_interface_examples.rb
198
+ - lib/decidim/api/test/shared_examples/categories_container_examples.rb
199
+ - lib/decidim/api/test/shared_examples/categorizable_interface_examples.rb
200
+ - lib/decidim/api/test/shared_examples/coauthorable_interface_examples.rb
201
+ - lib/decidim/api/test/shared_examples/commentable_interface_examples.rb
202
+ - lib/decidim/api/test/shared_examples/fingerprintable_interface_examples.rb
203
+ - lib/decidim/api/test/shared_examples/followable_interface_examples.rb
204
+ - lib/decidim/api/test/shared_examples/input_filter_examples.rb
205
+ - lib/decidim/api/test/shared_examples/input_sort_examples.rb
206
+ - lib/decidim/api/test/shared_examples/likeable_interface_examples.rb
207
+ - lib/decidim/api/test/shared_examples/localizable_interface_examples.rb
208
+ - lib/decidim/api/test/shared_examples/participatory_space_resourcable_interface_examples.rb
209
+ - lib/decidim/api/test/shared_examples/referable_interface_examples.rb
210
+ - lib/decidim/api/test/shared_examples/scopable_interface_examples.rb
163
211
  - lib/decidim/api/test/shared_examples/statistics_examples.rb
212
+ - lib/decidim/api/test/shared_examples/taxonomizable_interface_examples.rb
213
+ - lib/decidim/api/test/shared_examples/timestamps_interface_examples.rb
214
+ - lib/decidim/api/test/shared_examples/traceable_interface_examples.rb
164
215
  - lib/decidim/api/test/type_context.rb
165
216
  - lib/decidim/api/types.rb
166
217
  - lib/decidim/api/types/base_argument.rb
@@ -173,7 +224,10 @@ files:
173
224
  - lib/decidim/api/types/base_scalar.rb
174
225
  - lib/decidim/api/types/base_union.rb
175
226
  - lib/decidim/api/version.rb
227
+ - lib/devise/models/api_authenticatable.rb
228
+ - lib/devise/strategies/api_authenticatable.rb
176
229
  - lib/tasks/decidim_api_docs.rake
230
+ - lib/warden/jwt_auth/decidim_overrides.rb
177
231
  homepage: https://decidim.org
178
232
  licenses:
179
233
  - AGPL-3.0-or-later