groovestack-auth 0.1.5 → 0.1.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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +8 -1
  3. data/Gemfile.lock +120 -20
  4. data/{lib/groovestack/auth/action_cable.rb → app/channels/groovestack/auth/action_cable/connection.rb} +2 -2
  5. data/app/controllers/concerns/groovestack/auth/graphql/controllers/auth_helpers.rb +69 -0
  6. data/app/controllers/concerns/groovestack/auth/graphql/controllers/authed_execute.rb +16 -0
  7. data/app/controllers/groovestack/auth/authenticated_api_controller.rb +10 -0
  8. data/app/controllers/groovestack/auth/omniauth_callbacks_controller.rb +138 -0
  9. data/app/controllers/groovestack/auth/passwordless/magic_links_controller.rb +58 -0
  10. data/app/controllers/groovestack/auth/passwordless/sessions_controller.rb +75 -0
  11. data/app/graphql/graphql/identity_extensions.rb +11 -0
  12. data/app/graphql/graphql/user_extensions.rb +14 -0
  13. data/app/models/concerns/groovestack/auth/authorized_fields_for_serialization.rb +21 -0
  14. data/app/models/concerns/groovestack/auth/identity.rb +39 -0
  15. data/app/models/concerns/groovestack/auth/user.rb +14 -0
  16. data/app/views/devise/mailer/magic_link.html.erb +9 -0
  17. data/config/initializers/core_config.rb +0 -6
  18. data/config/initializers/devise.rb +387 -302
  19. data/config/initializers/omniauth.rb +0 -19
  20. data/config/locales/devise.en.yml +71 -0
  21. data/db/migrate/20231103174050_add_devise_to_users_and_identities.rb +59 -0
  22. data/groovestack-auth.gemspec +7 -7
  23. data/lib/groovestack/auth/{railtie.rb → engine.rb} +13 -2
  24. data/lib/groovestack/auth/graphql/authorized_field.rb +19 -0
  25. data/lib/groovestack/auth/graphql/authorized_object.rb +11 -0
  26. data/lib/groovestack/auth/graphql/schema_visibility.rb +40 -0
  27. data/lib/groovestack/auth/graphql/visible_field.rb +21 -0
  28. data/lib/groovestack/auth/graphql/visible_object.rb +17 -0
  29. data/lib/groovestack/auth/passwordless/t_otp_tokenizer.rb +89 -0
  30. data/lib/groovestack/auth/provider.rb +7 -0
  31. data/lib/groovestack/auth/providers/apple.rb +5 -5
  32. data/lib/groovestack/auth/providers/facebook.rb +17 -0
  33. data/lib/groovestack/auth/providers/google.rb +1 -1
  34. data/lib/groovestack/auth/providers/omni_auth.rb +2 -2
  35. data/lib/groovestack/auth/routes.rb +26 -0
  36. data/lib/groovestack/auth/settings.rb +43 -0
  37. data/lib/groovestack/auth/version.rb +1 -1
  38. data/lib/groovestack/auth.rb +33 -83
  39. metadata +55 -50
  40. data/config/initializers/devise_token_auth.rb +0 -72
  41. data/config/initializers/graphql_devise.rb +0 -58
  42. data/config/routes.rb +0 -11
  43. data/db/migrate/20231103172517_create_users.rb +0 -54
  44. data/db/migrate/20231103174037_create_identities.rb +0 -19
  45. data/lib/fabricators/user_fabricator.rb +0 -17
  46. data/lib/graphql/identity/filter.rb +0 -13
  47. data/lib/graphql/identity/mutations.rb +0 -27
  48. data/lib/graphql/identity/queries.rb +0 -25
  49. data/lib/graphql/identity/type.rb +0 -22
  50. data/lib/graphql/user/filter.rb +0 -15
  51. data/lib/graphql/user/mutations.rb +0 -63
  52. data/lib/graphql/user/queries.rb +0 -40
  53. data/lib/graphql/user/type.rb +0 -30
  54. data/lib/groovestack/auth/authenticated_api_controller.rb +0 -13
  55. data/lib/groovestack/auth/omniauth_callbacks_controller.rb +0 -111
  56. data/lib/groovestack/auth/schema_plugin.rb +0 -19
  57. data/lib/identity.rb +0 -31
  58. data/lib/user.rb +0 -53
  59. data/lib/users/roles.rb +0 -42
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6334f31e76abf39c1440546651f4dc291093c02b039c7d44bb88d79499cfad42
4
- data.tar.gz: b246b2c3d402db429cbb39a110ba33d37825e3f72e4c5bf7cd207f943c7ef8e5
3
+ metadata.gz: 8adb7a1b84a4b5aded491344181c888900557ce327e15b5d441581c6e3c88146
4
+ data.tar.gz: 9fef159454b305cc1b4a4a587bfb84a249cf9357e7bf28712b795492d1568dac
5
5
  SHA512:
