standard_id 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d55cdf40a33a4f541b1de5ecb1c19f827b92123b63bccd56781da4671944ff45
4
- data.tar.gz: 714bc65bd0aa6a6913b51c0f59f659f41a425a5a21489103d89624149e3ff61d
3
+ metadata.gz: 7a8069c98c5ede27fc1f485806e5a9b150d4f43294d177f59fe89008e11bd289
4
+ data.tar.gz: e88dae408bce4db81c42c780985d9e4a833b4864636a2d10b5ff979407dc9562
5
5
  SHA512:
6
- metadata.gz: 0625c5efdb00b0e0d8558191c2b8ad7c9512c6ec13a5fe5896393497241f2f8313f8633b3e319358915b00ff33b23a3979b28e32c64fc20664b9a5e513625d5b
7
- data.tar.gz: 0fb8080e7bee90799d27daa892fb9c221e5d4558b0a5c73916249988f6875793ec54ae2afdc6c93d11b526acee37f23e7350e3f2f47522c67b2e6c37c10e8626
6
+ metadata.gz: 19f08dcd3da104952f2313669bc7bceb18ef0ce866ff234a0beb966e64d83294cefc76886c2f5e74080b935271444afeae0057deadcbac7bcfda40f6dd0441b4
7
+ data.tar.gz: 2531fba6a76ea0dd4800cebe0444a86cdea8ab516c64888b40b7d60aa681a957af2f3f4fb9622aebc6cf6f25b44a3caf0b3d03f16dcc673ce2f0a30c79599211
data/README.md CHANGED
@@ -37,6 +37,11 @@ A comprehensive authentication engine for Rails applications, built on the secur
37
37
  - **Remember Me**: Extended session support
38
38
  - **Account Lockout**: Protection against brute force attacks
39
39
 
40
+ ### ⚡ Frontend Framework Support
41
+ - **Inertia.js Integration**: Optional support for React, Vue, or Svelte frontends
42
+ - **Conditional Rendering**: Automatically switches between ERB and Inertia based on configuration
43
+ - **External Redirects**: Proper handling of OAuth redirects in SPA contexts
44
+
40
45
  ## Installation
41
46
 
42
47
  Add this line to your application's Gemfile:
@@ -114,6 +119,10 @@ StandardId.configure do |config|
114
119
  # Custom layout for web views
115
120
  config.web_layout = "application"
116
121
 
122
+ # Inertia.js support (see Inertia.js Integration section below)
123
+ # config.use_inertia = true
124
+ # config.inertia_component_namespace = "auth"
125
+
117
126
  # Passwordless delivery callbacks
118
127
  # config.passwordless_email_sender = ->(email, code) { UserMailer.send_code(email, code).deliver_now }
119
128
  # config.passwordless_sms_sender = ->(phone, code) { SmsService.send_code(phone, code) }
@@ -192,6 +201,160 @@ end
192
201
 
193
202
  `social_info` is an indifferent-access hash containing at least `email`, `name`, and `provider_id`.
194
203
 
