kinde_sdk 1.6.6 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/kinde_sdk/auth_controller.rb +96 -13
- data/kinde_api/lib/kinde_api/api/frontend/billing_api.rb +148 -0
- data/kinde_api/lib/kinde_api/api/frontend/feature_flags_api.rb +85 -0
- data/kinde_api/lib/kinde_api/api/frontend/o_auth_api.rb +241 -0
- data/kinde_api/lib/kinde_api/api/frontend/permissions_api.rb +85 -0
- data/kinde_api/lib/kinde_api/api/frontend/properties_api.rb +85 -0
- data/kinde_api/lib/kinde_api/api/frontend/roles_api.rb +85 -0
- data/kinde_api/lib/kinde_api/api/frontend/self_serve_portal_api.rb +89 -0
- data/kinde_api/lib/kinde_api/models/frontend/error.rb +230 -0
- data/kinde_api/lib/kinde_api/models/frontend/error_response.rb +221 -0
- data/kinde_api/lib/kinde_api/models/frontend/get_entitlement_response.rb +228 -0
- data/kinde_api/lib/kinde_api/models/frontend/get_entitlement_response_data.rb +229 -0
- data/kinde_api/lib/kinde_api/models/frontend/get_entitlement_response_data_entitlement.rb +295 -0
- data/kinde_api/lib/kinde_api/models/frontend/get_entitlements_response.rb +228 -0
- data/kinde_api/lib/kinde_api/models/frontend/get_entitlements_response_data.rb +244 -0
- data/kinde_api/lib/kinde_api/models/frontend/get_entitlements_response_data_entitlements_inner.rb +294 -0
- data/kinde_api/lib/kinde_api/models/frontend/get_entitlements_response_data_plans_inner.rb +240 -0
- data/kinde_api/lib/kinde_api/models/frontend/get_entitlements_response_metadata.rb +230 -0
- data/kinde_api/lib/kinde_api/models/frontend/get_feature_flags_response.rb +219 -0
- data/kinde_api/lib/kinde_api/models/frontend/get_feature_flags_response_data.rb +222 -0
- data/kinde_api/lib/kinde_api/models/frontend/get_feature_flags_response_data_feature_flags_inner.rb +259 -0
- data/kinde_api/lib/kinde_api/models/frontend/get_feature_flags_response_data_feature_flags_inner_value.rb +108 -0
- data/kinde_api/lib/kinde_api/models/frontend/get_user_permissions_response.rb +228 -0
- data/kinde_api/lib/kinde_api/models/frontend/get_user_permissions_response_data.rb +232 -0
- data/kinde_api/lib/kinde_api/models/frontend/get_user_permissions_response_data_permissions_inner.rb +240 -0
- data/kinde_api/lib/kinde_api/models/frontend/get_user_permissions_response_metadata.rb +230 -0
- data/kinde_api/lib/kinde_api/models/frontend/get_user_properties_response.rb +228 -0
- data/kinde_api/lib/kinde_api/models/frontend/get_user_properties_response_data.rb +222 -0
- data/kinde_api/lib/kinde_api/models/frontend/get_user_properties_response_data_properties_inner.rb +249 -0
- data/kinde_api/lib/kinde_api/models/frontend/get_user_properties_response_data_properties_inner_value.rb +107 -0
- data/kinde_api/lib/kinde_api/models/frontend/get_user_properties_response_metadata.rb +230 -0
- data/kinde_api/lib/kinde_api/models/frontend/get_user_roles_response.rb +228 -0
- data/kinde_api/lib/kinde_api/models/frontend/get_user_roles_response_data.rb +232 -0
- data/kinde_api/lib/kinde_api/models/frontend/get_user_roles_response_data_roles_inner.rb +240 -0
- data/kinde_api/lib/kinde_api/models/frontend/get_user_roles_response_metadata.rb +230 -0
- data/kinde_api/lib/kinde_api/models/frontend/portal_link.rb +220 -0
- data/kinde_api/lib/kinde_api/models/frontend/token_error_response.rb +230 -0
- data/kinde_api/lib/kinde_api/models/frontend/token_introspect.rb +262 -0
- data/kinde_api/lib/kinde_api/models/frontend/user_profile_v2.rb +323 -0
- data/kinde_api/lib/kinde_api.rb +28 -0
- data/lib/kinde_sdk/client/entitlements.rb +86 -0
- data/lib/kinde_sdk/client/feature_flags.rb +246 -10
- data/lib/kinde_sdk/client/permissions.rb +197 -6
- data/lib/kinde_sdk/client/roles.rb +218 -0
- data/lib/kinde_sdk/client.rb +242 -3
- data/lib/kinde_sdk/configuration.rb +2 -0
- data/lib/kinde_sdk/errors.rb +7 -0
- data/lib/kinde_sdk/internal/frontend_client.rb +111 -0
- data/lib/kinde_sdk/version.rb +1 -1
- data/lib/kinde_sdk.rb +9 -2
- metadata +54 -12
@@ -0,0 +1,218 @@
|
|
1
|
+
module KindeSdk
|
2
|
+
class Client
|
3
|
+
module Roles
|
4
|
+
# Get all roles for the authenticated user
|
5
|
+
# Matches the JavaScript SDK API: getRoles(options?)
|
6
|
+
# Implements smart fallback: uses API automatically if token claims are empty
|
7
|
+
#
|
8
|
+
# @param options [Hash] Options for retrieving roles
|
9
|
+
# @option options [Boolean] :force_api (false) If true, calls the API to get fresh roles,
|
10
|
+
# otherwise extracts from token claims. Will auto-fallback to API if token claims are empty.
|
11
|
+
# @option options [Symbol] :token_type (:access_token) The token type to use for soft check (:access_token or :id_token)
|
12
|
+
# @return [Array] Array of role objects with id, name, and key
|
13
|
+
# @example
|
14
|
+
# # Soft check (from token) with auto-fallback to API if empty
|
15
|
+
# client.get_roles
|
16
|
+
# # => [{ id: "role_123", name: "Admin", key: "admin" }]
|
17
|
+
#
|
18
|
+
# # Hard check (from API - always fresh)
|
19
|
+
# client.get_roles(force_api: true)
|
20
|
+
# # => [{ id: "role_123", name: "Admin", key: "admin" }]
|
21
|
+
def get_roles(options = {})
|
22
|
+
# Handle legacy positional argument for backward compatibility
|
23
|
+
if options.is_a?(Symbol)
|
24
|
+
options = { token_type: options }
|
25
|
+
end
|
26
|
+
|
27
|
+
# Extract options with defaults - use member variable if not overridden
|
28
|
+
force_api = options[:force_api] || @force_api || false
|
29
|
+
token_type = options[:token_type] || :access_token
|
30
|
+
|
31
|
+
# Smart fallback logic matching js-utils exactly
|
32
|
+
# Check if we have role claims first (efficiency optimization)
|
33
|
+
roles_claim = get_claim("roles", token_type)
|
34
|
+
|
35
|
+
if force_api || !roles_claim&.dig(:value)&.any?
|
36
|
+
# Use API if explicitly requested OR if token claims are empty
|
37
|
+
log_info("Using API for roles: force_api=#{force_api}, empty_claims=#{!roles_claim&.dig(:value)&.any?}")
|
38
|
+
return get_roles_from_api
|
39
|
+
end
|
40
|
+
|
41
|
+
# Use token claims (soft check)
|
42
|
+
get_roles_from_token(token_type)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Check if user has specific roles
|
46
|
+
# Matches JavaScript SDK hasRoles functionality
|
47
|
+
#
|
48
|
+
# @param role_keys [Array<String>, String] Array of role keys to check, or single role key
|
49
|
+
# @param options [Hash] Options for retrieving roles (same as get_roles)
|
50
|
+
# @return [Boolean] True if user has all specified roles, false otherwise
|
51
|
+
def has_roles?(role_keys, options = {})
|
52
|
+
return true if role_keys.nil? || (role_keys.respond_to?(:empty?) && role_keys.empty?)
|
53
|
+
|
54
|
+
begin
|
55
|
+
user_roles = get_roles(options)
|
56
|
+
role_keys_array = Array(role_keys)
|
57
|
+
user_role_keys = user_roles.map { |role| role[:key] || role['key'] }.compact
|
58
|
+
|
59
|
+
result = role_keys_array.all? { |role_key| user_role_keys.include?(role_key.to_s) }
|
60
|
+
log_debug("Role check for #{role_keys_array}: #{result} (user has: #{user_role_keys})")
|
61
|
+
result
|
62
|
+
rescue StandardError => e
|
63
|
+
log_error("Error checking roles: #{e.message}")
|
64
|
+
false
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# PHP SDK compatible alias for get_roles with hard check
|
69
|
+
# Matches PHP: $client->getRoles()
|
70
|
+
#
|
71
|
+
# @return [Array] Array of role objects
|
72
|
+
def getRoles
|
73
|
+
# Use client's force_api setting, default to true for PHP SDK compatibility
|
74
|
+
force_api_setting = @force_api.nil? ? true : @force_api
|
75
|
+
get_roles(force_api: force_api_setting)
|
76
|
+
end
|
77
|
+
|
78
|
+
# JavaScript SDK compatible alias
|
79
|
+
alias_method :hasRoles, :has_roles?
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
# Get roles from token claims (soft check)
|
84
|
+
# Matches JavaScript logic: token.roles || token["x-hasura-roles"] || []
|
85
|
+
# Includes helpful warnings like js-utils
|
86
|
+
#
|
87
|
+
# @param token_type [Symbol] The token type to use
|
88
|
+
# @return [Array] Array of role objects
|
89
|
+
def get_roles_from_token(token_type = :access_token)
|
90
|
+
# First try standard roles claim
|
91
|
+
roles = get_claim("roles", token_type)&.dig(:value)
|
92
|
+
|
93
|
+
# Fallback to Hasura-specific roles (matches JS SDK)
|
94
|
+
if roles.nil? || roles.empty?
|
95
|
+
roles = get_claim("x-hasura-roles", token_type)&.dig(:value)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Warning message matching js-utils behavior
|
99
|
+
if roles.nil? || roles.empty?
|
100
|
+
log_warning("No roles found in token. Ensure roles have been included in the token customisation within the application settings.")
|
101
|
+
return []
|
102
|
+
end
|
103
|
+
|
104
|
+
# Ensure consistent format - normalize all role data
|
105
|
+
normalize_roles(roles)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Get roles from API (hard check)
|
109
|
+
# Matches JavaScript API endpoint and data extraction exactly
|
110
|
+
#
|
111
|
+
# @return [Array] Array of role objects
|
112
|
+
def get_roles_from_api
|
113
|
+
unless token_store.bearer_token
|
114
|
+
log_warning("No bearer token available for API call")
|
115
|
+
return []
|
116
|
+
end
|
117
|
+
|
118
|
+
begin
|
119
|
+
# Use the same pagination pattern as getAllEntitlements
|
120
|
+
all_roles = paginate_all_results('roles') do |starting_after|
|
121
|
+
user_roles(page_size: 100, starting_after: starting_after)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Extract and normalize role data (matches js-utils format exactly)
|
125
|
+
normalized_roles = normalize_roles(all_roles)
|
126
|
+
log_debug("Retrieved #{normalized_roles.count} roles from API")
|
127
|
+
normalized_roles
|
128
|
+
rescue KindeSdk::APIError => e
|
129
|
+
log_error("API Error getting roles: #{e.message}")
|
130
|
+
# Graceful fallback to token-based roles (matches JS behavior)
|
131
|
+
log_info("Falling back to token-based roles due to API error")
|
132
|
+
get_roles_from_token
|
133
|
+
rescue StandardError => e
|
134
|
+
log_error("Unexpected error getting roles from API: #{e.message}")
|
135
|
+
# Graceful fallback to token-based roles
|
136
|
+
log_info("Falling back to token-based roles due to unexpected error")
|
137
|
+
get_roles_from_token
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Normalize role data to consistent format
|
142
|
+
# Ensures we always return { id:, name:, key: } format like js-utils
|
143
|
+
#
|
144
|
+
# @param roles [Array] Raw role data from token or API
|
145
|
+
# @return [Array] Normalized role objects
|
146
|
+
def normalize_roles(roles)
|
147
|
+
return [] if roles.nil?
|
148
|
+
|
149
|
+
Array(roles).map do |role|
|
150
|
+
if role.is_a?(Hash) || role.respond_to?(:id)
|
151
|
+
{
|
152
|
+
id: extract_field(role, :id),
|
153
|
+
name: extract_field(role, :name),
|
154
|
+
key: extract_field(role, :key)
|
155
|
+
}
|
156
|
+
else
|
157
|
+
# Handle string-only roles (fallback)
|
158
|
+
role_str = role.to_s
|
159
|
+
{
|
160
|
+
id: nil,
|
161
|
+
name: role_str,
|
162
|
+
key: role_str
|
163
|
+
}
|
164
|
+
end
|
165
|
+
end.compact
|
166
|
+
end
|
167
|
+
|
168
|
+
# Helper to extract field from hash or object
|
169
|
+
def extract_field(item, field)
|
170
|
+
if item.respond_to?(field)
|
171
|
+
item.public_send(field)
|
172
|
+
elsif item.is_a?(Hash)
|
173
|
+
item[field] || item[field.to_s]
|
174
|
+
else
|
175
|
+
nil
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Configurable logging that works with or without Rails
|
180
|
+
# Supports multiple log levels for better debugging
|
181
|
+
def log_error(message)
|
182
|
+
write_log(:error, message)
|
183
|
+
end
|
184
|
+
|
185
|
+
def log_warning(message)
|
186
|
+
write_log(:warn, message)
|
187
|
+
end
|
188
|
+
|
189
|
+
def log_info(message)
|
190
|
+
write_log(:info, message)
|
191
|
+
end
|
192
|
+
|
193
|
+
def log_debug(message)
|
194
|
+
write_log(:debug, message)
|
195
|
+
end
|
196
|
+
|
197
|
+
def write_log(level, message)
|
198
|
+
formatted_message = "[KindeSdk::Roles] #{message}"
|
199
|
+
|
200
|
+
if defined?(Rails) && Rails.logger
|
201
|
+
Rails.logger.public_send(level, formatted_message)
|
202
|
+
elsif @logger && @logger.respond_to?(level)
|
203
|
+
@logger.public_send(level, formatted_message)
|
204
|
+
elsif respond_to?(:logger) && logger && logger.respond_to?(level)
|
205
|
+
logger.public_send(level, formatted_message)
|
206
|
+
else
|
207
|
+
# Fallback based on level
|
208
|
+
case level
|
209
|
+
when :error, :warn
|
210
|
+
$stderr.puts formatted_message
|
211
|
+
when :info, :debug
|
212
|
+
$stdout.puts formatted_message if ENV['KINDE_DEBUG']
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
data/lib/kinde_sdk/client.rb
CHANGED
@@ -2,9 +2,15 @@ require_relative '../../kinde_api/lib/kinde_api'
|
|
2
2
|
require_relative 'token_manager'
|
3
3
|
require_relative 'token_store'
|
4
4
|
require_relative 'current'
|
5
|
+
require_relative 'errors'
|
6
|
+
require_relative 'internal/frontend_client'
|
7
|
+
require_relative 'client/feature_flags'
|
8
|
+
require_relative 'client/permissions'
|
9
|
+
require_relative 'client/roles'
|
10
|
+
require_relative 'client/entitlements'
|
5
11
|
|
6
12
|
module KindeSdk
|
7
|
-
# Constants for portal page navigation
|
13
|
+
# Constants for portal page navigation - matches PHP SDK exactly
|
8
14
|
module PortalPage
|
9
15
|
ORGANIZATION_DETAILS = 'organization_details'
|
10
16
|
ORGANIZATION_MEMBERS = 'organization_members'
|
@@ -17,13 +23,16 @@ module KindeSdk
|
|
17
23
|
class Client
|
18
24
|
include FeatureFlags
|
19
25
|
include Permissions
|
26
|
+
include Roles
|
27
|
+
include Entitlements
|
20
28
|
|
21
|
-
attr_accessor :kinde_api_client, :auto_refresh_tokens, :token_store
|
29
|
+
attr_accessor :kinde_api_client, :auto_refresh_tokens, :token_store, :force_api
|
22
30
|
|
23
|
-
def initialize(sdk_api_client, tokens_hash, auto_refresh_tokens)
|
31
|
+
def initialize(sdk_api_client, tokens_hash, auto_refresh_tokens, force_api = false)
|
24
32
|
@kinde_api_client = sdk_api_client
|
25
33
|
@auto_refresh_tokens = auto_refresh_tokens
|
26
34
|
@token_store = TokenManager.create_store(tokens_hash)
|
35
|
+
@force_api = force_api
|
27
36
|
|
28
37
|
# refresh the token if it's expired and auto_refresh_tokens is enabled
|
29
38
|
refresh_token if auto_refresh_tokens && TokenManager.token_expired?(@token_store)
|
@@ -150,8 +159,237 @@ module KindeSdk
|
|
150
159
|
oauth_api
|
151
160
|
end
|
152
161
|
|
162
|
+
# Frontend API access - Internal use only
|
163
|
+
# Uses user access tokens instead of M2M tokens
|
164
|
+
def frontend
|
165
|
+
@frontend_client ||= KindeSdk::Internal::FrontendClient.new(@token_store, KindeSdk.config.domain)
|
166
|
+
end
|
167
|
+
|
168
|
+
# Public SDK methods that use Frontend API internally
|
169
|
+
|
170
|
+
# Get entitlements for the authenticated user with pagination support.
|
171
|
+
#
|
172
|
+
# @param page_size [Integer] Number of results per page (default: 10)
|
173
|
+
# @param starting_after [String] The ID to start after for pagination
|
174
|
+
# @return [OpenStruct] Response containing entitlements data
|
175
|
+
# @raise [KindeSdk::APIError] If the user is not authenticated or API request fails
|
176
|
+
def entitlements(page_size: 10, starting_after: nil)
|
177
|
+
frontend.get_entitlements(page_size: page_size, starting_after: starting_after)
|
178
|
+
rescue StandardError => e
|
179
|
+
Rails.logger.error("Failed to fetch entitlements: #{e.message}")
|
180
|
+
raise KindeSdk::APIError, "Unable to fetch entitlements: #{e.message}"
|
181
|
+
end
|
182
|
+
|
183
|
+
# Get all entitlements for the authenticated user, handling pagination automatically.
|
184
|
+
#
|
185
|
+
# @return [Array] All entitlements
|
186
|
+
# @raise [KindeSdk::APIError] If the user is not authenticated or API request fails
|
187
|
+
def getAllEntitlements
|
188
|
+
unless token_store.bearer_token
|
189
|
+
raise KindeSdk::APIError, 'User must be authenticated to get entitlements'
|
190
|
+
end
|
191
|
+
|
192
|
+
paginate_all_results('entitlements') { |starting_after| entitlements(page_size: 100, starting_after: starting_after) }
|
193
|
+
rescue StandardError => e
|
194
|
+
Rails.logger.error("Failed to fetch all entitlements: #{e.message}")
|
195
|
+
raise KindeSdk::APIError, "Unable to fetch all entitlements: #{e.message}"
|
196
|
+
end
|
197
|
+
|
198
|
+
# Ruby-style alias for getAllEntitlements
|
199
|
+
alias_method :all_entitlements, :getAllEntitlements
|
200
|
+
|
201
|
+
# Get a specific entitlement by key.
|
202
|
+
#
|
203
|
+
# @param key [String] The entitlement key to retrieve
|
204
|
+
# @return [OpenStruct, nil] The entitlement response or nil if not found
|
205
|
+
# @raise [KindeSdk::APIError] If the user is not authenticated or API request fails
|
206
|
+
def entitlement(key)
|
207
|
+
frontend.get_entitlement(key)
|
208
|
+
rescue StandardError => e
|
209
|
+
Rails.logger.error("Failed to fetch entitlement for #{key}: #{e.message}")
|
210
|
+
raise KindeSdk::APIError, "Unable to fetch entitlement: #{e.message}"
|
211
|
+
end
|
212
|
+
|
213
|
+
# PHP SDK compatible alias for entitlement
|
214
|
+
#
|
215
|
+
# @param key [String] The entitlement key to retrieve
|
216
|
+
# @return [OpenStruct, nil] The entitlement response or nil if not found
|
217
|
+
# @raise [KindeSdk::APIError] If the user is not authenticated or API request fails
|
218
|
+
def getEntitlement(key)
|
219
|
+
# Find the specific entitlement from all entitlements (like PHP SDK does)
|
220
|
+
entitlements = getAllEntitlements
|
221
|
+
|
222
|
+
entitlements.find { |entitlement| entitlement.feature_key == key }
|
223
|
+
rescue StandardError => e
|
224
|
+
Rails.logger.error("Failed to get entitlement for #{key}: #{e.message}")
|
225
|
+
raise KindeSdk::APIError, "Unable to get entitlement: #{e.message}"
|
226
|
+
end
|
227
|
+
|
228
|
+
# Check if the user has a specific entitlement.
|
229
|
+
#
|
230
|
+
# @param feature_key [String] The entitlement key to check
|
231
|
+
# @return [Boolean] True if the user has the entitlement, false otherwise
|
232
|
+
def has_entitlement?(feature_key)
|
233
|
+
entitlement_response = entitlement(feature_key)
|
234
|
+
entitlement_response&.data&.entitlement.present?
|
235
|
+
rescue StandardError => e
|
236
|
+
Rails.logger.error("Error checking entitlement for #{feature_key}: #{e.message}")
|
237
|
+
false
|
238
|
+
end
|
239
|
+
|
240
|
+
# PHP SDK compatible alias for has_entitlement?
|
241
|
+
#
|
242
|
+
# @param key [String] The entitlement key to check
|
243
|
+
# @return [Boolean] True if the user has the entitlement, false otherwise
|
244
|
+
def hasEntitlement(key)
|
245
|
+
getEntitlement(key) != nil
|
246
|
+
rescue StandardError => e
|
247
|
+
Rails.logger.error("Error checking entitlement for #{key}: #{e.message}")
|
248
|
+
false
|
249
|
+
end
|
250
|
+
|
251
|
+
# Get the maximum limit for a specific entitlement.
|
252
|
+
#
|
253
|
+
# @param key [String] The entitlement key
|
254
|
+
# @return [Integer, nil] The maximum limit or nil if not found
|
255
|
+
# @raise [KindeSdk::APIError] If the user is not authenticated or API request fails
|
256
|
+
def getEntitlementLimit(key)
|
257
|
+
entitlement = getEntitlement(key)
|
258
|
+
entitlement ? entitlement.entitlement_limit_max : nil
|
259
|
+
rescue StandardError => e
|
260
|
+
Rails.logger.error("Error getting entitlement limit for #{key}: #{e.message}")
|
261
|
+
nil
|
262
|
+
end
|
263
|
+
|
264
|
+
# Ruby-style alias for getEntitlementLimit
|
265
|
+
alias_method :entitlement_limit, :getEntitlementLimit
|
266
|
+
|
267
|
+
# Get user feature flags with pagination support.
|
268
|
+
#
|
269
|
+
# @param page_size [Integer] Number of results per page (default: 10)
|
270
|
+
# @param starting_after [String] The ID to start after for pagination
|
271
|
+
# @return [OpenStruct] Response containing feature flags data
|
272
|
+
# @raise [KindeSdk::APIError] If the user is not authenticated or API request fails
|
273
|
+
def user_feature_flags(page_size: 10, starting_after: nil)
|
274
|
+
frontend.get_feature_flags(page_size: page_size, starting_after: starting_after)
|
275
|
+
rescue StandardError => e
|
276
|
+
Rails.logger.error("Failed to fetch feature flags: #{e.message}")
|
277
|
+
raise KindeSdk::APIError, "Unable to fetch feature flags: #{e.message}"
|
278
|
+
end
|
279
|
+
|
280
|
+
# Get user permissions with pagination support.
|
281
|
+
#
|
282
|
+
# @param page_size [Integer] Number of results per page (default: 10)
|
283
|
+
# @param starting_after [String] The ID to start after for pagination
|
284
|
+
# @return [OpenStruct] Response containing permissions data
|
285
|
+
# @raise [KindeSdk::APIError] If the user is not authenticated or API request fails
|
286
|
+
def user_permissions(page_size: 10, starting_after: nil)
|
287
|
+
frontend.get_user_permissions(page_size: page_size, starting_after: starting_after)
|
288
|
+
rescue StandardError => e
|
289
|
+
Rails.logger.error("Failed to fetch permissions: #{e.message}")
|
290
|
+
raise KindeSdk::APIError, "Unable to fetch permissions: #{e.message}"
|
291
|
+
end
|
292
|
+
|
293
|
+
# Get user properties with pagination support.
|
294
|
+
#
|
295
|
+
# @param page_size [Integer] Number of results per page (default: 10)
|
296
|
+
# @param starting_after [String] The ID to start after for pagination
|
297
|
+
# @return [OpenStruct] Response containing properties data
|
298
|
+
# @raise [KindeSdk::APIError] If the user is not authenticated or API request fails
|
299
|
+
def user_properties(page_size: 10, starting_after: nil)
|
300
|
+
frontend.get_user_properties(page_size: page_size, starting_after: starting_after)
|
301
|
+
rescue StandardError => e
|
302
|
+
Rails.logger.error("Failed to fetch properties: #{e.message}")
|
303
|
+
raise KindeSdk::APIError, "Unable to fetch properties: #{e.message}"
|
304
|
+
end
|
305
|
+
|
306
|
+
# Get user roles with pagination support.
|
307
|
+
#
|
308
|
+
# @param page_size [Integer] Number of results per page (default: 10)
|
309
|
+
# @param starting_after [String] The ID to start after for pagination
|
310
|
+
# @return [OpenStruct] Response containing roles data
|
311
|
+
# @raise [KindeSdk::APIError] If the user is not authenticated or API request fails
|
312
|
+
def user_roles(page_size: 10, starting_after: nil)
|
313
|
+
frontend.get_user_roles(page_size: page_size, starting_after: starting_after)
|
314
|
+
rescue StandardError => e
|
315
|
+
Rails.logger.error("Failed to fetch roles: #{e.message}")
|
316
|
+
raise KindeSdk::APIError, "Unable to fetch roles: #{e.message}"
|
317
|
+
end
|
318
|
+
|
319
|
+
# Generate a URL to the user profile portal.
|
320
|
+
#
|
321
|
+
# @param return_url [String] URL to redirect to after completing the profile flow
|
322
|
+
# @param sub_nav [String] Sub-navigation section to display (defaults to 'profile')
|
323
|
+
# @return [Hash] A hash containing the generated URL
|
324
|
+
# @raise [KindeSdk::APIError] If the user is not authenticated or API request fails
|
325
|
+
def portal_link(return_url:, page: PortalPage::PROFILE)
|
326
|
+
frontend.get_portal_link(subnav: page, return_url: return_url)
|
327
|
+
rescue StandardError => e
|
328
|
+
Rails.logger.error("Failed to get portal link: #{e.message}")
|
329
|
+
raise KindeSdk::APIError, "Unable to get portal link: #{e.message}"
|
330
|
+
end
|
331
|
+
|
332
|
+
# PHP SDK compatible portal URL generation method.
|
333
|
+
#
|
334
|
+
# @param return_url [String] URL to redirect to after completing the profile flow
|
335
|
+
# @param sub_nav [String] Sub-navigation section to display (defaults to 'profile')
|
336
|
+
# @return [Hash] A hash containing the generated URL
|
337
|
+
# @raise [KindeSdk::APIError] If the user is not authenticated or API request fails
|
338
|
+
# @raise [ArgumentError] If the return_url is not an absolute URL
|
339
|
+
def generatePortalUrl(return_url, sub_nav = PortalPage::PROFILE)
|
340
|
+
unless token_store.bearer_token
|
341
|
+
raise KindeSdk::APIError, 'generatePortalUrl: Access Token not found'
|
342
|
+
end
|
343
|
+
|
344
|
+
unless return_url.start_with?('http')
|
345
|
+
raise ArgumentError, 'generatePortalUrl: returnUrl must be an absolute URL'
|
346
|
+
end
|
347
|
+
|
348
|
+
frontend.get_portal_link(subnav: sub_nav, return_url: return_url)
|
349
|
+
rescue StandardError => e
|
350
|
+
Rails.logger.error("Failed to generate portal URL: #{e.message}")
|
351
|
+
raise KindeSdk::APIError, "Unable to generate portal URL: #{e.message}"
|
352
|
+
end
|
353
|
+
|
354
|
+
# Get enhanced user profile information.
|
355
|
+
#
|
356
|
+
# @return [OpenStruct] Enhanced user profile data
|
357
|
+
# @raise [KindeSdk::APIError] If the user is not authenticated or API request fails
|
358
|
+
def enhanced_user_profile
|
359
|
+
frontend.get_user_profile_v2
|
360
|
+
rescue StandardError => e
|
361
|
+
Rails.logger.error("Failed to fetch enhanced profile: #{e.message}")
|
362
|
+
raise KindeSdk::APIError, "Unable to fetch enhanced profile: #{e.message}"
|
363
|
+
end
|
364
|
+
|
153
365
|
private
|
154
366
|
|
367
|
+
# Generic pagination helper for all paginated API calls
|
368
|
+
#
|
369
|
+
# @param data_key [String] The key in the response data containing the array of results
|
370
|
+
# @param block [Proc] Block that makes the API call with starting_after parameter
|
371
|
+
# @return [Array] All paginated results
|
372
|
+
def paginate_all_results(data_key, &block)
|
373
|
+
all_results = []
|
374
|
+
starting_after = nil
|
375
|
+
|
376
|
+
loop do
|
377
|
+
response = block.call(starting_after)
|
378
|
+
current_results = response&.data&.public_send(data_key) || []
|
379
|
+
all_results.concat(current_results)
|
380
|
+
|
381
|
+
metadata = response&.metadata
|
382
|
+
has_more = metadata&.has_more
|
383
|
+
|
384
|
+
break unless has_more && current_results.any?
|
385
|
+
|
386
|
+
starting_after = metadata&.next_page_starting_after
|
387
|
+
break unless starting_after
|
388
|
+
end
|
389
|
+
|
390
|
+
all_results
|
391
|
+
end
|
392
|
+
|
155
393
|
def init_instance_api(api_klass)
|
156
394
|
|
157
395
|
instance = api_klass.new(kinde_api_client)
|
@@ -169,5 +407,6 @@ module KindeSdk
|
|
169
407
|
end
|
170
408
|
instance
|
171
409
|
end
|
410
|
+
|
172
411
|
end
|
173
412
|
end
|
@@ -20,6 +20,7 @@ module KindeSdk
|
|
20
20
|
attr_accessor :oauth_client
|
21
21
|
attr_accessor :pkce_enabled
|
22
22
|
attr_accessor :auto_refresh_tokens
|
23
|
+
attr_accessor :force_api
|
23
24
|
|
24
25
|
def initialize
|
25
26
|
@authorize_url = '/oauth2/auth'
|
@@ -32,6 +33,7 @@ module KindeSdk
|
|
32
33
|
@scope = 'openid offline email profile'
|
33
34
|
@pkce_enabled = true
|
34
35
|
@auto_refresh_tokens = true
|
36
|
+
@force_api = false
|
35
37
|
|
36
38
|
yield(self) if block_given?
|
37
39
|
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'json'
|
3
|
+
require 'ostruct'
|
4
|
+
|
5
|
+
module KindeSdk
|
6
|
+
module Internal
|
7
|
+
class FrontendClient
|
8
|
+
include HTTParty
|
9
|
+
|
10
|
+
def initialize(token_store, domain)
|
11
|
+
@token_store = token_store
|
12
|
+
@domain = domain
|
13
|
+
@base_uri = "#{domain}/account_api/v1"
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_entitlements(page_size: 10, starting_after: nil)
|
17
|
+
make_request('/entitlements', {
|
18
|
+
page_size: page_size,
|
19
|
+
starting_after: starting_after
|
20
|
+
}.compact)
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_entitlement(key)
|
24
|
+
make_request("/entitlement", { key: key })
|
25
|
+
end
|
26
|
+
|
27
|
+
def get_user_permissions(page_size: 10, starting_after: nil)
|
28
|
+
make_request('/permissions', {
|
29
|
+
page_size: page_size,
|
30
|
+
starting_after: starting_after
|
31
|
+
}.compact)
|
32
|
+
end
|
33
|
+
|
34
|
+
def get_user_properties(page_size: 10, starting_after: nil)
|
35
|
+
make_request('/properties', {
|
36
|
+
page_size: page_size,
|
37
|
+
starting_after: starting_after
|
38
|
+
}.compact)
|
39
|
+
end
|
40
|
+
|
41
|
+
def get_user_roles(page_size: 10, starting_after: nil)
|
42
|
+
make_request('/roles', {
|
43
|
+
page_size: page_size,
|
44
|
+
starting_after: starting_after
|
45
|
+
}.compact)
|
46
|
+
end
|
47
|
+
|
48
|
+
def get_feature_flags(page_size: 10, starting_after: nil)
|
49
|
+
make_request('/feature_flags', {
|
50
|
+
page_size: page_size,
|
51
|
+
starting_after: starting_after
|
52
|
+
}.compact)
|
53
|
+
end
|
54
|
+
|
55
|
+
def get_portal_link(subnav: nil, return_url: nil)
|
56
|
+
make_request('/portal_link', {
|
57
|
+
subnav: subnav,
|
58
|
+
return_url: return_url
|
59
|
+
}.compact)
|
60
|
+
end
|
61
|
+
|
62
|
+
def get_user_profile_v2
|
63
|
+
url = "#{@domain}/oauth2/v2/user_profile"
|
64
|
+
|
65
|
+
response = HTTParty.get(url, {
|
66
|
+
headers: {
|
67
|
+
'Authorization' => "Bearer #{@token_store.bearer_token}",
|
68
|
+
'Content-Type' => 'application/json'
|
69
|
+
}
|
70
|
+
})
|
71
|
+
|
72
|
+
handle_response(response)
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def make_request(endpoint, params = {})
|
78
|
+
url = "#{@base_uri}#{endpoint}"
|
79
|
+
|
80
|
+
response = HTTParty.get(url, {
|
81
|
+
query: params,
|
82
|
+
headers: {
|
83
|
+
'Authorization' => "Bearer #{@token_store.bearer_token}",
|
84
|
+
'Content-Type' => 'application/json'
|
85
|
+
}
|
86
|
+
})
|
87
|
+
|
88
|
+
handle_response(response)
|
89
|
+
end
|
90
|
+
|
91
|
+
def handle_response(response)
|
92
|
+
case response.code
|
93
|
+
when 200
|
94
|
+
# Parse as OpenStruct for easy access while keeping it internal
|
95
|
+
JSON.parse(response.body, object_class: OpenStruct)
|
96
|
+
when 401
|
97
|
+
raise KindeSdk::AuthenticationError, "Invalid or expired token"
|
98
|
+
when 403
|
99
|
+
raise KindeSdk::AuthorizationError, "Insufficient permissions"
|
100
|
+
when 404
|
101
|
+
# For entitlements, return nil when not found (matches PHP SDK behavior)
|
102
|
+
nil
|
103
|
+
when 429
|
104
|
+
raise KindeSdk::RateLimitError, "Too many requests"
|
105
|
+
else
|
106
|
+
raise KindeSdk::APIError, "API request failed with status #{response.code}: #{response.body}"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
data/lib/kinde_sdk/version.rb
CHANGED
data/lib/kinde_sdk.rb
CHANGED
@@ -6,6 +6,7 @@ require "kinde_sdk/client/feature_flags"
|
|
6
6
|
require "kinde_sdk/client/permissions"
|
7
7
|
require "kinde_sdk/client"
|
8
8
|
require "kinde_sdk/current"
|
9
|
+
require "kinde_sdk/errors"
|
9
10
|
require 'securerandom'
|
10
11
|
require 'oauth2'
|
11
12
|
require 'pkce_challenge'
|
@@ -16,7 +17,12 @@ require 'jwt'
|
|
16
17
|
require 'openssl'
|
17
18
|
require 'base64'
|
18
19
|
|
20
|
+
|
21
|
+
|
19
22
|
module KindeSdk
|
23
|
+
|
24
|
+
|
25
|
+
|
20
26
|
class << self
|
21
27
|
attr_accessor :config
|
22
28
|
|
@@ -47,6 +53,7 @@ module KindeSdk
|
|
47
53
|
redirect_uri: redirect_uri,
|
48
54
|
state: SecureRandom.hex,
|
49
55
|
scope: @config.scope,
|
56
|
+
supports_reauth: "true"
|
50
57
|
}.merge(**kwargs)
|
51
58
|
return { url: @config.oauth_client(
|
52
59
|
client_id: client_id,
|
@@ -110,9 +117,9 @@ module KindeSdk
|
|
110
117
|
# "token_type"=>"bearer"}
|
111
118
|
#
|
112
119
|
# @return [KindeSdk::Client]
|
113
|
-
def client(tokens_hash, auto_refresh_tokens = @config.auto_refresh_tokens)
|
120
|
+
def client(tokens_hash, auto_refresh_tokens = @config.auto_refresh_tokens, force_api = @config.force_api)
|
114
121
|
sdk_api_client = api_client(tokens_hash[:access_token] || tokens_hash["access_token"])
|
115
|
-
KindeSdk::Client.new(sdk_api_client, tokens_hash, auto_refresh_tokens)
|
122
|
+
KindeSdk::Client.new(sdk_api_client, tokens_hash, auto_refresh_tokens, force_api)
|
116
123
|
end
|
117
124
|
|
118
125
|
def logout_url(logout_url: @config.logout_url, domain: @config.domain)
|