6
- metadata.gz: cf1349a5f3a9ac5e766263deb3f3dac46f7b7aa81e3ca12ac98d19e1d339489da14b08f73bd03441dcc74ea44561b6322e788c42f38ce288936cbf1923ad4728
7
- data.tar.gz: 4ddab00dd1014608fc1b4ce76b5998c905cef45d0c0981e2ba87220602db810e9bcde4cfb15deda756052f918203a4402d47801ae8189afd5081a0000ee85f87
6
+ metadata.gz: 54f7df9d82696102e44a34d251322042e030680c5fad56dca3b3c1ee92c6d6bd60e05629bf3cdd5bebe4baab2ddde555cc6e50ef47837b3b0e5d114fadcc3635
7
+ data.tar.gz: 8215562c42ae838366b9b79048f24d4c263059c017dcf1a0aaae616107d123ad88fb192f88e5a3d2a8f748af7441225ec1d32ed1e87bccb9a1d5fe89306c50a4
data/Gemfile CHANGED
@@ -5,8 +5,15 @@ source 'https://rubygems.org'
5
5
  # Specify your gem's dependencies in groovestack-auth.gemspec
6
6
  gemspec
7
7
 
8
+ gem 'devise-passwordless'
9
+ gem 'omniauth-apple'
10
+ gem 'omniauth-facebook'
11
+ gem 'omniauth-google-oauth2', '1.1.2'
12
+
8
13
  gem 'fabrication'
9
14
  gem 'faker'
10
15
  gem 'racksh' # get a console without a full Rails application
11
16
  gem 'rspec'
12
- gem 'rubocop', require: false
17
+ gem 'rubocop'
18
+ gem 'rubocop-graphql', '~> 1.0'
19
+ gem 'rubocop-rails', '~> 2.18'
data/Gemfile.lock CHANGED
@@ -2,15 +2,32 @@ PATH
2
2
  remote: .
3
3
  specs:
4
4
  groovestack-auth (0.1.5)
5
- groovestack-base (~> 0.1, >= 0.1.10)
6
- groovestack-config (~> 0.1, >= 0.1.2)
7
- jsonb_accessor (~> 1.4)
8
- omniauth-apple
9
- omniauth-google-oauth2
5
+ devise
6
+ devise-passwordless
7
+ groovestack-config (~> 0.1, >= 0.1.4)
8
+ groovestack-identities (~> 0.1, >= 0.1.3)
9
+ rotp
10
10
 
11
11
  GEM
12
12
  remote: https://rubygems.org/
13
13
  specs:
