aha_builder_core 1.0.16 → 1.0.17
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/README.md +6 -22
- data/lib/aha/auth/client.rb +44 -14
- data/lib/aha/auth/configuration.rb +4 -3
- data/lib/aha/version.rb +1 -1
- data/lib/aha_builder_core.rb +0 -1
- metadata +3 -5
- data/lib/aha/mail/delivery_method.rb +0 -66
- data/lib/aha/mail.rb +0 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 913c608a336e01e9d8ac3084627502ccdbb27de2e1e29be6e820f9121c810b30
|
|
4
|
+
data.tar.gz: 0f9fae30f6eb0295b361a778166adba1f5ab85410060993d4731a334bed35446
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d3aef9032b88ce77e637cecfba97720bcc47d00e2b0c340408df0fd1f1f60248707bcdf27a91150ddec008c034970f845fea601a756869a0002a03eda505eccf
|
|
7
|
+
data.tar.gz: 1070ef7ede56f66267aab822a8843beabe40fc3717faba0a3844b5a982072499e68b62fd392f045d2bce4d2c1d3ae3935299bc47259ae82d1e4cda655c95e4c2
|
data/README.md
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
# Aha Builder Core Client
|
|
2
2
|
|
|
3
|
-
Ruby client for Aha! Builder core which provides
|
|
4
|
-
|
|
5
|
-
* Authentication services for login and signup using email/password, social logins (Google, Github, Microsoft), SAML SSO and password reset.
|
|
6
|
-
* Email sending API.
|
|
7
|
-
* User guide content API.
|
|
3
|
+
Ruby client for Aha! Builder core authentication services which provides login and signup using email/password, social logins (Google, Github, Microsoft), SAML SSO and password reset.
|
|
8
4
|
|
|
9
5
|
## Installation
|
|
10
6
|
|
|
@@ -118,10 +114,15 @@ end
|
|
|
118
114
|
|
|
119
115
|
### Logout
|
|
120
116
|
|
|
117
|
+
Implement logout by making a Ruby call to the library.
|
|
118
|
+
|
|
121
119
|
```ruby
|
|
122
120
|
Aha::Auth.logout(session_token: session_token)
|
|
123
121
|
```
|
|
124
122
|
|
|
123
|
+
After logout the user should be redirected back to the login page. The front-end
|
|
124
|
+
should not use XHR for the logout request since it can't handle a redirect.
|
|
125
|
+
|
|
125
126
|
## User Management
|
|
126
127
|
|
|
127
128
|
Server-to-server operations (requires `api_key`):
|
|
@@ -205,20 +206,3 @@ Each page hash contains:
|
|
|
205
206
|
- `position` - Sort order (integer)
|
|
206
207
|
- `children_count` - Number of child pages
|
|
207
208
|
- `content_html` - HTML content (only in `load_page` response)
|
|
208
|
-
|
|
209
|
-
## Email Sending
|
|
210
|
-
|
|
211
|
-
Aha! Builder core provides a mail API that can be used as an ActionMailer delivery method.
|
|
212
|
-
|
|
213
|
-
### ActionMailer Integration
|
|
214
|
-
|
|
215
|
-
Configure your Rails application to send emails through Aha! Builder core:
|
|
216
|
-
|
|
217
|
-
```ruby
|
|
218
|
-
# config/initializers/action_mailer.rb
|
|
219
|
-
ActionMailer::Base.add_delivery_method :aha_mail, Aha::Mail::DeliveryMethod
|
|
220
|
-
|
|
221
|
-
# config/environments/production.rb
|
|
222
|
-
config.action_mailer.delivery_method = :aha_mail
|
|
223
|
-
```
|
|
224
|
-
|
data/lib/aha/auth/client.rb
CHANGED
|
@@ -49,27 +49,27 @@ module Aha
|
|
|
49
49
|
# @param refresh_token [String, nil] Optional refresh token for automatic refresh
|
|
50
50
|
# @return [Session] Session validation result
|
|
51
51
|
def validate_session(session_token, refresh_token: nil)
|
|
52
|
+
claims = nil
|
|
53
|
+
|
|
52
54
|
begin
|
|
53
55
|
claims = decode_and_verify_token(session_token)
|
|
54
|
-
return Session.from_claims(claims) if claims
|
|
55
56
|
rescue ExpiredTokenError
|
|
56
57
|
# Token has expired, attempt refresh
|
|
57
58
|
end
|
|
58
59
|
|
|
59
|
-
if
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return Session.from_claims(
|
|
65
|
-
new_claims || claims,
|
|
66
|
-
refreshed: true,
|
|
67
|
-
new_session_token: tokens[:session_token],
|
|
68
|
-
new_refresh_token: tokens[:refresh_token]
|
|
69
|
-
)
|
|
70
|
-
rescue Error
|
|
71
|
-
return Session.invalid
|
|
60
|
+
if claims
|
|
61
|
+
# Valid session - check if we should proactively refresh based on lifetime
|
|
62
|
+
if refresh_token && token_needs_refresh?(claims)
|
|
63
|
+
# If refresh fails, we still want to return the existing session as valid
|
|
64
|
+
# until it fully expires, so we don't disrupt active users
|
|
65
|
+
return attempt_refresh(refresh_token) || Session.from_claims(claims)
|
|
72
66
|
end
|
|
67
|
+
|
|
68
|
+
return Session.from_claims(claims)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
if refresh_token
|
|
72
|
+
return attempt_refresh(refresh_token) || Session.invalid
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
Session.invalid
|
|
@@ -101,6 +101,36 @@ module Aha
|
|
|
101
101
|
|
|
102
102
|
private
|
|
103
103
|
|
|
104
|
+
def attempt_refresh(refresh_token)
|
|
105
|
+
tokens = authenticate_with_refresh_token(refresh_token: refresh_token)
|
|
106
|
+
new_claims = decode_and_verify_token(tokens[:session_token])
|
|
107
|
+
Session.from_claims(
|
|
108
|
+
new_claims,
|
|
109
|
+
refreshed: true,
|
|
110
|
+
new_session_token: tokens[:session_token],
|
|
111
|
+
new_refresh_token: tokens[:refresh_token]
|
|
112
|
+
)
|
|
113
|
+
rescue Error => e
|
|
114
|
+
Rails.logger.error("Session refresh failed: #{e.message}")
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Determine if a token is approaching expiration and should be proactively refreshed
|
|
119
|
+
#
|
|
120
|
+
# @param claims [Hash] The decoded JWT claims
|
|
121
|
+
# @return [Boolean] true if the token should be refreshed
|
|
122
|
+
def token_needs_refresh?(claims)
|
|
123
|
+
iat = claims["iat"]
|
|
124
|
+
exp = claims["exp"]
|
|
125
|
+
return false unless iat && exp
|
|
126
|
+
|
|
127
|
+
lifetime = exp - iat
|
|
128
|
+
return false if lifetime <= 0
|
|
129
|
+
|
|
130
|
+
elapsed = Time.now.to_i - iat
|
|
131
|
+
elapsed.to_f / lifetime >= @configuration.refresh_lifetime_fraction
|
|
132
|
+
end
|
|
133
|
+
|
|
104
134
|
def decode_and_verify_token(token)
|
|
105
135
|
# First decode without verification to get the header
|
|
106
136
|
header = JWT.decode(token, nil, false).last
|
|
@@ -15,8 +15,9 @@ module Aha
|
|
|
15
15
|
# How long to cache JWKS keys (default: 1 hour)
|
|
16
16
|
attr_accessor :jwks_cache_ttl
|
|
17
17
|
|
|
18
|
-
#
|
|
19
|
-
|
|
18
|
+
# Fraction of token lifetime after which to attempt proactive refresh (default: 0.5)
|
|
19
|
+
# e.g. at 0.5, a 15-minute token triggers refresh after 7.5 minutes
|
|
20
|
+
attr_accessor :refresh_lifetime_fraction
|
|
20
21
|
|
|
21
22
|
# HTTP timeout in seconds (default: 30)
|
|
22
23
|
attr_accessor :timeout
|
|
@@ -32,7 +33,7 @@ module Aha
|
|
|
32
33
|
@api_key = ENV.fetch("AHA_CORE_API_KEY", nil)
|
|
33
34
|
@client_id = ENV.fetch("APPLICATION_ID", nil)
|
|
34
35
|
@jwks_cache_ttl = 3600 # 1 hour
|
|
35
|
-
@
|
|
36
|
+
@refresh_lifetime_fraction = 0.5
|
|
36
37
|
@timeout = 30
|
|
37
38
|
@enable_logging = false
|
|
38
39
|
@logger = Rails.logger
|
data/lib/aha/version.rb
CHANGED
data/lib/aha_builder_core.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: aha_builder_core
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.17
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Aha! Labs Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-02-
|
|
11
|
+
date: 2026-02-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -123,8 +123,6 @@ files:
|
|
|
123
123
|
- lib/aha/auth/token_cache.rb
|
|
124
124
|
- lib/aha/auth/user.rb
|
|
125
125
|
- lib/aha/auth/users_resource.rb
|
|
126
|
-
- lib/aha/mail.rb
|
|
127
|
-
- lib/aha/mail/delivery_method.rb
|
|
128
126
|
- lib/aha/user_guide.rb
|
|
129
127
|
- lib/aha/version.rb
|
|
130
128
|
- lib/aha_builder_core.rb
|
|
@@ -151,7 +149,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
151
149
|
requirements:
|
|
152
150
|
- - ">="
|
|
153
151
|
- !ruby/object:Gem::Version
|
|
154
|
-
version: 3.
|
|
152
|
+
version: 3.2.0
|
|
155
153
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
156
154
|
requirements:
|
|
157
155
|
- - ">="
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Aha
|
|
4
|
-
module Mail
|
|
5
|
-
class DeliveryMethod
|
|
6
|
-
attr_accessor :settings
|
|
7
|
-
|
|
8
|
-
def initialize(settings = {})
|
|
9
|
-
@settings = settings
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def deliver!(mail)
|
|
13
|
-
# Extract mail attributes
|
|
14
|
-
mail_params = {
|
|
15
|
-
from: format_addresses(mail.from).first,
|
|
16
|
-
to: format_addresses(mail.to),
|
|
17
|
-
cc: format_addresses(mail.cc),
|
|
18
|
-
bcc: format_addresses(mail.bcc),
|
|
19
|
-
reply_to: format_addresses(mail.reply_to),
|
|
20
|
-
subject: mail.subject,
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
# Determine content type and body
|
|
24
|
-
if mail.multipart?
|
|
25
|
-
# For multipart messages, prefer HTML over plain text
|
|
26
|
-
html_part = mail.html_part
|
|
27
|
-
text_part = mail.text_part
|
|
28
|
-
|
|
29
|
-
if html_part
|
|
30
|
-
mail_params[:body] = html_part.body.decoded
|
|
31
|
-
mail_params[:content_type] = "text/html"
|
|
32
|
-
elsif text_part
|
|
33
|
-
mail_params[:body] = text_part.body.decoded
|
|
34
|
-
mail_params[:content_type] = "text/plain"
|
|
35
|
-
else
|
|
36
|
-
mail_params[:body] = mail.body.decoded
|
|
37
|
-
mail_params[:content_type] = mail.content_type
|
|
38
|
-
end
|
|
39
|
-
else
|
|
40
|
-
mail_params[:body] = mail.body.decoded
|
|
41
|
-
mail_params[:content_type] = mail.content_type || "text/plain"
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# Send via the Aha::Mail API
|
|
45
|
-
Aha::Mail.send_mail(mail_params)
|
|
46
|
-
rescue StandardError => e
|
|
47
|
-
# ActionMailer expects delivery methods to raise exceptions on failure
|
|
48
|
-
raise "Failed to deliver mail via Aha::Mail: #{e.message}"
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
private
|
|
52
|
-
|
|
53
|
-
def format_addresses(addresses)
|
|
54
|
-
return [] if addresses.nil?
|
|
55
|
-
|
|
56
|
-
Array(addresses).map do |address|
|
|
57
|
-
if address.is_a?(::Mail::Address)
|
|
58
|
-
address.to_s
|
|
59
|
-
else
|
|
60
|
-
address
|
|
61
|
-
end
|
|
62
|
-
end.compact
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
data/lib/aha/mail.rb
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "mail/delivery_method"
|
|
4
|
-
|
|
5
|
-
module Aha
|
|
6
|
-
module Mail
|
|
7
|
-
class << self
|
|
8
|
-
def send_mail(mail_params)
|
|
9
|
-
client.send(:post, "/api/core/mail/send", mail: mail_params)
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
private
|
|
13
|
-
|
|
14
|
-
def client
|
|
15
|
-
Aha::Auth.send(:client)
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|