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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/kinde_sdk/auth_controller.rb +96 -13
  3. data/kinde_api/lib/kinde_api/api/frontend/billing_api.rb +148 -0
  4. data/kinde_api/lib/kinde_api/api/frontend/feature_flags_api.rb +85 -0
  5. data/kinde_api/lib/kinde_api/api/frontend/o_auth_api.rb +241 -0
  6. data/kinde_api/lib/kinde_api/api/frontend/permissions_api.rb +85 -0
  7. data/kinde_api/lib/kinde_api/api/frontend/properties_api.rb +85 -0
  8. data/kinde_api/lib/kinde_api/api/frontend/roles_api.rb +85 -0
  9. data/kinde_api/lib/kinde_api/api/frontend/self_serve_portal_api.rb +89 -0
  10. data/kinde_api/lib/kinde_api/models/frontend/error.rb +230 -0
  11. data/kinde_api/lib/kinde_api/models/frontend/error_response.rb +221 -0
  12. data/kinde_api/lib/kinde_api/models/frontend/get_entitlement_response.rb +228 -0
  13. data/kinde_api/lib/kinde_api/models/frontend/get_entitlement_response_data.rb +229 -0
  14. data/kinde_api/lib/kinde_api/models/frontend/get_entitlement_response_data_entitlement.rb +295 -0
  15. data/kinde_api/lib/kinde_api/models/frontend/get_entitlements_response.rb +228 -0
  16. data/kinde_api/lib/kinde_api/models/frontend/get_entitlements_response_data.rb +244 -0
  17. data/kinde_api/lib/kinde_api/models/frontend/get_entitlements_response_data_entitlements_inner.rb +294 -0
  18. data/kinde_api/lib/kinde_api/models/frontend/get_entitlements_response_data_plans_inner.rb +240 -0
  19. data/kinde_api/lib/kinde_api/models/frontend/get_entitlements_response_metadata.rb +230 -0
  20. data/kinde_api/lib/kinde_api/models/frontend/get_feature_flags_response.rb +219 -0
  21. data/kinde_api/lib/kinde_api/models/frontend/get_feature_flags_response_data.rb +222 -0
  22. data/kinde_api/lib/kinde_api/models/frontend/get_feature_flags_response_data_feature_flags_inner.rb +259 -0
  23. data/kinde_api/lib/kinde_api/models/frontend/get_feature_flags_response_data_feature_flags_inner_value.rb +108 -0
  24. data/kinde_api/lib/kinde_api/models/frontend/get_user_permissions_response.rb +228 -0
  25. data/kinde_api/lib/kinde_api/models/frontend/get_user_permissions_response_data.rb +232 -0
  26. data/kinde_api/lib/kinde_api/models/frontend/get_user_permissions_response_data_permissions_inner.rb +240 -0
  27. data/kinde_api/lib/kinde_api/models/frontend/get_user_permissions_response_metadata.rb +230 -0
  28. data/kinde_api/lib/kinde_api/models/frontend/get_user_properties_response.rb +228 -0
  29. data/kinde_api/lib/kinde_api/models/frontend/get_user_properties_response_data.rb +222 -0
  30. data/kinde_api/lib/kinde_api/models/frontend/get_user_properties_response_data_properties_inner.rb +249 -0
  31. data/kinde_api/lib/kinde_api/models/frontend/get_user_properties_response_data_properties_inner_value.rb +107 -0
  32. data/kinde_api/lib/kinde_api/models/frontend/get_user_properties_response_metadata.rb +230 -0
  33. data/kinde_api/lib/kinde_api/models/frontend/get_user_roles_response.rb +228 -0
  34. data/kinde_api/lib/kinde_api/models/frontend/get_user_roles_response_data.rb +232 -0
  35. data/kinde_api/lib/kinde_api/models/frontend/get_user_roles_response_data_roles_inner.rb +240 -0
  36. data/kinde_api/lib/kinde_api/models/frontend/get_user_roles_response_metadata.rb +230 -0
  37. data/kinde_api/lib/kinde_api/models/frontend/portal_link.rb +220 -0
  38. data/kinde_api/lib/kinde_api/models/frontend/token_error_response.rb +230 -0
  39. data/kinde_api/lib/kinde_api/models/frontend/token_introspect.rb +262 -0
  40. data/kinde_api/lib/kinde_api/models/frontend/user_profile_v2.rb +323 -0
  41. data/kinde_api/lib/kinde_api.rb +28 -0
  42. data/lib/kinde_sdk/client/entitlements.rb +86 -0
  43. data/lib/kinde_sdk/client/feature_flags.rb +246 -10
  44. data/lib/kinde_sdk/client/permissions.rb +197 -6
  45. data/lib/kinde_sdk/client/roles.rb +218 -0
  46. data/lib/kinde_sdk/client.rb +242 -3
  47. data/lib/kinde_sdk/configuration.rb +2 -0
  48. data/lib/kinde_sdk/errors.rb +7 -0
  49. data/lib/kinde_sdk/internal/frontend_client.rb +111 -0
  50. data/lib/kinde_sdk/version.rb +1 -1
  51. data/lib/kinde_sdk.rb +9 -2
  52. 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
@@ -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,7 @@
1
+ module KindeSdk
2
+ class Error < StandardError; end
3
+ class APIError < Error; end
4
+ class AuthenticationError < APIError; end
5
+ class AuthorizationError < APIError; end
6
+ class RateLimitError < APIError; end
7
+ 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
@@ -1,3 +1,3 @@
1
1
  module KindeSdk
2
- VERSION = "1.6.6"
2
+ VERSION = "1.7.0"
3
3
  end
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)