aven 0.0.1 → 0.0.2

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -1
  3. data/app/components/aven/views/oauth/error/component.html.erb +44 -0
  4. data/app/components/aven/views/oauth/error/component.rb +30 -0
  5. data/app/components/aven/views/static/index/component.html.erb +4 -4
  6. data/app/components/aven/views/static/index/component.rb +11 -0
  7. data/app/controllers/aven/admin/base.rb +4 -4
  8. data/app/controllers/aven/application_controller.rb +22 -0
  9. data/app/controllers/aven/auth_controller.rb +6 -58
  10. data/app/controllers/aven/oauth/auth0_controller.rb +84 -0
  11. data/app/controllers/aven/oauth/base_controller.rb +183 -0
  12. data/app/controllers/aven/oauth/documentation/auth0.md +387 -0
  13. data/app/controllers/aven/oauth/documentation/entra_id.md +608 -0
  14. data/app/controllers/aven/oauth/documentation/github.md +329 -0
  15. data/app/controllers/aven/oauth/documentation/google.md +253 -0
  16. data/app/controllers/aven/oauth/entra_id_controller.rb +92 -0
  17. data/app/controllers/aven/oauth/github_controller.rb +91 -0
  18. data/app/controllers/aven/oauth/google_controller.rb +64 -0
  19. data/app/controllers/aven/workspaces_controller.rb +20 -0
  20. data/app/controllers/concerns/aven/authentication.rb +49 -0
  21. data/app/controllers/concerns/aven/controller_helpers.rb +38 -0
  22. data/app/helpers/aven/application_helper.rb +2 -6
  23. data/app/models/aven/app_record.rb +1 -1
  24. data/app/models/aven/app_record_schema.rb +0 -1
  25. data/app/models/aven/log.rb +0 -1
  26. data/app/models/aven/loggable.rb +2 -3
  27. data/app/models/aven/user.rb +0 -23
  28. data/app/models/aven/workspace.rb +49 -5
  29. data/app/models/aven/workspace_role.rb +0 -1
  30. data/app/models/aven/workspace_user.rb +0 -1
  31. data/app/models/aven/workspace_user_role.rb +0 -1
  32. data/config/routes.rb +22 -7
  33. data/db/migrate/{20251003090752_create_aven_users.rb → 20200101000001_create_aven_users.rb} +1 -1
  34. data/db/migrate/{20251004182010_create_aven_workspace_users.rb → 20200101000003_create_aven_workspace_users.rb} +1 -1
  35. data/db/migrate/{20251004182020_create_aven_workspace_roles.rb → 20200101000004_create_aven_workspace_roles.rb} +1 -1
  36. data/db/migrate/{20251004182030_create_aven_workspace_user_roles.rb → 20200101000005_create_aven_workspace_user_roles.rb} +1 -1
  37. data/db/migrate/{20251004190000_create_aven_logs.rb → 20200101000006_create_aven_logs.rb} +2 -3
  38. data/db/migrate/{20251004190100_create_aven_app_record_schemas.rb → 20200101000007_create_aven_app_record_schemas.rb} +0 -1
  39. data/db/migrate/{20251004190110_create_aven_app_records.rb → 20200101000008_create_aven_app_records.rb} +0 -1
  40. data/lib/aven/configuration.rb +26 -10
  41. data/lib/aven/engine.rb +15 -16
  42. data/lib/aven/model/tenant_model.rb +91 -0
  43. data/lib/aven/model.rb +6 -0
  44. data/lib/aven/version.rb +1 -1
  45. metadata +42 -69
  46. data/config/initializers/devise.rb +0 -43
  47. /data/db/migrate/{20251004182000_create_aven_workspaces.rb → 20200101000002_create_aven_workspaces.rb} +0 -0
  48. /data/lib/tasks/{sqema_tasks.rake → aven_tasks.rake} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 787e11b1022af941769fa87fa5ba94984d46cb1dad8b0920ff780ece60f2b68d
4
- data.tar.gz: f6485456b219a405fc48d7ae645098fcda745877a7efd93a6a61522e5383b065
3
+ metadata.gz: a02e3ced6d96476bc2d2237a080cd459c7787295f0838558613b8c56c65417d2
4
+ data.tar.gz: da6fef70eddb6b4b3c9b22d9ef9dedc3553457a6745e4e8839f45352f5685c47
5
5
  SHA512:
