aha_builder_core 1.0.4 → 1.0.6

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: 67c88ef564275000006548bd40149fb9d8c9c6a28d7b9faf430a8dd6b338147c
4
- data.tar.gz: 10401cac65cfa9f711640911038c12491acaf73b160f340851becdce92edfbb2
3
+ metadata.gz: 9b8f4ea89a8c48e32987674fde51690fb12724503ca2284af49bb462c3daf70e
4
+ data.tar.gz: 0ce63753cead6d93aff650414fad65863903326409508c243df9c24bfc7f912e
5
5
  SHA512:
6
- metadata.gz: f8c526217a495e3e9ec239c4d169eb0848f9bcc6f876ba92f02f114a420f0220313e7960ef1a56b1271c0d270f2906480a5524510e988ecda939eca8491e3f10
7
- data.tar.gz: 1eb5ccd35517e18df03ea87a2c93b4eadea3152d79023f440ee19c9930f600543085c6c6ab2e45fccbfb3ee0df1a1bd14b0223ca11315e8826e2d655f5df5880
6
+ metadata.gz: 795df34548c6321d7bd28f514071fbbb3b797ad5aaabb5f52b86ca8fc1b186741e4bdd87a9d91efbc7151c38e81b8d81340bcff1bb6de42e10832fe47f1bda3b
7
+ data.tar.gz: 417d5fe39792591decf0e02be88c2095da37cf972ae94dbf5c683e7081cd2252bfc51398d9d3e1737681b280caa85b89ba9acf3dd5905afb9133462bb66bc5f6
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Aha
4
4
  module Auth
5
- VERSION = "1.0.4"
5
+ VERSION = "1.0.6"
6
6
  end
7
7
  end
data/lib/aha/auth.rb CHANGED
@@ -42,15 +42,15 @@ module Aha
42
42
 
43
43
  # Generate login URL for redirecting users to the auth server
44
44
  #
45
- # @param session [Hash] Session hash to store the nonce for CSRF verification
45
+ # @param cookies [Hash] Cookies hash to store the nonce for CSRF verification
46
46
  # @param state [String] Optional state parameter to pass through the auth flow
47
47
  # @return [String] The login URL
48
- def login_url(session:, state: nil)
48
+ def login_url(cookies:, state: nil)
49
49
  # Generate a nonce for CSRF protection
50
50
  nonce = SecureRandom.hex(10)
51
51
 
52
52
  # Store nonce in the client's session if provided
53
- session[:auth_nonce] = nonce if session
53
+ cookies[:auth_nonce] = nonce
54
54
 
55
55
  # Encode the state with nonce