14
+ actionpack (7.2.2.1)
15
+ actionview (= 7.2.2.1)
16
+ activesupport (= 7.2.2.1)
17
+ nokogiri (>= 1.8.5)
18
+ racc
19
+ rack (>= 2.2.4, < 3.2)
20
+ rack-session (>= 1.0.1)
21
+ rack-test (>= 0.6.3)
22
+ rails-dom-testing (~> 2.2)
23
+ rails-html-sanitizer (~> 1.6)
24
+ useragent (~> 0.16)
25
+ actionview (7.2.2.1)
26
+ activesupport (= 7.2.2.1)
27
+ builder (~> 3.1)
28
+ erubi (~> 1.11)
29
+ rails-dom-testing (~> 2.2)
30
+ rails-html-sanitizer (~> 1.6)
14
31
  activemodel (7.2.2.1)
15
32
  activesupport (= 7.2.2.1)
16
33
  activerecord (7.2.2.1)
@@ -32,12 +49,25 @@ GEM
32
49
  aes_key_wrap (1.1.0)
33
50
  ast (2.4.3)
34
51
  base64 (0.2.0)
52
+ bcrypt (3.1.20)
35
53
  benchmark (0.4.0)
36
54
  bigdecimal (3.1.9)
37
55
  bindata (2.5.0)
56
+ builder (3.3.0)
38
57
  concurrent-ruby (1.3.5)
39
58
  connection_pool (2.5.0)
40
- diff-lcs (1.6.1)
59
+ crass (1.0.6)
60
+ date (3.4.1)
61
+ devise (4.9.4)
62
+ bcrypt (~> 3.0)
63
+ orm_adapter (~> 0.1)
64
+ railties (>= 4.1.0)
65
+ responders
66
+ warden (~> 1.2.3)
67
+ devise-passwordless (1.1.0)
68
+ devise
69
+ globalid
70
+ diff-lcs (1.6.2)
41
71
  drb (2.2.1)
42
72
  dry-configurable (1.3.0)
43
73
  dry-core (~> 1.1)
@@ -46,6 +76,7 @@ GEM
46
76
  concurrent-ruby (~> 1.0)
47
77
  logger
48
78
  zeitwerk (~> 2.6)
79
+ erubi (1.13.1)
49
80
  fabrication (2.31.0)
50
81
  faker (3.5.1)
51
82
  i18n (>= 1.8.11, < 2)
@@ -58,21 +89,30 @@ GEM
58
89
  faraday-net_http (3.4.0)
59
90
  net-http (>= 0.5.0)
60
91
  fiber-storage (1.0.0)
92
+ globalid (1.2.1)
93
+ activesupport (>= 6.1)
61
94
  graphql (2.3.22)
62
95
  base64
63
96
  fiber-storage
64
- groovestack-base (0.1.10)
97
+ groovestack-base (0.1.12)
65
98
  activerecord (~> 7.0)
66
99
  dry-configurable (~> 1.0)
67
100
  graphql (>= 1.8, < 2.4)
68
101
  pg (~> 1.0)
69
102
  pg_lock (~> 1.0)
70
- puma (~> 5.0)
71
- groovestack-config (0.1.2)
72
- groovestack-base (~> 0.1.10)
103
+ puma (>= 5.0, < 7)
104
+ groovestack-config (0.1.4)
105
+ groovestack-base (~> 0.1, >= 0.1.12)
106
+ groovestack-identities (0.1.3)
107
+ groovestack-base (~> 0.1, >= 0.1.12)
73
108
  hashie (5.0.0)
74
109
  i18n (1.14.7)
75
110
  concurrent-ruby (~> 1.0)
111
+ io-console (0.8.0)
112
+ irb (1.15.2)
113
+ pp (>= 0.6.0)
114
+ rdoc (>= 4.0.0)
115
+ reline (>= 0.4.2)
76
116
  json (2.10.2)
77
117
  json-jwt (1.16.7)
78
118
  activesupport (>= 4.2)
@@ -81,21 +121,22 @@ GEM
81
121
  bindata
82
122
  faraday (~> 2.0)
83
123
  faraday-follow_redirects
84
- jsonb_accessor (1.4)
85
- activerecord (>= 6.1)
86
- activesupport (>= 6.1)
87
- pg (>= 0.18.1)
88
124
  jwt (2.10.1)
