Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add after_session_confirm hook after session confirmation #237

Merged
merged 7 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,51 @@ Passwordless.configure do |config|
config.sign_out_redirect_path = '/' # After a user signs out

config.paranoid = false # Display email sent notice even when the resource is not found.

config.after_session_confirm = ->(session) {} # Called after a session is confirmed.
mikker marked this conversation as resolved.
Show resolved Hide resolved
end
```

## After Session Confirmation Hook
Passwordless now supports an after_session_confirm hook that allows you to perform custom actions after a session has been successfully confirmed. This is particularly useful for tasks such as marking an email as verified or updating user attributes after their first successful login.
mikker marked this conversation as resolved.
Show resolved Hide resolved

#### Configuration
You can configure the after_session_confirm hook in your Passwordless configuration:

```ruby
Passwordless.configure do |config|
# ... other configuration options ...

config.after_session_confirm = ->(session) {
# Your custom logic here
user = session.authenticatable
user.update(email_verified: true)
}
end
```

#### Usage
The after_session_confirm hook is called automatically after a successful session confirmation. It receives the session object as an argument, which you can use to access the authenticated user (via session.authenticatable) and perform any necessary operations.

**Example: Marking an Email as Verified**

```ruby
Passwordless.configure do |config|
config.after_session_confirm = ->(session) {
user = session.authenticatable
user.update(email_verified: true)
}
end
```

**Example: Updating Last Login Timestamp**

```ruby
Passwordless.configure do |config|
config.after_session_confirm = ->(session) {
user = session.authenticatable
user.update(last_login_at: Time.current)
}
end
```

Expand Down
11 changes: 11 additions & 0 deletions app/controllers/passwordless/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ def artificially_slow_down_brute_force_attacks(token)
def authenticate_and_sign_in(session, token)
if session.authenticate(token)
sign_in(session)
call_after_session_confirm(session)
redirect_to(
passwordless_success_redirect_path(session.authenticatable),
status: :see_other,
Expand Down Expand Up @@ -188,6 +189,16 @@ def call_or_return(value, *args)
end
end

def call_after_session_confirm(session)
return unless Passwordless.config.after_session_confirm.respond_to?(:call)

if Passwordless.config.after_session_confirm.arity == 2
Passwordless.config.after_session_confirm.call(session, request)
else
Passwordless.config.after_session_confirm.call(session)
end
end

mikker marked this conversation as resolved.
Show resolved Hide resolved
def find_authenticatable
if authenticatable_class.respond_to?(:fetch_resource_for_passwordless)
authenticatable_class.fetch_resource_for_passwordless(normalized_email_param)
Expand Down
64 changes: 64 additions & 0 deletions test/controllers/passwordless/sessions_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,70 @@ class << User
assert_nil pwless_session(User)
end

test("PATCH /:passwordless_for/sign_in/:id -> SUCCESS with after_session_confirm") do
user = create_user(email: "[email protected]")
passwordless_session = create_pwless_session(authenticatable: user, token: "valid_token")

confirm_called = false

with_config(after_session_confirm: ->(session) {
confirm_called = true
assert_equal user, session.authenticatable
}) do
patch(
"/users/sign_in/#{passwordless_session.identifier}",
params: {passwordless: {token: "valid_token"}}
)
end

assert_equal 303, status
assert confirm_called, "after_session_confirm hook was not called"

follow_redirect!
assert_equal 200, status
assert_equal "/", path

assert_equal pwless_session(User), Session.last!.id
end

test("PATCH /:passwordless_for/sign_in/:id -> after_session_confirm with request object") do
user = create_user(email: "[email protected]")
passwordless_session = create_pwless_session(authenticatable: user, token: "valid_token")

confirm_called = false

with_config(after_session_confirm: ->(session, request) {
confirm_called = true
assert_equal user, session.authenticatable
assert_kind_of ActionDispatch::Request, request
}) do
patch(
"/users/sign_in/#{passwordless_session.identifier}",
params: {passwordless: {token: "valid_token"}}
)
end

assert_equal 303, status
assert confirm_called, "after_session_confirm hook was not called"
end

test("PATCH /:passwordless_for/sign_in/:id -> after_session_confirm not called on invalid token") do
user = create_user(email: "[email protected]")
passwordless_session = create_pwless_session(authenticatable: user, token: "valid_token")

confirm_called = false

with_config(after_session_confirm: ->(_) { confirm_called = true }) do
patch(
"/users/sign_in/#{passwordless_session.identifier}",
params: {passwordless: {token: "invalid_token"}}
)
end

assert_equal 403, status
assert_not confirm_called, "after_session_confirm hook was called with invalid token"
end

test("DELETE /:passwordless_for/sign_out") do
user = User.create(email: "a@a")
passwordless_session = create_pwless_session(authenticatable: user, token: "hi")
Expand Down