6
- metadata.gz: a5e5a300447b6cf2768acd41bf5c16ba3f56400a8da4e3a491e8263e7127e2b1d2b3346c033281e2500671d4c9bf6e86f9286799478afa27b73ca66bee2d886e
7
- data.tar.gz: ba97a554b0c026cb4fb3af9ae5c6b5470746f47cf3d8c74788aa2f8dcedb477b08dc9a766c8cd8c425718c0b934339f20ecee788e732f8d7b04499be5b6b50e8
6
+ metadata.gz: 1024472cdaf1a181500949d2a40ac7d997299411f53f9614826f501c43e383a0a1f288c03bf74e508180074eaec9b89a9a451409900fb24e972033f6496799cf
7
+ data.tar.gz: c94494371d0a5f62d3a81bcfc8c0b31026189e7d2df8a04675dacebd3724def86e137172691ef1c905b313f14d2e3f5556edc428e7a1b4f794824f338eeedb96
data/Rakefile CHANGED
@@ -1,6 +1,6 @@
1
1
  require "bundler/setup"
2
2
 
3
- APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
4
  load "rails/tasks/engine.rake"
5
5
 
6
6
  load "rails/tasks/statistics.rake"
@@ -0,0 +1,44 @@
1
+ <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
2
+ <div class="max-w-md w-full space-y-8">
3
+ <div>
4
+ <h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
5
+ Authentication Failed
6
+ </h2>
7
+ <div class="mt-4 bg-red-50 border border-red-200 rounded-md p-4">
8
+ <div class="flex">
9
+ <div class="flex-shrink-0">
10
+ <svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
11
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
12
+ </svg>
13
+ </div>
14
+ <div class="ml-3">
15
+ <h3 class="text-sm font-medium text-red-800">
16
+ <%= error_message %>
17
+ </h3>
18
+ <% if error_class.present? %>
19
+ <p class="mt-2 text-xs text-red-700">
20
+ Error type: <%= error_class %>
21
+ </p>
22
+ <% end %>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ </div>
27
+
28
+ <div class="space-y-4">
29
+ <% provider_links.each do |provider| %>
30
+ <%= link_to(
31
+ "Try again with #{provider.to_s.capitalize}",
32
+ oauth_path_for(provider),
33
+ class: "group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
34
+ ) %>
35
+ <% end %>
36
+
37
+ <%= link_to(
38
+ "Back to home",
39
+ home_path,
40
+ class: "group relative w-full flex justify-center py-2 px-4 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
41
+ ) %>
42
+ </div>
43
+ </div>
44
+ </div>
@@ -0,0 +1,30 @@
1
+ module Aven::Views::Oauth::Error
2
+ class Component < Aven::ApplicationViewComponent
3
+ option :error_message
4
+ option :error_class, optional: true
5
+ option :current_user, optional: true
6
+
7
+ def provider_links
8
+ Aven.configuration.oauth_providers.keys
9
+ end
10
+
11
+ def oauth_path_for(provider)
12
+ case provider.to_sym
13
+ when :github
14
+ Aven::Engine.routes.url_helpers.oauth_github_path
15
+ when :google
16
+ Aven::Engine.routes.url_helpers.oauth_google_path
17
+ else
18
+ "#"
19
+ end
20
+ end
21
+
22
+ def home_path
23
+ if helpers.respond_to?(:main_app) && helpers.main_app.respond_to?(:root_path)
24
+ helpers.main_app.root_path
25
+ else
26
+ Aven::Engine.routes.url_helpers.root_path
27
+ end
28
+ end
29
+ end
30
+ end
@@ -4,13 +4,13 @@
4
4
 
5
5
  <% if current_user %>
6
6
  <div><%= current_user.id %></div>
7
- <%= link_to("Logout", helpers.logout_path) %>
7
+ <%= link_to("Logout", Aven::Engine.routes.url_helpers.logout_path) %>
8
8
  <% else %>
9
- <% Aven.configuration.auth.providers.each do |provider_config| %>
9
+ <% Aven.configuration.oauth_providers.each do |provider, config| %>
10
10
  <%=
11
11
  link_to(
12
- "Login with #{provider_config[:provider]}",
13
- helpers.authenticate_path(provider: provider_config[:provider])
12
+ "Login with #{provider.to_s.capitalize}",
13
+ oauth_path_for(provider)
14
14
  )