89
125
  base64
90
126
  language_server-protocol (3.17.0.4)
91
127
  lint_roller (1.1.0)
92
128
  logger (1.7.0)
129
+ loofah (2.24.0)
130
+ crass (~> 1.0.2)
131
+ nokogiri (>= 1.12.0)
93
132
  minitest (5.25.5)
94
133
  multi_xml (0.7.1)
95
134
  bigdecimal (~> 3.1)
96
135
  net-http (0.6.0)
97
136
  uri
98
137
  nio4r (2.7.4)
138
+ nokogiri (1.18.8-arm64-darwin)
139
+ racc (~> 1.4)
99
140
  oauth2 (2.0.9)
100
141
  faraday (>= 0.17.3, < 3.0)
101
142
  jwt (>= 1.0, < 3.0)
@@ -110,21 +151,31 @@ GEM
110
151
  omniauth-apple (1.3.0)
111
152
  json-jwt
112
153
  omniauth-oauth2
113
- omniauth-google-oauth2 (1.2.1)
114
- jwt (>= 2.9.2)
154
+ omniauth-facebook (10.0.0)
155
+ bigdecimal
156
+ omniauth-oauth2 (>= 1.2, < 3)
157
+ omniauth-google-oauth2 (1.1.2)
158
+ jwt (>= 2.0)
115
159
  oauth2 (~> 2.0)
116
160
  omniauth (~> 2.0)
117
161
  omniauth-oauth2 (~> 1.8)
118
162
  omniauth-oauth2 (1.8.0)
119
163
  oauth2 (>= 1.4, < 3)
120
164
  omniauth (~> 2.0)
165
+ orm_adapter (0.5.0)
121
166
  parallel (1.26.3)
122
167
  parser (3.3.7.4)
123
168
  ast (~> 2.4.1)
124
169
  racc
125
170
  pg (1.5.9)
126
171
  pg_lock (1.0.0)
172
+ pp (0.6.2)
173
+ prettyprint
174
+ prettyprint (0.2.0)
127
175
  prism (1.4.0)
176
+ psych (5.2.3)
177
+ date
178
+ stringio
128
179
  puma (5.6.9)
129
180
  nio4r (~> 2.0)
130
181
  racc (1.8.1)
@@ -133,26 +184,55 @@ GEM
133
184
  base64 (>= 0.1.0)
134
185
  logger (>= 1.6.0)
135
186
  rack (>= 3.0.0, < 4)
187
+ rack-session (2.1.0)
188
+ base64 (>= 0.1.0)
189
+ rack (>= 3.0.0)
136
190
  rack-test (2.2.0)
137
191
  rack (>= 1.3)
138
192
  racksh (1.0.1)
139
193
  rack (>= 1.0)
140
194
  rack-test (>= 0.5)
195
+ rackup (2.2.1)
196
+ rack (>= 3)
197
+ rails-dom-testing (2.2.0)
198
+ activesupport (>= 5.0.0)
199
+ minitest
200
+ nokogiri (>= 1.6)
201
+ rails-html-sanitizer (1.6.2)
202
+ loofah (~> 2.21)
203
+ nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
204
+ railties (7.2.2.1)
205
+ actionpack (= 7.2.2.1)
206
+ activesupport (= 7.2.2.1)
207
+ irb (~> 1.13)
208
+ rackup (>= 1.0.0)
209
+ rake (>= 12.2)
210
+ thor (~> 1.0, >= 1.2.2)
211
+ zeitwerk (~> 2.6)
141
212
  rainbow (3.1.1)
213
+ rake (13.2.1)
214
+ rdoc (6.13.1)
215
+ psych (>= 4.0.0)
142
216
  regexp_parser (2.10.0)
217
+ reline (0.6.1)
218
+ io-console (~> 0.5)
219
+ responders (3.1.1)
220
+ actionpack (>= 5.2)
221
+ railties (>= 5.2)
222
+ rotp (6.3.0)
143
223
  rspec (3.13.0)