204
+ ### Inertia.js Integration
205
+
206
+ StandardId supports [Inertia.js](https://inertiajs.com/) for modern React, Vue, or Svelte frontends. When enabled, web controllers render Inertia components instead of ERB views.
207
+
208
+ #### Setup
209
+
210
+ 1. Add the `inertia_rails` gem to your Gemfile:
211
+
212
+ ```ruby
213
+ gem "inertia_rails"
214
+ ```
215
+
216
+ 2. Enable Inertia in your StandardId configuration:
217
+
218
+ ```ruby
219
+ StandardId.configure do |config|
220
+ config.use_inertia = true
221
+ config.inertia_component_namespace = "auth" # Optional, defaults to "standard_id"
222
+ end
223
+ ```
224
+
225
+ 3. Create the corresponding frontend components. The component path follows the pattern:
226
+ `{namespace}/{ControllerName}/{action}`
227
+
228
+ For example, with `inertia_component_namespace = "auth"`:
229
+ - Login page: `pages/auth/login/show.tsx`
230
+ - Signup page: `pages/auth/signup/show.tsx`
231
+
232
+ #### Example Component (React)
233
+
234
+ ```tsx
235
+ // frontend/pages/auth/login/show.tsx
236
+ import { useForm } from '@inertiajs/react'
237
+
238
+ interface Props {
239
+ redirect_uri: string
240
+ connection: string | null
241
+ flash: { notice?: string; alert?: string }
242
+ social_providers: { google_enabled: boolean; apple_enabled: boolean }
243
+ }
244
+
245
+ export default function LoginShow({ redirect_uri, flash, social_providers }: Props) {
246
+ const { data, setData, post, processing } = useForm({
247
+ 'login[email]': '',
248
+ 'login[password]': '',
249
+ 'login[remember_me]': false,
250
+ redirect_uri,
251
+ })
252
+
253
+ const handleSubmit = (e: React.FormEvent) => {
254
+ e.preventDefault()
255
+ post('/login')
256
+ }
257
+
258
+ const handleSocialLogin = (connection: string) => {
259
+ post('/login', { data: { connection, redirect_uri } })
260
+ }
261
+
262
+ return (
263
+ <div className="login-container">
264
+ {flash.alert && <div className="alert alert-error">{flash.alert}</div>}
265
+ {flash.notice && <div className="alert alert-success">{flash.notice}</div>}
266
+
267
+ <form onSubmit={handleSubmit}>
268
+ <div>
269
+ <label htmlFor="email">Email</label>
270
+ <input
271
+ id="email"
272
+ type="email"
273
+ value={data['login[email]']}
274
+ onChange={e => setData('login[email]', e.target.value)}
275
+ required
276
+ />
277
+ </div>
278
+
279
+ <div>
280
+ <label htmlFor="password">Password</label>
281
+ <input
282
+ id="password"
283
+ type="password"
284
+ value={data['login[password]']}
285
+ onChange={e => setData('login[password]', e.target.value)}
286
+ required
287
+ />
288
+ </div>
289
+
290
+ <div>
291
+ <label>
292
+ <input
293
+ type="checkbox"
294
+ checked={data['login[remember_me]'] as boolean}
295
+ onChange={e => setData('login[remember_me]', e.target.checked)}
296
+ />
297
+ Remember me
298
+ </label>
299
+ </div>
300
+
301
+ <button type="submit" disabled={processing}>
302
+ {processing ? 'Signing in...' : 'Sign In'}
303
+ </button>
304
+ </form>
305
+
306
+ {(social_providers.google_enabled || social_providers.apple_enabled) && (
307
+ <div className="social-login">
308
+ <p>Or continue with</p>
309
+ {social_providers.google_enabled && (
310
+ <button type="button" onClick={() => handleSocialLogin('google')}>
311
+ Sign in with Google
312
+ </button>
313
+ )}
314
+ {social_providers.apple_enabled && (
315
+ <button type="button" onClick={() => handleSocialLogin('apple')}>
316
+ Sign in with Apple
317
+ </button>
318
+ )}
319
+ </div>
320
+ )}
321
+ </div>
322
+ )
323
+ }
324
+ ```
325
+
326
+ > **Note:** The `useForm` hook from `@inertiajs/react` automatically handles CSRF tokens. When you call `post()`, `put()`, `patch()`, or `delete()`, Inertia reads the CSRF token from the `<meta name="csrf-token">` tag in your layout and includes it in the request headers.
327
+
328
+ #### Props Passed to Components
329
+
330
+ Authentication pages receive the following props:
331
+
332
+ | Prop | Type | Description |
333
+ |------|------|-------------|
334
+ | `redirect_uri` | `string` | URL to redirect to after authentication |
335
+ | `connection` | `string \| null` | Social provider connection (if any) |
336
+ | `flash` | `{ notice?: string, alert?: string }` | Flash messages |
337
+ | `social_providers` | `{ google_enabled: boolean, apple_enabled: boolean }` | Available social providers |
338
+ | `errors` | `object` | Validation errors (on form submission failures) |
339
+
340
+ #### Using Authentication in Host App Controllers
341
+
342
+ You can use the `authenticate_account!` method in your own controllers to require authentication with Inertia-compatible redirects:
343
+
344
+ ```ruby
345
+ class DashboardController < ApplicationController
346
+ include StandardId::WebAuthentication
347
+
348
+ before_action :authenticate_account!
349
+
350
+ def show
351
+ # Only authenticated users can access this
352
+ end
353
+ end
354
+ ```
355
+
356
+ This will redirect unauthenticated users to the login page using `inertia_location` for Inertia requests, ensuring proper SPA navigation.
357
+
195
358
  ### Passwordless Authentication
196
359
 
197
360
  ```ruby
@@ -0,0 +1,49 @@
1
+ module StandardId
2
+ module InertiaRendering
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ include StandardId::InertiaSupport
7
+ end
8
+
9
+ private
10
+
11
+ # Render with Inertia if enabled, otherwise use standard Rails rendering
12
+ def render_with_inertia(action: nil, props: {}, component: nil, status: :ok, **options)
13
+ if use_inertia?
14
+ component_name = component || inertia_component_name(action)
15
+ render inertia: component_name, props: props, status: status, **options
16
+ else
17
+ render_options = { status: status }
18
+ render_options[:action] = action if action.present?
19
+ render_options.merge!(options.except(:inertia, :props))
20
+ render(**render_options)
21
+ end
22
+ end
23
+
24
+ # Generate the Inertia component name based on controller and action
25
+ def inertia_component_name(action = nil)
26
+ namespace = StandardId.config.inertia_component_namespace.presence || "standard_id"
27
+ controller_name = self.class.name.demodulize.delete_suffix("Controller")
28
+ action_str = (action || self.action_name).to_s
29
+
30
+ "#{namespace}/#{controller_name}/#{action_str}"
31
+ end
32
+
33
+ # Build common props for authentication pages
34
+ def auth_page_props(additional_props = {})
35
+ {
36
+ redirect_uri: @redirect_uri,
37
+ connection: @connection,
38
+ flash: {
39
+ notice: flash[:notice],
40
+ alert: flash[:alert]
41
+ }.compact,
42
+ social_providers: {
43
+ google_enabled: StandardId.config.google_client_id.present?,
44
+ apple_enabled: StandardId.config.apple_client_id.present?
45
+ }
46
+ }.deep_merge(additional_props)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,31 @@
1
+ module StandardId
2
+ module InertiaSupport
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ helper_method :use_inertia?
7
+ end
8
+
9
+ private
10
+
11
+ # Check if Inertia rendering should be used
12
+ def use_inertia?
13
+ StandardId.config.use_inertia && inertia_available?
14
+ end
15
+
16
+ # Check if inertia_rails gem is available in the host application
17
+ def inertia_available?
18
+ defined?(::InertiaRails)
19
+ end
20
+
21
+ # Redirect to an external URL or non-Inertia endpoint
22
+ # Uses inertia_location for Inertia requests, otherwise standard redirect_to
23
+ def redirect_with_inertia(url, **options)
24
+ if use_inertia? && request.inertia?
25
+ inertia_location url
26
+ else
27
+ redirect_to url, **options
28
+ end
29
+ end
30
+ end
31
+ end
@@ -3,6 +3,7 @@ module StandardId
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
+ include StandardId::InertiaSupport
6
7
  helper_method :current_account, :authenticated?
7
8
  end
8
9
 
@@ -18,6 +19,26 @@ module StandardId
18
19
  authentication_guard.require_session!(session_manager, session: session, request: request)
19
20
  end
20
21
 
22
+ # Require authentication with redirect to login page instead of raising an error.
23
+ # Use this for pages that should redirect unauthenticated users to login.
24
+ def authenticate_account!
25
+ return if authenticated?
26
+
27
+ store_location_for_redirect
28
+ redirect_to_login
29
+ end
30
+
31
+ # Store the current URL to redirect back after authentication
32
+ def store_location_for_redirect
33
+ session[:return_to_after_authenticating] = request.url if request.get?
34
+ end
35
+
36
+ # Redirect to login page, handling both Inertia and standard requests
37
+ def redirect_to_login
38
+ login_path = StandardId.config.login_url.presence || "/login"
39
+ redirect_with_inertia login_path
40
+ end
41
+
21
42
  def after_authentication_url
22
43
  # TODO: add configurable value
23
44
  session.delete(:return_to_after_authenticating) || "/"
@@ -1,6 +1,8 @@
1
1
  module StandardId
2
2
  module Web
3
3
  class LoginController < BaseController
4
+ include StandardId::InertiaRendering
5
+
4
6
  layout "public"
5
7
 
6
8
  skip_before_action :require_browser_session!, only: [:show, :create]
@@ -11,6 +13,8 @@ module StandardId
11
13
  def show
12
14
  @redirect_uri = params[:redirect_uri] || after_authentication_url
13
15
  @connection = params[:connection]
16
+
17
+ render_with_inertia props: auth_page_props
14
18
  end
15
19
 
16
20
  def create
@@ -18,7 +22,7 @@ module StandardId
18
22
  redirect_to params[:redirect_uri] || after_authentication_url, status: :see_other, notice: "Successfully signed in"
19
23
  else
20
24
  flash.now[:alert] = "Invalid email or password"
21
- render :show, status: :unprocessable_content
25
+ render_with_inertia action: :show, props: auth_page_props, status: :unprocessable_content
22
26
  end
23
27
  end
24
28
 
@@ -29,7 +33,7 @@ module StandardId
29
33
  end
30
34
 
31
35
  def redirect_if_social_login
32
- redirect_to social_login_url, allow_other_host: true if params[:connection].present?
36
+ redirect_with_inertia social_login_url, allow_other_host: true if params[:connection].present?
33
37
  end
34
38
 
35
39
  def social_login_url
@@ -1,6 +1,8 @@
1
1
  module StandardId
2
2
  module Web
3
3
  class SignupController < BaseController
4
+ include StandardId::InertiaRendering
5
+
4
6
  layout "public"
5
7
 
6
8
  skip_before_action :require_browser_session!, only: [:show, :create]
@@ -11,6 +13,8 @@ module StandardId
11
13
  def show
12
14
  @redirect_uri = params[:redirect_uri] || after_authentication_url
13
15
  @connection = params[:connection] # For social login detection
16
+
17
+ render_with_inertia props: auth_page_props
14
18
  end
15
19
 
16
20
  def create
@@ -24,7 +28,7 @@ module StandardId
24
28
  end
25
29
 
26
30
  def redirect_if_social_login
27
- redirect_to social_signup_url, allow_other_host: true if params[:connection].present?
31
+ redirect_with_inertia social_signup_url, allow_other_host: true if params[:connection].present?
28
32
  end
29
33
 
30
34
  def handle_password_signup
@@ -35,8 +39,10 @@ module StandardId
35
39
  redirect_to params[:redirect_uri] || after_authentication_url,
36
40
  notice: "Account created successfully"
37
41
  else
42
+ @redirect_uri = params[:redirect_uri] || after_authentication_url
43
+ @connection = params[:connection]
38
44
  flash.now[:alert] = form.errors.full_messages.join(", ")
39
- render :show, status: :unprocessable_content
45
+ render_with_inertia action: :show, props: auth_page_props(errors: form.errors.to_hash), status: :unprocessable_content
40
46
  end
41
47
  end
42
48
 
@@ -7,6 +7,14 @@ StandardId.configure do |c|
7
7
  # c.cache_store = Rails.cache
8
8
  # c.logger = Rails.logger
9
9
  # c.web_layout = "application"
10
+
11
+ # Inertia.js support (requires inertia_rails gem)
12
+ # When enabled, StandardId web controllers will render Inertia components
13
+ # instead of ERB views. You must create the corresponding components in your
14
+ # frontend (e.g., pages/auth/login/show.tsx)
15
+ # c.use_inertia = true
16
+ # c.inertia_component_namespace = "auth" # Component path prefix (e.g., "auth/login/show")
17
+
10
18
  # c.passwordless_email_sender = ->(email, code) { PasswordlessMailer.with(code: code, to: email).deliver_later }
11
19
  # c.passwordless_sms_sender = ->(phone, code) { SmsProvider.send_code(phone: phone, code: code) }
12
20
 
@@ -42,6 +42,14 @@ module StandardConfig
42
42
  # Examples: "application", "standard_id/web/application", "my_custom_layout"
43
43
  attr_accessor :web_layout
44
44
 
45
+ # Enable Inertia.js rendering for StandardId Web controllers
46
+ # When true and inertia_rails gem is available, controllers will render Inertia components
47
+ attr_accessor :use_inertia
48
+
49
+ # Namespace prefix for Inertia component paths
50
+ # Example: "Auth" will generate component paths like "Auth/Login/show"
51
+ attr_accessor :inertia_component_namespace
52
+
45
53
  def initialize
46
54
  @account_class_name = nil
47
55
  @cache_store = nil
@@ -61,6 +69,8 @@ module StandardConfig
61
69
  @passwordless_sms_sender = nil
62
70
  @allowed_post_logout_redirect_uris = []
63
71
  @web_layout = nil
72
+ @use_inertia = nil
73
+ @inertia_component_namespace = nil
64
74
  end
65
75
 
66
76
  def account_class
@@ -14,6 +14,8 @@ StandardConfig.schema.draw do
14
14
  field :issuer, type: :string, default: nil
15
15
  field :login_url, type: :string, default: nil
16
16
  field :allowed_post_logout_redirect_uris, type: :array, default: []
17
+ field :use_inertia, type: :boolean, default: false
18
+ field :inertia_component_namespace, type: :string, default: "standard_id"
17
19
  end
18
20
 
19
21
  scope :passwordless do
@@ -1,3 +1,3 @@
1
1
  module StandardId
2
- VERSION = "0.1.5"
2
+ VERSION = "0.1.6"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard_id
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim
@@ -65,6 +65,8 @@ files:
65
65
  - Rakefile
66
66
  - app/assets/stylesheets/standard_id/application.css
67
67
  - app/controllers/concerns/standard_id/api_authentication.rb
68
+ - app/controllers/concerns/standard_id/inertia_rendering.rb
69
+ - app/controllers/concerns/standard_id/inertia_support.rb
68
70
  - app/controllers/concerns/standard_id/social_authentication.rb
69
71
  - app/controllers/concerns/standard_id/web_authentication.rb
70
72
  - app/controllers/standard_id/api/authorization_controller.rb