15
15
  %>
16
16
  <% end %>
@@ -1,5 +1,16 @@
1
1
  module Aven::Views::Static::Index
2
2
  class Component < Aven::ApplicationViewComponent
3
3
  option(:current_user, optional: true)
4
+
5
+ def oauth_path_for(provider)
6
+ case provider.to_sym
7
+ when :github
8
+ Aven::Engine.routes.url_helpers.oauth_github_path
9
+ when :google
10
+ Aven::Engine.routes.url_helpers.oauth_google_path
11
+ else
12
+ "#"
13
+ end
14
+ end
4
15
  end
5
16
  end
@@ -6,11 +6,11 @@ module Aven
6
6
 
7
7
  private
8
8
 
9
- def authenticate_admin!
10
- unless current_user&.admin
11
- redirect_to(root_path)
9
+ def authenticate_admin!
10
+ unless current_user&.admin
11
+ redirect_to(root_path)
12
+ end
12
13
  end
13
- end
14
14
  end
15
15
  end
16
16
  end
@@ -1,5 +1,27 @@
1
1
  module Aven
2
2
  class ApplicationController < ActionController::Base
3
3
  include Aven::ApplicationHelper
4
+
5
+ helper_method :current_workspace
6
+
7
+ # Get the current workspace from session
8
+ def current_workspace
9
+ return @current_workspace if defined?(@current_workspace)
10
+
11
+ @current_workspace = if session[:workspace_id].present? && current_user
12
+ current_user.workspaces.find_by(id: session[:workspace_id])
13
+ elsif current_user
14
+ # Auto-select first workspace if none selected
15
+ workspace = current_user.workspaces.first
16
+ session[:workspace_id] = workspace&.id
17
+ workspace
18
+ end
19
+ end
20
+
21
+ # Set the current workspace
22
+ def current_workspace=(workspace)
23
+ @current_workspace = workspace
24
+ session[:workspace_id] = workspace&.id
25
+ end
4
26
  end
5
27
  end
@@ -1,64 +1,12 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Aven
4
2
  class AuthController < ApplicationController
5
- def authenticate
6
- provider = params[:provider].to_s
7
-
8
- raise(StandardError, "invalid provider") unless configured_providers.include?(provider)
9
-
10
- redirect_post(
11
- send("user_#{provider}_omniauth_authorize_path"),
12
- params: { authenticity_token: form_authenticity_token }
13
- )
14
- end
15
-
16
- def action_missing(action_name)
17
- if configured_providers.include?(action_name.to_s)
18
- handle_omniauth(action_name.to_s)
19
- else
20
- raise AbstractController::ActionNotFound, "The action '#{action_name}' could not be found for #{self.class.name}"
21
- end
22
- end
23
-
24
- def passthru
25
- logout if request.method == "GET"
26
- end
27
-
28
- def failure
29
- logout if request.method == "GET"
30
- end
31
-
32
3
  def logout
33
- sign_out(current_user) if current_user
34
- reset_session
35
- redirect_to after_sign_out_path_for(nil)
36
- end
37
-
38
- private
39
-
40
- def configured_providers
41
- @configured_providers ||= Aven.configuration.auth.providers.map { |p| p[:provider].to_s }
42
- end
43
-
44
- def handle_omniauth(kind)
45
- auth_tenant = request.host # or however you determine tenant
46
- user = Aven::User.create_from_omniauth!(request.env, auth_tenant)
47
-
48
- if user.persisted?
49
- sign_in_and_redirect user, event: :authentication
50
- else
51
- session["devise.auth"] = request.env["omniauth.auth"].except(:extra)
52
- redirect_to new_user_registration_url
4
+ sign_out
5
+ begin
6
+ redirect_to(main_app.root_path, notice: "You have been signed out successfully.")
7
+ rescue NoMethodError
8
+ redirect_to(root_path, notice: "You have been signed out successfully.")
53
9
  end
54
10
  end
55
-
56
- def after_sign_in_path_for(resource)
57
- stored_location_for(resource) || Aven.configuration.authenticated_root_path || root_path
58
- end
59
-
60
- def after_sign_out_path_for(resource_or_scope)
61
- Aven.configuration.authenticated_root_path || root_path
62
- end
63
11
  end