144
224
  rspec-core (~> 3.13.0)
145
225
  rspec-expectations (~> 3.13.0)
146
226
  rspec-mocks (~> 3.13.0)
147
227
  rspec-core (3.13.3)
148
228
  rspec-support (~> 3.13.0)
149
- rspec-expectations (3.13.3)
229
+ rspec-expectations (3.13.4)
150
230
  diff-lcs (>= 1.2.0, < 2.0)
151
231
  rspec-support (~> 3.13.0)
152
- rspec-mocks (3.13.2)
232
+ rspec-mocks (3.13.4)
153
233
  diff-lcs (>= 1.2.0, < 2.0)
154
234
  rspec-support (~> 3.13.0)
155
- rspec-support (3.13.2)
235
+ rspec-support (3.13.3)
156
236
  rubocop (1.75.2)
157
237
  json (~> 2.3)
158
238
  language_server-protocol (~> 3.17.0.2)
@@ -167,11 +247,22 @@ GEM
167
247
  rubocop-ast (1.44.0)
168
248
  parser (>= 3.3.7.2)
169
249
  prism (~> 1.4)
250
+ rubocop-graphql (1.5.5)
251
+ lint_roller (~> 1.1)
252
+ rubocop (>= 1.72.1, < 2)
253
+ rubocop-rails (2.31.0)
254
+ activesupport (>= 4.2.0)
255
+ lint_roller (~> 1.1)
256
+ rack (>= 1.1)
257
+ rubocop (>= 1.75.0, < 2.0)
258
+ rubocop-ast (>= 1.38.0, < 2.0)
170
259
  ruby-progressbar (1.13.0)
171
260
  securerandom (0.4.1)
172
261
  snaky_hash (2.0.1)
173
262
  hashie
174
263
  version_gem (~> 1.1, >= 1.1.1)
264
+ stringio (3.1.7)
265
+ thor (1.3.2)
175
266
  timeout (0.4.3)
176
267
  tzinfo (2.0.6)
177
268
  concurrent-ruby (~> 1.0)
@@ -179,19 +270,28 @@ GEM
179
270
  unicode-emoji (~> 4.0, >= 4.0.4)
180
271
  unicode-emoji (4.0.4)
181
272
  uri (1.0.3)
273
+ useragent (0.16.11)
182
274
  version_gem (1.1.6)
275
+ warden (1.2.9)
276
+ rack (>= 2.0.9)
183
277
  zeitwerk (2.6.18)
184
278
 
185
279
  PLATFORMS
186
280
  arm64-darwin-23
187
281
 
188
282
  DEPENDENCIES
283
+ devise-passwordless
189
284
  fabrication
190
285
  faker
191
286
  groovestack-auth!
287
+ omniauth-apple
288
+ omniauth-facebook
289
+ omniauth-google-oauth2 (= 1.1.2)
192
290
  racksh
193
291
  rspec
194
292
  rubocop
293
+ rubocop-graphql (~> 1.0)
294
+ rubocop-rails (~> 2.18)
195
295
 
196
296
  BUNDLED WITH
197
- 2.6.7
297
+ 2.6.2
@@ -7,11 +7,11 @@ module Groovestack
7
7
  extend ActiveSupport::Concern
8
8
 
9
9
  included do
10
- identified_by :current_resource
10
+ identified_by :current_user
11
11
  end
12
12
 
13
13
  def connect
14
- self.current_resource = find_verified_resource
14
+ self.current_user = find_verified_resource
15
15
  end
16
16
 
