standard_id 0.1.0

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 (100) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/stylesheets/standard_id/application.css +15 -0
  6. data/app/controllers/concerns/standard_id/api_authentication.rb +29 -0
  7. data/app/controllers/concerns/standard_id/web_authentication.rb +51 -0
  8. data/app/controllers/standard_id/api/authorization_controller.rb +73 -0
  9. data/app/controllers/standard_id/api/base_controller.rb +61 -0
  10. data/app/controllers/standard_id/api/oauth/base_controller.rb +22 -0
  11. data/app/controllers/standard_id/api/oauth/tokens_controller.rb +44 -0
  12. data/app/controllers/standard_id/api/oidc/logout_controller.rb +50 -0
  13. data/app/controllers/standard_id/api/passwordless_controller.rb +38 -0
  14. data/app/controllers/standard_id/api/providers_controller.rb +175 -0
  15. data/app/controllers/standard_id/api/userinfo_controller.rb +36 -0
  16. data/app/controllers/standard_id/web/account_controller.rb +32 -0
  17. data/app/controllers/standard_id/web/auth/callback/providers_controller.rb +126 -0
  18. data/app/controllers/standard_id/web/base_controller.rb +14 -0
  19. data/app/controllers/standard_id/web/login_controller.rb +69 -0
  20. data/app/controllers/standard_id/web/logout_controller.rb +20 -0
  21. data/app/controllers/standard_id/web/reset_password/confirm_controller.rb +46 -0
  22. data/app/controllers/standard_id/web/reset_password/start_controller.rb +27 -0
  23. data/app/controllers/standard_id/web/sessions_controller.rb +26 -0
  24. data/app/controllers/standard_id/web/signup_controller.rb +83 -0
  25. data/app/forms/standard_id/web/reset_password_confirm_form.rb +37 -0
  26. data/app/forms/standard_id/web/reset_password_start_form.rb +38 -0
  27. data/app/forms/standard_id/web/signup_form.rb +65 -0
  28. data/app/helpers/standard_id/application_helper.rb +4 -0
  29. data/app/jobs/standard_id/application_job.rb +4 -0
  30. data/app/mailers/standard_id/application_mailer.rb +6 -0
  31. data/app/models/concerns/standard_id/account_associations.rb +14 -0
  32. data/app/models/concerns/standard_id/credentiable.rb +12 -0
  33. data/app/models/standard_id/application_record.rb +5 -0
  34. data/app/models/standard_id/authorization_code.rb +86 -0
  35. data/app/models/standard_id/browser_session.rb +27 -0
  36. data/app/models/standard_id/client_application.rb +143 -0
  37. data/app/models/standard_id/client_secret_credential.rb +63 -0
  38. data/app/models/standard_id/credential.rb +16 -0
  39. data/app/models/standard_id/device_session.rb +38 -0
  40. data/app/models/standard_id/email_identifier.rb +5 -0
  41. data/app/models/standard_id/identifier.rb +25 -0
  42. data/app/models/standard_id/password_credential.rb +24 -0
  43. data/app/models/standard_id/passwordless_challenge.rb +30 -0
  44. data/app/models/standard_id/phone_number_identifier.rb +5 -0
  45. data/app/models/standard_id/service_session.rb +44 -0
  46. data/app/models/standard_id/session.rb +54 -0
  47. data/app/models/standard_id/username_identifier.rb +5 -0
  48. data/app/views/standard_id/web/account/edit.html.erb +26 -0
  49. data/app/views/standard_id/web/account/show.html.erb +31 -0
  50. data/app/views/standard_id/web/login/show.html.erb +108 -0
  51. data/app/views/standard_id/web/reset_password/confirm/show.html.erb +27 -0
  52. data/app/views/standard_id/web/reset_password/start/show.html.erb +20 -0
  53. data/app/views/standard_id/web/sessions/index.html.erb +112 -0
  54. data/app/views/standard_id/web/signup/show.html.erb +96 -0
  55. data/config/initializers/generators.rb +9 -0
  56. data/config/initializers/migration_helpers.rb +32 -0
  57. data/config/routes/api.rb +24 -0
  58. data/config/routes/web.rb +26 -0
  59. data/db/migrate/20250830000000_create_standard_id_client_applications.rb +56 -0
  60. data/db/migrate/20250830171553_create_standard_id_password_credentials.rb +10 -0
  61. data/db/migrate/20250830232800_create_standard_id_identifiers.rb +17 -0
  62. data/db/migrate/20250831075703_create_standard_id_credentials.rb +10 -0
  63. data/db/migrate/20250831154635_create_standard_id_sessions.rb +43 -0
  64. data/db/migrate/20250901134520_create_standard_id_client_secret_credentials.rb +20 -0
  65. data/db/migrate/20250903063000_create_standard_id_authorization_codes.rb +46 -0
  66. data/db/migrate/20250903135906_create_standard_id_passwordless_challenges.rb +22 -0
  67. data/lib/generators/standard_id/install/install_generator.rb +14 -0
  68. data/lib/generators/standard_id/install/templates/standard_id.rb +11 -0
  69. data/lib/standard_id/api/authentication_guard.rb +20 -0
  70. data/lib/standard_id/api/session_manager.rb +39 -0
  71. data/lib/standard_id/api/token_manager.rb +50 -0
  72. data/lib/standard_id/api_engine.rb +7 -0
  73. data/lib/standard_id/config.rb +69 -0
  74. data/lib/standard_id/engine.rb +5 -0
  75. data/lib/standard_id/errors.rb +55 -0
  76. data/lib/standard_id/jwt_service.rb +50 -0
  77. data/lib/standard_id/oauth/authorization_code_authorization_flow.rb +47 -0
  78. data/lib/standard_id/oauth/authorization_code_flow.rb +53 -0
  79. data/lib/standard_id/oauth/authorization_flow.rb +91 -0
  80. data/lib/standard_id/oauth/base_request_flow.rb +43 -0
  81. data/lib/standard_id/oauth/client_credentials_flow.rb +38 -0
  82. data/lib/standard_id/oauth/implicit_authorization_flow.rb +79 -0
  83. data/lib/standard_id/oauth/password_flow.rb +70 -0
  84. data/lib/standard_id/oauth/passwordless_otp_flow.rb +87 -0
  85. data/lib/standard_id/oauth/refresh_token_flow.rb +61 -0
  86. data/lib/standard_id/oauth/subflows/base.rb +19 -0
  87. data/lib/standard_id/oauth/subflows/social_login_grant.rb +66 -0
  88. data/lib/standard_id/oauth/subflows/traditional_code_grant.rb +52 -0
  89. data/lib/standard_id/oauth/token_grant_flow.rb +107 -0
  90. data/lib/standard_id/passwordless/base_strategy.rb +67 -0
  91. data/lib/standard_id/passwordless/email_strategy.rb +27 -0
  92. data/lib/standard_id/passwordless/sms_strategy.rb +29 -0
  93. data/lib/standard_id/version.rb +3 -0
  94. data/lib/standard_id/web/authentication_guard.rb +23 -0
  95. data/lib/standard_id/web/session_manager.rb +71 -0
  96. data/lib/standard_id/web/token_manager.rb +30 -0
  97. data/lib/standard_id/web_engine.rb +7 -0
  98. data/lib/standard_id.rb +49 -0
  99. data/lib/tasks/standard_id_tasks.rake +4 -0
  100. metadata +186 -0
