aha_builder_core 1.0.15 → 1.0.16

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ab96bb0d7db25dcab119a0fc590ddfdc0a60b2984ae041f42f7380f9da1a229
4
- data.tar.gz: 83b6752ca39af681cf7a617855b9cd3b89b2a83eef98c28c7e440b9018389bfb
3
+ metadata.gz: 040e466bb6704c03036a10c3b1242abe7180212e6c34319ff6377d090f1f5373
4
+ data.tar.gz: 8fae9ac629308b1f7ecf306f3f5fee5b362f181fab5dd2a2f75502e0ed22dccf
5
5
  SHA512:
6
- metadata.gz: da4a0a3dd5af21d498c34fab9c845928f017c8489c6cc857986fa70bc01fc318e440b1c555288afb872c8a29d46b6ed43dff0e8e8723f5d562635f598b7bcbff
7
- data.tar.gz: 8c27600959bfa94734744c8ef7b2f27fd22173da39a04eef001835261d695368b31cc1959f4496e5a481b5c792ff0d199884e6d265b7d98771ff30f725483b3a
6
+ metadata.gz: 16711e2de018b08fdedcb47a92d5e1a6a3ea3862d88d786c8ce609d76de9f620293760cbb74bb7cff68d243133ee0ad6041020c02e98ee7d6210d68ea234b7a3
7
+ data.tar.gz: 883be935bafe88f56091079f1b38cdcc3104b115d3f1d3e633bb78d501e9c0b9d7df9b3010416162343b7c943623eb124645bbf2282dc8336b5be2c691308534
data/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # Aha Builder Core Client
2
2
 
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.
3
+ Ruby client for Aha! Builder core which provides application services:
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.
4
8
 
5
9
  ## Installation
6
10
 
@@ -18,7 +22,7 @@ No configuration is necessary and no environment variables are necessary.
18
22
 
19
23
  The authentication UI is provided completely by the core system. During authentication the user is redirected to the login page, and will return (via HTTP redirect) to the `/callback` URL when authentication is complete. Your application must implement a callback action at `/callback` to receive the code. Any value passed in as `state` is returned verbatim to the callback.
20
24
 
21
- Protection against CSRF atacks is handled internally by the Aha::Auth library and there is no need to use a nonce in the state parameter.
25
+ Protection against CSRF attacks is handled internally by the Aha::Auth library and there is no need to use a nonce in the state parameter.
22
26
 
23
27
  ### Generate Login URL
24
28
 
