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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +8 -0
- data/app/assets/stylesheets/standard_id/application.css +15 -0
- data/app/controllers/concerns/standard_id/api_authentication.rb +29 -0
- data/app/controllers/concerns/standard_id/web_authentication.rb +51 -0
- data/app/controllers/standard_id/api/authorization_controller.rb +73 -0
- data/app/controllers/standard_id/api/base_controller.rb +61 -0
- data/app/controllers/standard_id/api/oauth/base_controller.rb +22 -0
- data/app/controllers/standard_id/api/oauth/tokens_controller.rb +44 -0
- data/app/controllers/standard_id/api/oidc/logout_controller.rb +50 -0
- data/app/controllers/standard_id/api/passwordless_controller.rb +38 -0
- data/app/controllers/standard_id/api/providers_controller.rb +175 -0
- data/app/controllers/standard_id/api/userinfo_controller.rb +36 -0
- data/app/controllers/standard_id/web/account_controller.rb +32 -0
- data/app/controllers/standard_id/web/auth/callback/providers_controller.rb +126 -0
- data/app/controllers/standard_id/web/base_controller.rb +14 -0
- data/app/controllers/standard_id/web/login_controller.rb +69 -0
- data/app/controllers/standard_id/web/logout_controller.rb +20 -0
- data/app/controllers/standard_id/web/reset_password/confirm_controller.rb +46 -0
- data/app/controllers/standard_id/web/reset_password/start_controller.rb +27 -0
- data/app/controllers/standard_id/web/sessions_controller.rb +26 -0
- data/app/controllers/standard_id/web/signup_controller.rb +83 -0
- data/app/forms/standard_id/web/reset_password_confirm_form.rb +37 -0
- data/app/forms/standard_id/web/reset_password_start_form.rb +38 -0
- data/app/forms/standard_id/web/signup_form.rb +65 -0
- data/app/helpers/standard_id/application_helper.rb +4 -0
- data/app/jobs/standard_id/application_job.rb +4 -0
- data/app/mailers/standard_id/application_mailer.rb +6 -0
- data/app/models/concerns/standard_id/account_associations.rb +14 -0
- data/app/models/concerns/standard_id/credentiable.rb +12 -0
- data/app/models/standard_id/application_record.rb +5 -0
- data/app/models/standard_id/authorization_code.rb +86 -0
- data/app/models/standard_id/browser_session.rb +27 -0
- data/app/models/standard_id/client_application.rb +143 -0
- data/app/models/standard_id/client_secret_credential.rb +63 -0
- data/app/models/standard_id/credential.rb +16 -0
- data/app/models/standard_id/device_session.rb +38 -0
- data/app/models/standard_id/email_identifier.rb +5 -0
- data/app/models/standard_id/identifier.rb +25 -0
- data/app/models/standard_id/password_credential.rb +24 -0
- data/app/models/standard_id/passwordless_challenge.rb +30 -0
- data/app/models/standard_id/phone_number_identifier.rb +5 -0
- data/app/models/standard_id/service_session.rb +44 -0
- data/app/models/standard_id/session.rb +54 -0
- data/app/models/standard_id/username_identifier.rb +5 -0
- data/app/views/standard_id/web/account/edit.html.erb +26 -0
- data/app/views/standard_id/web/account/show.html.erb +31 -0
- data/app/views/standard_id/web/login/show.html.erb +108 -0
- data/app/views/standard_id/web/reset_password/confirm/show.html.erb +27 -0
- data/app/views/standard_id/web/reset_password/start/show.html.erb +20 -0
- data/app/views/standard_id/web/sessions/index.html.erb +112 -0
- data/app/views/standard_id/web/signup/show.html.erb +96 -0
- data/config/initializers/generators.rb +9 -0
- data/config/initializers/migration_helpers.rb +32 -0
- data/config/routes/api.rb +24 -0
- data/config/routes/web.rb +26 -0
- data/db/migrate/20250830000000_create_standard_id_client_applications.rb +56 -0
- data/db/migrate/20250830171553_create_standard_id_password_credentials.rb +10 -0
- data/db/migrate/20250830232800_create_standard_id_identifiers.rb +17 -0
- data/db/migrate/20250831075703_create_standard_id_credentials.rb +10 -0
- data/db/migrate/20250831154635_create_standard_id_sessions.rb +43 -0
- data/db/migrate/20250901134520_create_standard_id_client_secret_credentials.rb +20 -0
- data/db/migrate/20250903063000_create_standard_id_authorization_codes.rb +46 -0
- data/db/migrate/20250903135906_create_standard_id_passwordless_challenges.rb +22 -0
- data/lib/generators/standard_id/install/install_generator.rb +14 -0
- data/lib/generators/standard_id/install/templates/standard_id.rb +11 -0
- data/lib/standard_id/api/authentication_guard.rb +20 -0
- data/lib/standard_id/api/session_manager.rb +39 -0
- data/lib/standard_id/api/token_manager.rb +50 -0
- data/lib/standard_id/api_engine.rb +7 -0
- data/lib/standard_id/config.rb +69 -0
- data/lib/standard_id/engine.rb +5 -0
- data/lib/standard_id/errors.rb +55 -0
- data/lib/standard_id/jwt_service.rb +50 -0
- data/lib/standard_id/oauth/authorization_code_authorization_flow.rb +47 -0
- data/lib/standard_id/oauth/authorization_code_flow.rb +53 -0
- data/lib/standard_id/oauth/authorization_flow.rb +91 -0
- data/lib/standard_id/oauth/base_request_flow.rb +43 -0
- data/lib/standard_id/oauth/client_credentials_flow.rb +38 -0
- data/lib/standard_id/oauth/implicit_authorization_flow.rb +79 -0
- data/lib/standard_id/oauth/password_flow.rb +70 -0
- data/lib/standard_id/oauth/passwordless_otp_flow.rb +87 -0
- data/lib/standard_id/oauth/refresh_token_flow.rb +61 -0
- data/lib/standard_id/oauth/subflows/base.rb +19 -0
- data/lib/standard_id/oauth/subflows/social_login_grant.rb +66 -0
- data/lib/standard_id/oauth/subflows/traditional_code_grant.rb +52 -0
- data/lib/standard_id/oauth/token_grant_flow.rb +107 -0
- data/lib/standard_id/passwordless/base_strategy.rb +67 -0
- data/lib/standard_id/passwordless/email_strategy.rb +27 -0
- data/lib/standard_id/passwordless/sms_strategy.rb +29 -0
- data/lib/standard_id/version.rb +3 -0
- data/lib/standard_id/web/authentication_guard.rb +23 -0
- data/lib/standard_id/web/session_manager.rb +71 -0
- data/lib/standard_id/web/token_manager.rb +30 -0
- data/lib/standard_id/web_engine.rb +7 -0
- data/lib/standard_id.rb +49 -0
- data/lib/tasks/standard_id_tasks.rake +4 -0
- 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,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
|