64
- end
12
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+
6
+ module Aven
7
+ module Oauth
8
+ class Auth0Controller < BaseController
9
+ # Auth0 uses your domain (e.g., your-tenant.auth0.com or your-tenant.us.auth0.com)
10
+ # These URLs will be constructed dynamically based on the configured domain
11
+ DEFAULT_SCOPE = "openid email profile"
12
+
13
+ protected
14
+
15
+ def authorization_url(state)
16
+ params = {
17
+ client_id: oauth_config[:client_id],
18
+ redirect_uri: callback_url,
19
+ response_type: "code",
20
+ scope: oauth_config[:scope] || DEFAULT_SCOPE,
21
+ state:
22
+ }
23
+
24
+ # Optionally add audience parameter if specified (for API access)
25
+ params[:audience] = oauth_config[:audience] if oauth_config[:audience].present?
26
+
27
+ "#{auth0_authorization_url}?#{params.to_query}"
28
+ end
29
+
30
+ def exchange_code_for_token(code)
31
+ params = {
32
+ grant_type: "authorization_code",
33
+ client_id: oauth_config[:client_id],
34
+ client_secret: oauth_config[:client_secret],
35
+ code:,
36
+ redirect_uri: callback_url
37
+ }
38
+
39
+ oauth_request(URI(auth0_token_url), params)
40
+ end
41
+
42
+ def fetch_user_info(access_token)
43
+ response = oauth_get_request(URI(auth0_userinfo_url), access_token)
44
+
45
+ {
46
+ id: response[:sub],
47
+ email: response[:email],
48
+ name: response[:name] || response[:nickname],
49
+ picture: response[:picture]
50
+ }
51
+ end
52
+
53
+ private
54
+
55
+ def callback_url
56
+ aven.oauth_auth0_callback_url(host: request.host, protocol: request.protocol)
57
+ end
58
+
59
+ def oauth_config
60
+ @oauth_config ||= Aven.configuration.oauth_providers[:auth0] || raise("Auth0 OAuth not configured")
61
+ end
62
+
63
+ def auth0_domain
64
+ @auth0_domain ||= oauth_config[:domain] || raise("Auth0 domain not configured")
65
+ end
66
+
67
+ def auth0_base_url
68
+ @auth0_base_url ||= auth0_domain.start_with?("http") ? auth0_domain : "https://#{auth0_domain}"
69
+ end
70
+
71
+ def auth0_authorization_url
72
+ "#{auth0_base_url}/authorize"
73
+ end
74
+
75
+ def auth0_token_url
76
+ "#{auth0_base_url}/oauth/token"
77
+ end
78
+
79
+ def auth0_userinfo_url
80
+ "#{auth0_base_url}/userinfo"
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aven
4
+ module Oauth
5
+ class BaseController < ApplicationController
6
+ skip_before_action :verify_authenticity_token, only: [:callback]
7
+
8
+ # Initiates OAuth flow
9
+ def create
10
+ state = SecureRandom.hex(16)
11
+ session[:oauth_state] = state
12
+
13
+ redirect_to authorization_url(state), allow_other_host: true
14
+ end
15
+
16
+ # Handles OAuth callback
17
+ def callback
18
+ validate_state!
19
+
20
+ token_data = exchange_code_for_token(params[:code])
21
+ user_info = fetch_user_info(token_data[:access_token])
22
+
23
+ user = find_or_create_user(user_info, token_data)
24
+
25
+ if user.persisted?
26
+ sign_in_and_redirect(user)
27
+ else
28
+ handle_failed_authentication(user)
29
+ end
30
+ rescue => e
31
+ Rails.logger.error("OAuth authentication failed: #{e.class.name} - #{e.message}")
32
+ Rails.logger.error(e.backtrace.first(10).join("\n")) unless Rails.env.production?
33
+
34
+ error_message = if Rails.env.production?
35
+ "Authentication failed. Please try again."
36
+ else
37
+ "#{e.message}"
38
+ end
39
+
40
+ error_class = Rails.env.production? ? nil : e.class.name
41
+ render_error_page(error_message, error_class)
42
+ end
43
+
44
+ # Renders OAuth error page
45
+ def error
46
+ @error_message = params[:message] || "Authentication failed"
47
+ @error_class = params[:error_class]
48
+
49
+ view_component(
50
+ "oauth/error",
51
+ error_message: @error_message,
52
+ error_class: @error_class,
53
+ current_user:
54
+ )
55
+ end
56
+
57
+ protected
58
+
59
+ # Must be implemented by subclasses
60
+ def authorization_url(state)
61
+ raise NotImplementedError
62
+ end
63
+
64
+ def exchange_code_for_token(code)
65
+ raise NotImplementedError
66
+ end
67
+
68
+ def fetch_user_info(access_token)
69
+ raise NotImplementedError
70
+ end
71
+
72
+ # Common helper methods
73
+ def validate_state!
74
+ if params[:state] != session[:oauth_state]
75
+ raise StandardError, "Invalid state parameter"
76
+ end
77
+ session.delete(:oauth_state)
78
+ end
79
+
80
+ def find_or_create_user(user_info, token_data)
81
+ auth_tenant = request.host
82
+
83
+ user = Aven::User.where(auth_tenant:)
84
+ .where("remote_id = ? OR email = ?", user_info[:id].to_s, user_info[:email])
85
+ .first_or_initialize
86
+
87
+ user.tap do |u|
88
+ u.auth_tenant = auth_tenant
89
+ u.remote_id = user_info[:id].to_s
90
+ u.email = user_info[:email]
91
+ u.access_token = token_data[:access_token]
92
+ u.save
93
+ end
94
+ end
95
+
96
+ def sign_in_and_redirect(user)
97
+ sign_in(user)
98
+ set_current_workspace_for(user)
99
+ redirect_to after_sign_in_path_for(user)
100
+ end
101
+
102
+ def set_current_workspace_for(user)
103
+ workspace = user.workspaces.first
104
+
105
+ # Create default workspace if user has none (new sign up)
106
+ if workspace.nil?
107
+ workspace = Aven::Workspace.create!(label: "Default Workspace")
108
+ Aven::WorkspaceUser.create!(user: user, workspace: workspace)
109
+ user.reload
110
+ end
111
+
112
+ # Set current workspace in session
113
+ session[:workspace_id] = workspace.id
114
+ end
115
+
116
+ def handle_failed_authentication(user)
117
+ error_message = if !Rails.env.production? && user.errors.any?
118
+ user.errors.full_messages.join(", ")
119
+ else
120
+ "Failed to create user account"
121
+ end
122
+
123
+ error_class = Rails.env.production? ? nil : "User::ValidationError"
124
+ render_error_page(error_message, error_class)
125
+ end
126
+
127
+ def render_error_page(message, error_class = nil)
128
+ view_component(
129
+ "oauth/error",
130
+ error_message: message,
131
+ error_class:,
132
+ current_user:
133
+ )
134
+ end
135
+
136
+ def after_sign_in_path_for(resource)
137
+ stored_location_for(resource) ||
138
+ Aven.configuration.resolve_authenticated_root_path ||
139
+ begin
140
+ main_app.root_path
141
+ rescue NoMethodError
142
+ root_path
143
+ end
144
+ end
145
+
146
+ # HTTP helper for OAuth requests
147
+ def oauth_request(uri, params, headers = {})
148
+ http = Net::HTTP.new(uri.host, uri.port)
149
+ http.use_ssl = true
150
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if Rails.env.development?
151
+
152
+ request = Net::HTTP::Post.new(uri)
153
+ request.set_form_data(params)
154
+ headers.each { |key, value| request[key] = value }
155
+
156
+ response = http.request(request)
157
+
158
+ unless response.is_a?(Net::HTTPSuccess)
159
+ raise StandardError, "OAuth request failed: #{response.body}"
160
+ end
161
+
162
+ JSON.parse(response.body, symbolize_names: true)
163
+ end
164
+
165
+ def oauth_get_request(uri, access_token)
166
+ http = Net::HTTP.new(uri.host, uri.port)
167
+ http.use_ssl = true
168
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if Rails.env.development?
169
+
170
+ request = Net::HTTP::Get.new(uri)
171
+ request["Authorization"] = "Bearer #{access_token}"
172
+
173
+ response = http.request(request)
174
+
175
+ unless response.is_a?(Net::HTTPSuccess)
176
+ raise StandardError, "OAuth request failed: #{response.body}"
177
+ end
178
+
179
+ JSON.parse(response.body, symbolize_names: true)
180
+ end
181
+ end
182
+ end
183
+ end