@@ -88,7 +92,6 @@ local_user.update!(
88
92
  # Store tokens in session or database
89
93
  session[:session_token] = result[:session_token]
90
94
  session[:refresh_token] = result[:refresh_token]
91
- session[:user_id] = local_user.id
92
95
 
93
96
  # Redirect to application
94
97
  redirect_to root_path
@@ -202,3 +205,20 @@ Each page hash contains:
202
205
  - `position` - Sort order (integer)
203
206
  - `children_count` - Number of child pages
204
207
  - `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
+
@@ -52,7 +52,7 @@ module Aha
52
52
  begin
53
53
  claims = decode_and_verify_token(session_token)
54
54
  return Session.from_claims(claims) if claims
55
- rescue ExpiredTokenError
55
+ rescue ExpiredTokenError
56
56
  # Token has expired, attempt refresh
57
57
  end
58
58
 
@@ -73,7 +73,8 @@ module Aha
73
73
  end
74
74
 
75
75
  Session.invalid
76
- rescue InvalidTokenError, ExpiredTokenError
76
+ rescue InvalidTokenError, ExpiredTokenError => e
77
+ Rails.logger.info("Session token invalid or expired #{e.message}")
77
78
  Session.invalid
78
79
  end
79
80
 
@@ -82,9 +83,12 @@ module Aha
82
83
  # @param session_token [String] The session token to revoke
83
84
  # @return [Boolean] true if successful
84
85
  def logout(session_token:)
85
- get("/api/core/auth_ui/logout", headers: { "Authorization" => "Bearer #{session_token}" })
86
+ claims = decode_and_verify_token(session_token)
87
+ user_id = claims["sub"] # JWT subject is the user_id
88
+
89
+ post("/api/core/auth/users/#{user_id}/logout", {})
86
90
  true
87
- rescue ApiError
91
+ rescue InvalidTokenError, ExpiredTokenError, ApiError
88
92
  false
89
93
  end
90
94
 
@@ -106,7 +110,7 @@ module Aha
106
110
  public_key = @token_cache.get_key(kid) { fetch_jwks }
107
111
 
108
112
  unless public_key
109
- # Try refreshing the cache in case there's a new key
113
+ # Force refresh the cache in case there's a new key
110
114
  @token_cache.refresh! { fetch_jwks }
111
115
  public_key = @token_cache.get_key(kid) { fetch_jwks }
112
116
  raise InvalidTokenError, "Unknown key ID: #{kid}" unless public_key
@@ -115,7 +119,11 @@ module Aha
115
119
  # Verify the token
116
120
  options = {
117
121
  algorithm: ALGORITHM,
118
- verify_expiration: true
122
+ aud: @configuration.client_id.to_s,
123
+ verify_expiration: true,
124
+ verify_aud: true,
125
+ verify_iat: true,
126
+ required_claims: %w[sub sid aud iat exp],
119
127
  }
120
128
 
121
129
  decoded = JWT.decode(token, public_key, true, options)
@@ -157,6 +165,10 @@ module Aha
157
165
 
158
166
  def http_client
159
167
  @http_client ||= Faraday.new(url: @configuration.server_url) do |conn|
168
+ if @configuration.enable_logging && @configuration.logger
169
+ conn.response :logger, @configuration.logger, bodies: true
170
+ end
171
+
160
172
  conn.request :retry, max: 2, interval: 0.5, backoff_factor: 2
161
173
  conn.options.timeout = @configuration.timeout
162
174
 
@@ -21,6 +21,12 @@ module Aha
21
21
  # HTTP timeout in seconds (default: 30)
22
22
  attr_accessor :timeout
23
23
 
24
+ # Enable logging of HTTP requests and responses (default: false)
25
+ attr_accessor :enable_logging
26
+
27
+ # Logger instance for HTTP logging (default: Rails.logger)
28
+ attr_accessor :logger
29
+
24
30
  def initialize
25
31
  @server_url = ENV.fetch("AHA_CORE_SERVER_URL", "https://secure.aha.io/api/core")
26
32
  @api_key = ENV.fetch("AHA_CORE_API_KEY", nil)
@@ -28,6 +34,8 @@ module Aha
28
34
  @jwks_cache_ttl = 3600 # 1 hour
29
35
  @refresh_threshold = 120 # 2 minutes
30
36
  @timeout = 30
37
+ @enable_logging = false
38
+ @logger = Rails.logger
31
39
  end
32
40
 
33
41
  def validate!
@@ -34,11 +34,6 @@ module Aha
34
34
  end
35
35
  end
36
36
 
37
- # Check if the cache is stale
38
- def stale?
39
- @fetched_at.nil? || (Time.now.utc - @fetched_at) > @ttl
40
- end
41
-
42
37
  # Clear the cache
43
38
  def clear!
44
39
  @mutex.synchronize do
@@ -49,6 +44,10 @@ module Aha
49
44
 
50
45
  private
51
46
 
47
+ def stale?
48
+ @fetched_at.nil? || (Time.now.utc - @fetched_at) > @ttl
49
+ end
50
+
52
51
  def refresh_if_needed(&fetcher)
53
52
  fetch_keys(&fetcher) if stale?
54
53
  end
@@ -59,7 +58,10 @@ module Aha
59
58
 
60
59
  @keys = {}
61
60
  jwks["keys"].each do |key_data|
61
+ # Valdate the key is appropriate for our use.
62
62
  next unless key_data["kty"] == "RSA"
63
+ next unless key_data["use"] == "sig"
64
+ next unless key_data["alg"] == "RS256"
63
65
 
64
66
  kid = key_data["kid"]
65
67
  @keys[kid] = build_rsa_key(key_data)
@@ -11,9 +11,9 @@ module Aha
11
11
  # Create a new user
12
12
  #
13
13
  # @param email [String] User's email address
14
- # @param first_name [String] User's first name
15
- # @param last_name [String] User's last name
16
- # @param password [String] User's password
14
+ # @param first_name [String] User's first name (optional)
15
+ # @param last_name [String] User's last name (optional)
16
+ # @param password [String] User's password (optional)
17
17
  # @return [User] The created user
18
18
  def create(email:, first_name:, last_name:, password:)
19
19
  response = post(
data/lib/aha/auth.rb CHANGED
@@ -77,26 +77,13 @@ module Aha
77
77
  def authenticate_with_code(code:, cookies:)
78
78
  # Split the code and nonce if present
79
79
  actual_code, nonce = code.split(".", 2)
80
+
81
+ raise "CSRF verification failed: unable to verify nonce" unless nonce
80
82
 
81
- # Verify CSRF protection if nonce is present
82
- if nonce
83
- cookie_nonce = cookies[:auth_nonce]
84
-
85
- # Verify nonce matches
86
- if cookie_nonce.blank?
87
- raise "CSRF verification failed: nonce missing in cookie"
88
- end
89
-
90
- if cookie_nonce != nonce
91
- raise "CSRF verification failed: nonce mismatch"
92
- end
93
-
94
- # Clear the nonce from session after verification
95
- cookies.delete(:auth_nonce)
96
- else
97
- # If we fon't have a none, we can't verify CSRF.
98
- raise "CSRF verification failed: unable to verify nonce"
99
- end
83
+ cookie_nonce = cookies[:auth_nonce]
84
+ cookies.delete(:auth_nonce)
85
+ raise "CSRF verification failed: nonce missing in cookie" if cookie_nonce.blank?
86
+ raise "CSRF verification failed: nonce mismatch" if cookie_nonce != nonce
100
87
 
101
88
  client.authenticate_with_code(code: actual_code)
102
89
  end
@@ -0,0 +1,66 @@
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 ADDED
@@ -0,0 +1,19 @@
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
data/lib/aha/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aha
4
- VERSION = "1.0.15"
4
+ VERSION = "1.0.16"
5
5
  end
@@ -2,3 +2,4 @@
2
2
 
3
3
  require_relative "aha/auth"
4
4
  require_relative "aha/user_guide"
5
+ require_relative "aha/mail"
@@ -16,7 +16,7 @@ Example:
16
16
  And add routes:
17
17
  get "login" => "sessions#new"
18
18
  get "callback" => "sessions#callback"
19
- delete "logout" => "sessions#logout"
19
+ get "logout" => "sessions#logout"
20
20
 
21
21
  You can add custom attributes to the User model:
22
22
  bin/rails generate aha_builder_core:auth company:string role:string
@@ -23,7 +23,7 @@ module Authentication
23
23
  session[:refresh_token] = session_result.new_refresh_token
24
24
  end
25
25
 
26
- @current_user = User.find_by(id: session[:user_id])
26
+ @current_user = User.find_by(id: session_result.user_id)
27
27
  Current.user = @current_user
28
28
  else
29
29
  clear_session
@@ -50,9 +50,7 @@ module Authentication
50
50
  end
51
51
 
52
52
  def clear_session
53
- session.delete(:session_token)
54
- session.delete(:refresh_token)
55
- session.delete(:user_id)
53
+ reset_session
56
54
  @current_user = nil
57
55
  end
58
56
  end
@@ -4,7 +4,7 @@ class SessionsController < ApplicationController
4
4
 
5
5
  def new
6
6
  redirect_to Aha::Auth.login_url(
7
- state: { return_to: params[:return_to] || root_path }.to_json,
7
+ state: { return_to: relative_url(params[:return_to] || root_path) }.to_json,
8
8
  cookies:
9
9
  ), allow_other_host: true
10
10
  end
@@ -25,10 +25,9 @@ class SessionsController < ApplicationController
25
25
 
26
26
  session[:session_token] = result[:session_token]
27
27
  session[:refresh_token] = result[:refresh_token]
28
- session[:user_id] = user.id
29
28
 
30
29
  state = JSON.parse(params[:state]) rescue {}
31
- redirect_to state["return_to"] || root_path
30
+ redirect_to relative_url(state["return_to"] || root_path)
32
31
  else
33
32
  redirect_to login_path, alert: "Authentication failed. Please try again."
34
33
  end
@@ -49,4 +48,13 @@ class SessionsController < ApplicationController
49
48
  clear_session
50
49
  redirect_to root_path, notice: "Successfully signed out."
51
50
  end
51
+
52
+ private
53
+
54
+ def relative_url(url)
55
+ uri = URI.parse(url)
56
+ [[uri.path, uri.query].compact.join("?"), uri.fragment].compact.join("#")
57
+ rescue StandardError => e
58
+ nil
59
+ end
52
60
  end
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.15
4
+ version: 1.0.16
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-01-16 00:00:00.000000000 Z
11
+ date: 2026-02-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -123,6 +123,8 @@ 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
126
128
  - lib/aha/user_guide.rb
127
129
  - lib/aha/version.rb
128
130
  - lib/aha_builder_core.rb
@@ -149,7 +151,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
149
151
  requirements:
150
152
  - - ">="
151
153
  - !ruby/object:Gem::Version
152
- version: 3.2.0
154
+ version: 3.3.0
153
155
  required_rubygems_version: !ruby/object:Gem::Requirement
154
156
  requirements:
155
157
  - - ">="