17
17
  private
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Auth
5
+ module GraphQL
6
+ module Controllers
7
+ module AuthHelpers
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ include Pundit::Authorization if defined?(::Pundit)
12
+
13
+ before_action :authenticate_request!, unless: :graphiql?
14
+
15
+ private
16
+
17
+ def context
18
+ {
19
+ visibility_profile: visibility_profile,
20
+ current_user: current_user
21
+ }.tap do |ctx|
22
+ ctx[:authorize] = method(:authorize) if defined?(::Pundit)
23
+ end
24
+ end
25
+
26
+ def graphiql?
27
+ return false unless defined?(::GraphiQL)
28
+
29
+ Rails.env.development? && request.referer == ::Rails.application.routes.url_helpers.graphiql_rails_url
30
+ end
31
+
32
+ def graphiql_visibility_profile
33
+ @graphiql_visibility_profile ||= request.headers['HTTP_GRAPHIQL_VISIBILITY_PROFILE']
34
+ end
35
+
36
+ def visibility_profile
37
+ @visibility_profile ||= if graphiql?
38
+ if graphiql_visibility_profile.blank?
39
+ raise 'must set GRAPHIQL_VISIBILITY_PROFILE header in graphiql initializer '
40
+ end
41
+
42
+ graphiql_visibility_profile.to_sym
43
+ elsif current_user.nil?
44
+ :anon
45
+ elsif current_user.admin?
46
+ ::User::Role::ADMIN.to_sym
47
+ elsif current_user.exec?
48
+ ::User::Role::EXEC.to_sym
49
+ elsif current_user.staff?
50
+ ::User::Role::STAFF.to_sym
51
+ else
52
+ :user
53
+ end
54
+ end
55
+
56
+ def authenticate_request!
57
+ # skip authentication for AppConfig queries
58
+ # introspection query is public. fields will be visible/not based on visibility_profile
59
+
60
+ return if %w[AppConfig IntrospectionQuery].include?(operation_name)
61
+
62
+ authenticate_user!
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Auth
5
+ module GraphQL
6
+ module Controllers
7
+ module AuthedExecute
8
+ extend ActiveSupport::Concern
9
+
10
+ include ::Groovestack::Base::GraphQL::Controllers::Execute
11
+ include ::Groovestack::Auth::GraphQL::Controllers::AuthHelpers
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Auth
5
+ class AuthenticatedApiController < ApplicationController
6
+ prepend_before_action :authenticate_user!
7
+ protect_from_forgery prepend: true, with: :reset_session unless -> { Rails.env.test? }
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Auth
5
+ class OmniauthCallbacksController < ::Devise::OmniauthCallbacksController
6
+ include ::Devise::Controllers::Rememberable
7
+
8
+ skip_before_action :verify_authenticity_token
9
+ before_action :set_auth_hash
10
+
11
+ Groovestack::Auth.configured_providers(ancestor: Groovestack::Auth::Providers::OmniAuth).each do |p|
12
+ define_method(p.k) do
13
+ handle_oauth(provider: p.k.to_s.capitalize)
14
+ end
15
+ end
16
+
17
+ def failure
18
+ # override for error notifications & to support custom origin redirects
19
+
20
+ known_reasons = %w[user_cancelled_authorize user_denied]
21
+
22
+ handled = known_reasons.any? do |reason|
23
+ request.params&.dig('error_reason') == reason || request.params&.dig('error') == reason
24
+ end
25
+
26
+ if !handled && Rails.env.production?
27
+ failure_kind = OmniAuth::Utils.camelize(failed_strategy.name)
28
+ blob = {
29
+ kind: failure_kind,
30
+ reason: failure_message,
31
+ params: request.params,
32
+ omniauth: request.env['omniauth.auth']
33
+ }
34
+
35
+ exception = OmniauthFailureError.new(blob.to_s)
36
+
37
+ ::Groovestack::Base.notify_error('Groovestack::Auth::OmniauthCallbacksController.omniauth_failure', exception)
38
+ end
39
+
40
+ set_flash_message! :alert, :failure, kind: failure_kind, reason: failure_message
41
+ redirect_to after_omniauth_failure_path_for(resource_name),
42
+ allow_other_host: ::Groovestack::Auth.allow_other_host_redirects
43
+ end
44
+
45
+ protected
46
+
47
+ def set_auth_hash
48
+ # set omniauth.auth for /callback requests (that skip the request phase)
49
+ return if params['omniauth'].blank?
50
+
51
+ # only set if not already set (i.e. no override)
52
+ request.env['omniauth.auth'] ||= JSON.parse(params['omniauth'].to_json, object_class: OmniAuth::AuthHash)
53
+ end
54
+
55
+ def after_omniauth_failure_path_for(scope)
56
+ if (origin_url = omniauth_request_origin)
57
+ return origin_url
58
+ end
59
+
60
+ super
61
+ end
62
+
63
+ def omniauth_request_origin
64
+ return ::Groovestack::Auth.omniauth_origin_url if ::Groovestack::Auth.omniauth_origin_url.present?
65
+ return ::Rails.application.routes.url_helpers.root_url if same_origin_request?
66
+
67
+ request.env['omniauth.origin']
68
+ end
69
+
70
+ private
71
+
72
+ def json_format?
73
+ # WHY necessary?
74
+ # request.format.json? may not be set correctly for FE auth that handles the
75
+ # request phase and only hits /callback
76
+ # request.path_parameters[:format] is set to 'json' in the same case
77
+ request.format.json? || request.path_parameters[:format] == 'json'
78
+ end
79
+
80
+ def same_origin_request?
81
+ return true if request.env['omniauth.origin'].blank?
82
+
83
+ request.env['omniauth.origin'].starts_with?(request.base_url)
84
+ end
85
+
86
+ def add_search_params(url, params)
87
+ uri = URI.parse(url)
88
+ existing_params = Rack::Utils.parse_nested_query(uri.query)
89
+ merged_params = existing_params.merge(params.deep_stringify_keys)
90
+ uri.query = Rack::Utils.build_nested_query(merged_params)
91
+ uri.to_s
92
+ end
93
+
94
+ def handle_oauth(provider:)
95
+ auth = request.env['omniauth.auth']
96
+
97
+ identity_params = {
98
+ auth: auth,
99
+ user_attrs: {
100
+ defaults: {
101
+ password: ::Devise.friendly_token[0, 20]
102
+ }
103
+ }
104
+ }
105
+
106
+ @user = ::Identity.find_or_create_from_omniauth!(**identity_params).user
107
+
108
+ begin
109
+ unless @user.confirmed?
110
+ @user.skip_confirmation_notification! # skip sending confirmation email
111
+ @user.confirm
112
+ end
113
+ rescue StandardError => e
114
+ msg = "OmniAuth Callback Confirmation Failure: #{e}"
115
+ ::Groovestack::Base.notify_error(e.class, msg)
116
+ end
117
+
118
+ remember_me @user
119
+ sign_in(:user, @user)
120
+
121
+ if json_format?
122
+ # return connected identity info for FE user hydration
123
+ render json: @user.slice(:id, :email, :name), status: :ok
124
+ elsif same_origin_request?
125
+ redirect_to after_sign_in_path_for(@user)
126
+ else
127
+ redirect_to omniauth_request_origin, allow_other_host: ::Groovestack::Auth.allow_other_host_redirects
128
+ end
129
+ rescue ActiveRecord::RecordInvalid => e
130
+ # let FE handle missing email gracefully
131
+ raise e unless e.record.errors[:email].present? && e.record.errors[:email].include?("can't be blank")
132
+
133
+ redirect_to add_search_params(after_omniauth_failure_path_for(resource_name), { errors: { email_missing: true } }),
134
+ allow_other_host: ::Groovestack::Auth.allow_other_host_redirects
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Auth
5
+ module Passwordless
6
+ class MagicLinksController < ::DeviseController
7
+ skip_before_action :verify_authenticity_token
8
+ prepend_before_action :require_no_authentication, only: :show
9
+ prepend_before_action :allow_params_authentication!, only: :show
10
+ prepend_before_action(only: [:show]) { request.env['devise.skip_timeout'] = true }
11
+
12
+ def show
13
+ self.resource = warden.authenticate!(auth_options)
14
+
15
+ begin
16
+ unless resource.confirmed?
17
+ resource.skip_confirmation_notification! # skip sending confirmation email
18
+ resource.confirm
19
+ end
20
+ rescue StandardError => e
21
+ msg = "Passwordless Magic Link Confirmation Failure: #{e}"
22
+ ::Groovestack::Base.notify_error(e.class, msg)
23
+ end
24
+
25
+ set_flash_message!(:notice, :signed_in)
26
+ sign_in(resource_name, resource, event: :authentication)
27
+ yield resource if block_given?
28
+
29
+ respond_to do |format|
30
+ format.html { redirect_to after_sign_in_path_for(resource) }
31
+ format.json { render json: { success: true, resource: resource.as_json, status: :ok } }
32
+ end
33
+ end
34
+
35
+ protected
36
+
37
+ def auth_options
38
+ mapping = ::Devise.mappings[resource_name]
39
+ { scope: resource_name, recall: "#{mapping.controllers[:sessions]}#new" }
40
+ end
41
+
42
+ def translation_scope
43
+ 'devise.sessions'
44
+ end
45
+
46
+ private
47
+
48
+ def resource_params
49
+ params.permit({ user: %i[email remember_me token] })
50
+ end
51
+
52
+ def create_params
53
+ resource_params.permit({ user: %i[email remember_me] })
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Auth
5
+ module Passwordless
6
+ class SessionsController < ::Devise::SessionsController
7
+ skip_before_action :verify_authenticity_token # , only: :destroy
8
+
9
+ def create
10
+ self.resource = resource_class.find_for_authentication(email: create_params[:email])
11
+
12
+ if resource.blank?
13
+ # register a new account
14
+
15
+ u = ::User.new(email: create_params[:email], password: ::Devise.friendly_token[0, 20])
16
+ # u.skip_confirmation_notification!
17
+ u.save!
18
+
19
+ self.resource = u
20
+ end
21
+
22
+ send_magic_link(resource)
23
+
24
+ respond_to do |format|
25
+ format.html { redirect_to(after_magic_link_sent_path_for(resource), status: devise_redirect_status) }
26
+ format.json do
27
+ render json: { success: true, resource: resource.as_json, message: 'Magic link sent.' }, status: :ok
28
+ end
29
+ end
30
+ rescue StandardError => e
31
+ msg = "Passwordless Magic Link Send Failure for #{create_params[:email]}: #{e}"
32
+ ::Groovestack::Base.notify_error('Groovestack::Auth::Passwordless::SessionsController', msg)
33
+
34
+ respond_to do |format|
35
+ format.html { render :new, status: devise_error_status }
36
+ format.json do
37
+ render json: { error: 'User not found or unable to register account. Our team has been notified.' },
38
+ status: :not_found
39
+ end
40
+ end
41
+ end
42
+
43
+ protected
44
+
45
+ def send_magic_link(resource)
46
+ resource.send_magic_link(remember_me: create_params[:remember_me])
47
+ end
48
+
49
+ def translation_scope
50
+ if action_name == 'create'
51
+ 'devise.passwordless'
52
+ else
53
+ super
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def create_params
60
+ resource_params.permit(:email, :remember_me)
61
+ end
62
+
63
+ # Devise < 4.9 fallback support
64
+ # See: https://github.com/heartcombo/devise/wiki/How-To:-Upgrade-to-Devise-4.9.0-%5BHotwire-Turbo-integration%5D
65
+ def devise_redirect_status
66
+ ::Devise.try(:responder).try(:redirect_status) || :found
67
+ end
68
+
69
+ def devise_error_status
70
+ ::Devise.try(:responder).try(:error_status) || :ok
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end