@@ -0,0 +1,32 @@
1
+ module StandardId
2
+ module Web
3
+ class AccountController < BaseController
4
+ def show
5
+ @account = current_account
6
+ @sessions = current_account.sessions.active.order(created_at: :desc)
7
+ end
8
+
9
+ def edit
10
+ @account = current_account
11
+ end
12
+
13
+ def update
14
+ @account = current_account
15
+
16
+ if @account.update(account_params)
17
+ redirect_to account_path, notice: "Account updated successfully"
18
+ else
19
+ flash.now[:alert] = @account.errors.full_messages.join(", ")
20
+ render :edit, status: :unprocessable_content
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def account_params
27
+ # Add account fields as they're defined in the Account model
28
+ params.require(:account).permit()
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,126 @@
1
+ module StandardId
2
+ module Web
3
+ module Auth
4
+ module Callback
5
+ class ProvidersController < StandardId::Web::BaseController
6
+ include StandardId::WebAuthentication
7
+
8
+ # Social callbacks must be accessible without an existing browser session
9
+ # because they create/sign-in the session upon successful callback.
10
+ skip_before_action :require_browser_session!, only: [:google, :apple]
11
+
12
+ def google
13
+ handle_social_callback("google-oauth2")
14
+ end
15
+
16
+ def apple
17
+ handle_social_callback("apple")
18
+ end
19
+
20
+ private
21
+
22
+ def handle_social_callback(provider)
23
+ # This handles the callback from social providers in web context
24
+ # After successful social auth, we need to:
25
+ # 1. Extract user info from the callback
26
+ # 2. Create/find account
27
+ # 3. Sign in the user with web session
28
+ # 4. Redirect to original destination
29
+
30
+ if params[:error].present?
31
+ handle_callback_error
32
+ return
33
+ end
34
+
35
+ begin
36
+ user_info = extract_user_info(provider)
37
+ account = find_or_create_account_from_social(user_info, provider)
38
+ session_manager.sign_in_account(account)
39
+
40
+ redirect_to decode_redirect_uri, notice: "Successfully signed in with #{provider.humanize}"
41
+ rescue StandardId::OAuthError => e
42
+ redirect_to login_path, alert: "Authentication failed: #{e.message}"
43
+ end
44
+ end
45
+
46
+ def extract_user_info(provider)
47
+ case provider
48
+ when "google-oauth2"
49
+ extract_google_user_info
50
+ when "apple"
51
+ extract_apple_user_info
52
+ else
53
+ raise StandardId::InvalidRequestError, "Unsupported connection/provider: #{provider}"
54
+ end
55
+ end
56
+
57
+ def extract_google_user_info
58
+ # Exchange code for Google user info
59
+ # This would integrate with Google OAuth API
60
+ {
61
+ email: params[:email] || "user@example.com", # Placeholder
62
+ name: params[:name] || "Google User",
63
+ provider: "google-oauth2",
64
+ provider_id: params[:sub] || "google_123"
65
+ }
66
+ end
67
+
68
+ def extract_apple_user_info
69
+ # Extract user info from Apple Sign In callback
70
+ # This would decode the Apple ID token
71
+ {
72
+ email: params[:email] || "user@privaterelay.appleid.com", # Placeholder
73
+ name: params[:name] || "Apple User",
74
+ provider: "apple",
75
+ provider_id: params[:sub] || "apple_123"
76
+ }
77
+ end
78
+
79
+ def find_or_create_account_from_social(user_info, provider)
80
+ # Find existing account by email or create new one
81
+ identifier = StandardId::EmailIdentifier.find_by(value: user_info[:email])
82
+
83
+ if identifier
84
+ identifier.account
85
+ else
86
+ # Create new account with social login
87
+ account = ::Account.create!(
88
+ email: user_info[:email],
89
+ name: user_info[:name].presence || "User"
90
+ )
91
+ StandardId::EmailIdentifier.create!(
92
+ account: account,
93
+ value: user_info[:email]
94
+ )
95
+ account
96
+ end
97
+ end
98
+
99
+ def decode_redirect_uri
100
+ return "/" unless params[:state].present?
101
+
102
+ begin
103
+ state_data = JSON.parse(Base64.urlsafe_decode64(params[:state]))
104
+ state_data["redirect_uri"] || "/"
105
+ rescue JSON::ParserError, ArgumentError
106
+ "/"
107
+ end
108
+ end
109
+
110
+ def handle_callback_error
111
+ error_message = case params[:error]
112
+ when "access_denied"
113
+ "Authentication was cancelled"
114
+ when "invalid_request"
115
+ "Invalid authentication request"
116
+ else
117
+ "Authentication failed"
118
+ end
119
+
120
+ redirect_to login_path, alert: error_message
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,14 @@
1
+ module StandardId
2
+ module Web
3
+ class BaseController < ApplicationController
4
+ include StandardId::WebAuthentication
5
+
6
+ include StandardId::WebEngine.routes.url_helpers
7
+ helper StandardId::WebEngine.routes.url_helpers
8
+
9
+ layout -> { StandardId.config.web_layout.presence || "application" }
10
+
11
+ before_action :require_browser_session!
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,69 @@
1
+ module StandardId
2
+ module Web
3
+ class LoginController < BaseController
4
+ layout "public"
5
+
6
+ skip_before_action :require_browser_session!, only: [:show, :create]
7
+
8
+ before_action :redirect_if_authenticated, only: [:show]
9
+ before_action :redirect_if_social_login, only: [:create]
10
+
11
+ def show
12
+ @redirect_uri = params[:redirect_uri] || after_authentication_url
13
+ @connection = params[:connection]
14
+ end
15
+
16
+ def create
17
+ if sign_in_account(login_params)
18
+ redirect_to params[:redirect_uri] || after_authentication_url, status: :see_other, notice: "Successfully signed in"
19
+ else
20
+ flash.now[:alert] = "Invalid email or password"
21
+ render :show, status: :unprocessable_content
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def redirect_if_authenticated
28
+ redirect_to after_authentication_url, status: :see_other, notice: "You are already signed in" if authenticated?
29
+ end
30
+
31
+ def redirect_if_social_login
32
+ redirect_to social_login_url, allow_other_host: true if params[:connection].present?
33
+ end
34
+
35
+ def social_login_url
36
+ uri = URI.parse("/api/authorize")
37
+ query = {
38
+ response_type: "code",
39
+ client_id: StandardId.config.default_client_id,
40
+ redirect_uri: callback_url,
41
+ connection: params[:connection],
42
+ state: encode_state
43
+ }.to_query
44
+ uri.query = query
45
+ uri.to_s
46
+ end
47
+
48
+ def callback_url
49
+ case params[:connection]
50
+ when "google-oauth2"
51
+ auth_callback_google_path
52
+ when "apple"
53
+ auth_callback_apple_path
54
+ end
55
+ end
56
+
57
+ def encode_state
58
+ Base64.urlsafe_encode64({
59
+ redirect_uri: params[:redirect_uri] || after_authentication_url,
60
+ timestamp: Time.current.to_i
61
+ }.to_json)
62
+ end
63
+
64
+ def login_params
65
+ params.require(:login).permit(:email, :password, :remember_me)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,20 @@
1
+ module StandardId
2
+ module Web
3
+ class LogoutController < BaseController
4
+ skip_before_action :require_browser_session!, only: [:create]
5
+
6
+ before_action :redirect_if_not_authenticated
7
+
8
+ def create
9
+ revoke_current_session!
10
+ redirect_to params[:redirect_uri] || root_path, notice: "Successfully signed out"
11
+ end
12
+
13
+ private
14
+
15
+ def redirect_if_not_authenticated
16
+ redirect_to params[:redirect_uri] || root_path unless authenticated?
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,46 @@
1
+ module StandardId
2
+ module Web
3
+ module ResetPassword
4
+ class ConfirmController < BaseController
5
+ layout "public"
6
+
7
+ skip_before_action :require_browser_session!, only: [:show, :update]
8
+
9
+ before_action :prepare_password_credential, only: [:show, :update]
10
+ before_action :redirect_if_token_invalid, only: [:show, :update]
11
+
12
+ def show
13
+ end
14
+
15
+ def update
16
+ form = StandardId::Web::ResetPasswordConfirmForm.new(@password_credential, reset_password_confirm_form_params)
17
+
18
+ if form.submit
19
+ flash[:notice] = "Your password has been successfully reset. Please sign in with your new password."
20
+ redirect_to login_path, status: :see_other
21
+ else
22
+ flash.now[:alert] = form.errors.full_messages.to_sentence
23
+ render :show, status: :unprocessable_content
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def prepare_password_credential
30
+ @password_credential = StandardId::PasswordCredential.find_by_token_for(:password_reset, params[:token])
31
+ end
32
+
33
+ def reset_password_confirm_form_params
34
+ params.permit(:password, :password_confirmation)
35
+ end
36
+
37
+ def redirect_if_token_invalid
38
+ return if @password_credential.present?
39
+
40
+ flash[:alert] = "Invalid or expired password reset link"
41
+ redirect_to login_path, status: :see_other
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,27 @@
1
+ module StandardId
2
+ module Web
3
+ module ResetPassword
4
+ class StartController < BaseController
5
+ layout "public"
6
+
7
+ skip_before_action :require_browser_session!, only: [:show, :create]
8
+
9
+ def show
10
+ # Display the password reset request form
11
+ end
12
+
13
+ def create
14
+ form = StandardId::Web::ResetPasswordStartForm.new(email: params[:email])
15
+
16
+ if form.submit
17
+ flash[:notice] = "If an account with that email exists, we've sent password reset instructions."
18
+ redirect_to login_path, status: :see_other
19
+ else
20
+ flash.now[:alert] = form.errors[:email].first || "Please enter your email address"
21
+ render :show, status: :unprocessable_content
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ module StandardId
2
+ module Web
3
+ class SessionsController < BaseController
4
+ def index
5
+ @sessions = current_account.sessions.active.order(created_at: :desc)
6
+ @current_session = current_session
7
+ end
8
+
9
+ def destroy
10
+ session = current_account.sessions.find(params[:id])
11
+
12
+ if session == current_session
13
+ # If revoking current session, sign out and redirect
14
+ revoke_current_session!
15
+ redirect_to "/", notice: "Session revoked. You have been signed out."
16
+ else
17
+ # Revoke other session
18
+ session.revoke!
19
+ redirect_to sessions_path, notice: "Session revoked successfully"
20
+ end
21
+ rescue ActiveRecord::RecordNotFound
22
+ redirect_to sessions_path, alert: "Session not found"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,83 @@
1
+ module StandardId
2
+ module Web
3
+ class SignupController < BaseController
4
+ layout "public"
5
+
6
+ skip_before_action :require_browser_session!, only: [:show, :create]
7
+
8
+ before_action :redirect_if_authenticated, only: [:show]
9
+ before_action :redirect_if_social_login, only: [:create]
10
+
11
+ def show
12
+ @redirect_uri = params[:redirect_uri] || after_authentication_url
13
+ @connection = params[:connection] # For social login detection
14
+ end
15
+
16
+ def create
17
+ handle_password_signup
18
+ end
19
+
20
+ private
21
+
22
+ def redirect_if_authenticated
23
+ redirect_to after_authentication_url if authenticated?
24
+ end
25
+
26
+ def redirect_if_social_login
27
+ redirect_to social_signup_url, allow_other_host: true if params[:connection].present?
28
+ end
29
+
30
+ def handle_password_signup
31
+ form = StandardId::Web::SignupForm.new(signup_params)
32
+
33
+ if form.submit
34
+ session_manager.sign_in_account(form.account)
35
+ redirect_to params[:redirect_uri] || after_authentication_url,
36
+ notice: "Account created successfully"
37
+ else
38
+ flash.now[:alert] = form.errors.full_messages.join(", ")
39
+ render :show, status: :unprocessable_content
40
+ end
41
+ end
42
+
43
+ def social_signup_url
44
+ # Same as login - social providers handle signup/login automatically
45
+ uri = URI.parse("/api/authorize")
46
+ query = {
47
+ response_type: "code",
48
+ client_id: StandardId.config.default_client_id,
49
+ redirect_uri: callback_url,
50
+ connection: params[:connection],
51
+ state: encode_state
52
+ }.to_query
53
+ uri.query = query
54
+ uri.to_s
55
+ end
56
+
57
+ def callback_url
58
+ case params[:connection]
59
+ when "google-oauth2"
60
+ auth_callback_google_url
61
+ when "apple"
62
+ auth_callback_apple_url
63
+ end
64
+ end
65
+
66
+ def encode_state
67
+ Base64.urlsafe_encode64({
68
+ redirect_uri: params[:redirect_uri] || after_authentication_url,
69
+ timestamp: Time.current.to_i
70
+ }.to_json)
71
+ end
72
+
73
+ def account_params
74
+ # Add any additional account fields as needed
75
+ {}
76
+ end
77
+
78
+ def signup_params
79
+ params.require(:signup).permit(:email, :password, :password_confirmation)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,37 @@
1
+ module StandardId
2
+ module Web
3
+ class ResetPasswordConfirmForm
4
+ include ActiveModel::Model
5
+ include ActiveModel::Attributes
6
+
7
+ attribute :password, :string
8
+ attribute :password_confirmation, :string
9
+
10
+ attr_reader :password_credential
11
+
12
+ validates :password,
13
+ presence: { message: "cannot be blank" },
14
+ length: { minimum: 8, too_short: "must be at least 8 characters long" },
15
+ confirmation: { message: "confirmation doesn't match" }
16
+
17
+ def initialize(password_credential, params = {})
18
+ @password_credential = password_credential
19
+ super(params)
20
+ end
21
+
22
+ def submit
23
+ return false unless valid?
24
+
25
+ ActiveRecord::Base.transaction do
26
+ @password_credential.update!(password: password, password_confirmation: password_confirmation)
27
+ @password_credential.account.sessions.destroy_all
28
+ end
29
+
30
+ true
31
+ rescue ActiveRecord::RecordInvalid => e
32
+ errors.add(:base, e.record.errors.full_messages.join(", "))
33
+ false
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,38 @@
1
+ module StandardId
2
+ module Web
3
+ class ResetPasswordStartForm
4
+ include ActiveModel::Model
5
+ include ActiveModel::Attributes
6
+
7
+ attribute :email, :string
8
+
9
+ attr_reader :password_credential, :token
10
+
11
+ validates :email, presence: { message: "Please enter your email address" }, format: { with: URI::MailTo::EMAIL_REGEXP }
12
+
13
+ def submit
14
+ return false unless valid?
15
+
16
+ if token.present?
17
+ # TODO: send reset link via email
18
+ end
19
+
20
+ true
21
+ end
22
+
23
+ def password_credential
24
+ @password_credential ||= identifier&.account&.credentials&.where(credentialable_type: "StandardId::PasswordCredential")&.sole&.credentialable
25
+ end
26
+
27
+ def token
28
+ @token ||= password_credential&.generate_token_for(:password_reset)
29
+ end
30
+
31
+ private
32
+
33
+ def identifier
34
+ @identifier ||= StandardId::EmailIdentifier.find_by(value: email.to_s.strip.downcase)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,65 @@
1
+ module StandardId
2
+ module Web
3
+ class SignupForm
4
+ include ActiveModel::Model
5
+ include ActiveModel::Attributes
6
+
7
+ attribute :email, :string
8
+ attribute :password, :string
9
+ attribute :password_confirmation, :string
10
+
11
+ attr_reader :account
12
+
13
+ validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
14
+ validates :password, presence: true, length: { minimum: 8 }, confirmation: true
15
+
16
+ def submit
17
+ return false unless valid?
18
+
19
+ ActiveRecord::Base.transaction do
20
+ @account = Account.create!(account_params)
21
+ StandardId::PasswordCredential.create!(
22
+ password_credential_params.merge(
23
+ credential_attributes: {
24
+ identifier_attributes: email_identifier_params.merge(account: @account)
25
+ }
26
+ )
27
+ )
28
+ end
29
+
30
+ true
31
+ rescue ActiveRecord::RecordInvalid => e
32
+ errors.add(:base, e.record.errors.full_messages.join(", "))
33
+ false
34
+ rescue ActiveRecord::RecordNotUnique => e
35
+ errors.add(:base, e.message)
36
+ false
37
+ end
38
+
39
+ private
40
+
41
+ def account_params
42
+ # Placeholder for account fields. Add/permit additional attributes as needed.
43
+ {
44
+ name: (email.to_s.split("@").first.presence || "User"),
45
+ email: email
46
+ }
47
+ end
48
+
49
+ def email_identifier_params
50
+ {
51
+ value: email,
52
+ verified_at: Time.current,
53
+ type: "StandardId::EmailIdentifier"
54
+ }
55
+ end
56
+
57
+ def password_credential_params
58
+ {
59
+ login: email,
60
+ password: password
61
+ }
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,4 @@
1
+ module StandardId
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module StandardId
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module StandardId
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,14 @@
1
+ module StandardId
2
+ module AccountAssociations
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ has_many :identifiers, class_name: "StandardId::Identifier", dependent: :restrict_with_exception
7
+ has_many :credentials, class_name: "StandardId::Credential", through: :identifiers, source: :credentials, dependent: :restrict_with_exception
8
+ has_many :sessions, class_name: "StandardId::Session", dependent: :restrict_with_exception
9
+ has_many :client_applications, class_name: "StandardId::ClientApplication", as: :owner, dependent: :restrict_with_exception
10
+
11
+ accepts_nested_attributes_for :identifiers
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ module StandardId
2
+ module Credentiable
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ has_one :credential, as: :credentialable, touch: true
7
+ accepts_nested_attributes_for :credential
8
+
9
+ delegate :account, to: :credential
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ module StandardId
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end