56
56
  state_data = {
@@ -72,23 +72,27 @@ module Aha
72
72
  # Exchange an authorization code for tokens
73
73
  #
74
74
  # @param code [String] The authorization code from the callback (may include nonce)
75
- # @param session [Hash] Session hash containing the nonce for CSRF verification
75
+ # @param cookies [Hash] Cookies hash containing the nonce for CSRF verification
76
76
  # @return [Hash] Token response with :session_token, :refresh_token, :expires_at, :user
77
- def authenticate_with_code(code:, session:)
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
80
 
81
81
  # Verify CSRF protection if nonce is present
82
82
  if nonce
83
- session_nonce = session[:auth_nonce]
83
+ cookie_nonce = cookies[:auth_nonce]
84
84
 
85
85
  # Verify nonce matches
86
- if session_nonce.blank? || session_nonce != nonce
86
+ if cookie_nonce.blank?
87
+ raise "CSRF verification failed: nonce missing in session"
88
+ end
89
+
90
+ if cookie_nonce != nonce
87
91
  raise "CSRF verification failed: nonce mismatch"
88
92
  end
89
93
 
90
94
  # Clear the nonce from session after verification
91
- session.delete(:auth_nonce)
95
+ cookies.delete(:auth_nonce)
92
96
  else
93
97
  # If we fon't have a none, we can't verify CSRF.
94
98
  raise "CSRF verification failed: unable to verify nonce"
@@ -0,0 +1,27 @@
1
+ Description:
2
+ Generates authentication scaffolding for Aha Builder Core integration.
3
+ Creates User model, migration, SessionsController, Authentication concern,
4
+ Current model, and routes.
5
+
6
+ Example:
7
+ bin/rails generate aha_builder_core:auth
8
+
9
+ This will create:
10
+ app/models/user.rb
11
+ app/models/current.rb
12
+ app/controllers/sessions_controller.rb
13
+ app/controllers/concerns/authentication.rb
14
+ db/migrate/XXXXXXXXXXXXXX_create_users.rb
15
+
16
+ And add routes:
17
+ get "login" => "sessions#new"
18
+ get "callback" => "sessions#callback"
19
+ delete "logout" => "sessions#logout"
20
+
21
+ You can add custom attributes to the User model:
22
+ bin/rails generate aha_builder_core:auth company:string role:string
23
+
24
+ This adds company and role columns to the users table migration.
25
+
26
+ Options:
27
+ --skip-migration Skip generating the database migration
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module AhaBuilderCore
7
+ class AuthGenerator < Rails::Generators::Base
8
+ include ActiveRecord::Generators::Migration
9
+
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ argument :attributes, type: :array, default: [], banner: "field[:type] field[:type]"
13
+
14
+ class_option :skip_migration, type: :boolean, default: false, desc: "Skip migration generation"
15
+
16
+ def generate_migration_file
17
+ return if options[:skip_migration]
18
+
19
+ migration_template "migration.rb.tt", File.join(db_migrate_path, "create_users.rb")
20
+ end
21
+
22
+ def create_user_model
23
+ template "user.rb.tt", "app/models/user.rb"
24
+ end
25
+
26
+ def create_current_model
27
+ template "current.rb.tt", "app/models/current.rb"
28
+ end
29
+
30
+ def create_sessions_controller
31
+ template "sessions_controller.rb.tt", "app/controllers/sessions_controller.rb"
32
+ end
33
+
34
+ def create_authentication_concern
35
+ template "authentication.rb.tt", "app/controllers/concerns/authentication.rb"
36
+ end
37
+
38
+ def add_routes
39
+ route <<~RUBY
40
+ get "login", to: "sessions#new", as: :new_session
41
+ get "callback", to: "sessions#callback", as: :session_callback
42
+ delete "logout", to: "sessions#logout", as: :logout
43
+ RUBY
44
+ end
45
+
46
+ def add_inertia_auth_share
47
+ return unless File.exist?("app/controllers/inertia_controller.rb")
48
+
49
+ inject_into_file "app/controllers/inertia_controller.rb",
50
+ after: "inertia_share flash: -> { flash.to_hash }\n" do
51
+ <<-RUBY
52
+ inertia_share auth: -> {
53
+ {
54
+ user: current_user&.as_json(only: %i[id email first_name last_name])
55
+ }
56
+ }
57
+ RUBY
58
+ end
59
+ end
60
+
61
+ def add_authentication_to_application_controller
62
+ inject_into_file "app/controllers/application_controller.rb",
63
+ after: "class ApplicationController < ActionController::Base\n" do
64
+ " include Authentication\n"
65
+ end
66
+ end
67
+
68
+ def add_typescript_types
69
+ types_file = "app/frontend/types/index.ts"
70
+ return unless File.exist?(types_file)
71
+
72
+ gsub_file types_file,
73
+ /export interface Flash \{\n alert\?: string\n notice\?: string\n\}\n\nexport interface SharedData \{\n flash: Flash\n \[key: string\]: unknown\n\}/,
74
+ <<~TYPESCRIPT.chomp
75
+ export interface Flash {
76
+ alert?: string
77
+ notice?: string
78
+ }
79
+
80
+ export interface AuthUser {
81
+ id: number
82
+ email: string
83
+ first_name: string | null
84
+ last_name: string | null
85
+ }
86
+
87
+ export interface SharedData {
88
+ flash: Flash
89
+ auth: {
90
+ user: AuthUser | null
91
+ }
92
+ [key: string]: unknown
93
+ }
94
+ TYPESCRIPT
95
+ end
96
+
97
+ def generate_js_routes
98
+ say "\nRegenerating js-routes file...", :green
99
+ rails_command "js:routes"
100
+ end
101
+
102
+ def display_instructions
103
+ say "\nAha Auth setup complete!", :green
104
+ end
105
+
106
+ private
107
+
108
+ def auth_attributes
109
+ %w[
110
+ auth_identifier:string
111
+ email:string
112
+ first_name:string
113
+ last_name:string
114
+ email_verified:boolean
115
+ ]
116
+ end
117
+
118
+ def all_attributes
119
+ @all_attributes ||= (auth_attributes + attributes.map(&:to_s)).map do |attr|
120
+ Rails::Generators::GeneratedAttribute.parse(attr)
121
+ end
122
+ end
123
+
124
+ def custom_attributes
125
+ @custom_attributes ||= attributes.map do |attr|
126
+ Rails::Generators::GeneratedAttribute.parse(attr.to_s)
127
+ end
128
+ end
129
+
130
+ def migration_class_name
131
+ "CreateUsers"
132
+ end
133
+
134
+ def table_name
135
+ "users"
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,57 @@
1
+ module Authentication
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_action :authenticate
6
+ helper_method :current_user, :logged_in?
7
+ end
8
+
9
+ private
10
+
11
+ def authenticate
12
+ return unless session[:session_token].present?
13
+
14
+ session_result = Aha::Auth.validate_session(
15
+ session[:session_token],
16
+ refresh_token: session[:refresh_token]
17
+ )
18
+
19
+ if session_result.valid?
20
+ if session_result.refreshed?
21
+ session[:session_token] = session_result.new_session_token
22
+ session[:refresh_token] = session_result.new_refresh_token
23
+ end
24
+
25
+ @current_user = User.find_by(id: session[:user_id])
26
+ Current.user = @current_user
27
+ else
28
+ clear_session
29
+ redirect_to login_path, alert: "Your session has expired. Please log in again."
30
+ end
31
+ rescue Aha::Auth::ApiError => e
32
+ Rails.logger.error "Session validation failed: #{e.message}"
33
+ clear_session
34
+ redirect_to login_path, alert: "Authentication error. Please log in again."
35
+ end
36
+
37
+ def current_user
38
+ @current_user ||= Current.user
39
+ end
40
+
41
+ def logged_in?
42
+ current_user.present?
43
+ end
44
+
45
+ def require_authentication
46
+ return if logged_in?
47
+
48
+ redirect_to login_path(return_to: request.fullpath), alert: "You must be logged in to access this page."
49
+ end
50
+
51
+ def clear_session
52
+ session.delete(:session_token)
53
+ session.delete(:refresh_token)
54
+ session.delete(:user_id)
55
+ @current_user = nil
56
+ end
57
+ end
@@ -0,0 +1,3 @@
1
+ class Current < ActiveSupport::CurrentAttributes
2
+ attribute :user
3
+ end
@@ -0,0 +1,21 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ create_table :users do |t|
4
+ t.string :auth_identifier, null: false
5
+ t.string :email, null: false
6
+ t.string :first_name
7
+ t.string :last_name
8
+ t.boolean :email_verified, default: false, null: false
9
+ <% custom_attributes.each do |attribute| -%>
10
+ <% unless attribute.virtual? -%>
11
+ t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
12
+ <% end -%>
13
+ <% end -%>
14
+
15
+ t.timestamps
16
+ end
17
+
18
+ add_index :users, :auth_identifier, unique: true
19
+ add_index :users, :email, unique: true
20
+ end
21
+ end
@@ -0,0 +1,50 @@
1
+ class SessionsController < ApplicationController
2
+ skip_before_action :authenticate, only: [ :new, :callback ]
3
+
4
+ def new
5
+ redirect_to Aha::Auth.login_url(
6
+ state: { return_to: params[:return_to] || root_path }.to_json,
7
+ cookies:
8
+ ), allow_other_host: true
9
+ end
10
+
11
+ def callback
12
+ if params[:code].present?
13
+ result = Aha::Auth.authenticate_with_code(code: params[:code], cookies:)
14
+
15
+ user = User.find_or_initialize_by(auth_identifier: result[:user]["id"])
16
+
17
+ user.update!(
18
+ email: result[:user]["email"],
19
+ first_name: result[:user]["first_name"],
20
+ last_name: result[:user]["last_name"],
21
+ email_verified: result[:user]["email_verified"]
22
+ )
23
+
24
+ session[:session_token] = result[:session_token]
25
+ session[:refresh_token] = result[:refresh_token]
26
+ session[:user_id] = user.id
27
+
28
+ state = JSON.parse(params[:state]) rescue {}
29
+ redirect_to state["return_to"] || root_path
30
+ else
31
+ redirect_to new_session_path, alert: "Authentication failed. Please try again."
32
+ end
33
+ rescue Aha::Auth::ApiError => e
34
+ Rails.logger.error "Authentication failed: #{e.message}"
35
+ redirect_to new_session_path, alert: "Authentication failed. Please try again."
36
+ end
37
+
38
+ def logout
39
+ if session[:session_token]
40
+ begin
41
+ Aha::Auth.logout(session_token: session[:session_token])
42
+ rescue => e
43
+ Rails.logger.error "Logout error: #{e.message}"
44
+ end
45
+ end
46
+
47
+ clear_session
48
+ redirect_to root_path, notice: "Successfully signed out."
49
+ end
50
+ end
@@ -0,0 +1,8 @@
1
+ class User < ApplicationRecord
2
+ validates :auth_identifier, presence: true, uniqueness: true
3
+ validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
4
+
5
+ def name
6
+ "#{first_name} #{last_name}".strip.presence || email
7
+ end
8
+ end
@@ -0,0 +1,19 @@
1
+ Description:
2
+ Configures Active Storage to use Aha blob storage service.
3
+
4
+ This generator sets up S3-compatible blob storage for file uploads
5
+ using environment variables provided by the container orchestrator.
6
+
7
+ Example:
8
+ bin/rails generate aha_builder_core:blob_storage
9
+
10
+ This will:
11
+ - Run active_storage:install to create required migrations
12
+ - Add aws-sdk-s3 gem to Gemfile
13
+ - Update config/storage.yml with blob storage configuration
14
+ - Set config.active_storage.service = :blob in development.rb
15
+ - Set config.active_storage.service = :blob in production.rb
16
+
17
+ Prerequisites:
18
+ The container must have blob storage attached via the SetupBlobStorageTool
19
+ which sets the required BLOB_UPLOADS_S3_* environment variables.
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module AhaBuilderCore
6
+ class BlobStorageGenerator < Rails::Generators::Base
7
+ source_root File.expand_path("templates", __dir__)
8
+
9
+ def install_active_storage
10
+ run "bin/rails active_storage:install"
11
+ end
12
+
13
+ def add_aws_sdk_gem
14
+ gem "aws-sdk-s3", require: false unless gem_exists?("aws-sdk-s3")
15
+ end
16
+
17
+ def update_storage_yml
18
+ template "storage.yml.tt", "config/storage.yml", force: true
19
+ end
20
+
21
+ def update_development_environment
22
+ gsub_file "config/environments/development.rb",
23
+ /config\.active_storage\.service\s*=\s*:\w+/,
24
+ "config.active_storage.service = :blob"
25
+ end
26
+
27
+ def update_production_environment
28
+ gsub_file "config/environments/production.rb",
29
+ /config\.active_storage\.service\s*=\s*:\w+/,
30
+ "config.active_storage.service = :blob"
31
+ end
32
+
33
+ def display_instructions
34
+ say "\nBlob storage configured!", :green
35
+ end
36
+
37
+ private
38
+
39
+ def gem_exists?(gem_name)
40
+ File.read("Gemfile").include?(gem_name)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,16 @@
1
+ test:
2
+ service: Disk
3
+ root: <%%= Rails.root.join("tmp/storage") %>
4
+
5
+ local:
6
+ service: Disk
7
+ root: <%%= Rails.root.join("storage") %>
8
+
9
+ blob:
10
+ service: S3
11
+ endpoint: <%%= ENV.fetch('BLOB_UPLOADS_S3_ENDPOINT') %>
12
+ access_key_id: <%%= ENV.fetch('BLOB_UPLOADS_S3_ACCESS_KEY_ID') %>
13
+ secret_access_key: <%%= ENV.fetch('BLOB_UPLOADS_S3_SECRET_ACCESS_KEY') %>
14
+ region: <%%= ENV.fetch('BLOB_UPLOADS_S3_REGION', 'us-east-1') %>
15
+ bucket: <%%= ENV.fetch('BLOB_UPLOADS_S3_BUCKET') %>
16
+ force_path_style: true
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aha_builder_core
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.4
4
+ version: 1.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aha! Labs Inc.
@@ -125,6 +125,16 @@ files:
125
125
  - lib/aha/auth/users_resource.rb
126
126
  - lib/aha/auth/version.rb
127
127
  - lib/aha_builder_core.rb
128
+ - lib/generators/aha_builder_core/auth/USAGE
129
+ - lib/generators/aha_builder_core/auth/auth_generator.rb
130
+ - lib/generators/aha_builder_core/auth/templates/authentication.rb.tt
131
+ - lib/generators/aha_builder_core/auth/templates/current.rb.tt
132
+ - lib/generators/aha_builder_core/auth/templates/migration.rb.tt
133
+ - lib/generators/aha_builder_core/auth/templates/sessions_controller.rb.tt
134
+ - lib/generators/aha_builder_core/auth/templates/user.rb.tt
135
+ - lib/generators/aha_builder_core/blob_storage/USAGE
136
+ - lib/generators/aha_builder_core/blob_storage/blob_storage_generator.rb
137
+ - lib/generators/aha_builder_core/blob_storage/templates/storage.yml.tt
128
138
  homepage: https://www.aha.io
129
139
  licenses:
130
140
  - MIT