Add remember me checkbox, center and narrow sign-in form
- Add "Remember me for 30 days" checkbox (30-day vs 24-hour session expiry) - Center heading and constrain form width to max-w-md - Preserve remember_me preference through TOTP and WebAuthn auth flows Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -43,9 +43,9 @@ module Authentication
|
||||
session.delete(:return_to_after_authenticating) || root_url
|
||||
end
|
||||
|
||||
def start_new_session_for(user, acr: "1")
|
||||
def start_new_session_for(user, acr: "1", remember_me: false)
|
||||
user.update!(last_sign_in_at: Time.current)
|
||||
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip, acr: acr).tap do |session|
|
||||
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip, acr: acr, remember_me: remember_me).tap do |session|
|
||||
Current.session = session
|
||||
|
||||
# Extract root domain for cross-subdomain cookies (required for forward auth)
|
||||
|
||||
@@ -76,6 +76,7 @@ class SessionsController < ApplicationController
|
||||
# TOTP is enabled, proceed to verification
|
||||
# Store user ID in session temporarily for TOTP verification
|
||||
session[:pending_totp_user_id] = user.id
|
||||
session[:pending_remember_me] = remember_me?
|
||||
# Preserve the redirect URL through TOTP verification (after validation)
|
||||
if params[:rd].present?
|
||||
validated_url = validate_redirect_url(params[:rd])
|
||||
@@ -86,7 +87,7 @@ class SessionsController < ApplicationController
|
||||
end
|
||||
|
||||
# Sign in successful (password only)
|
||||
start_new_session_for user, acr: "1"
|
||||
start_new_session_for user, acr: "1", remember_me: remember_me?
|
||||
|
||||
# Use status: :see_other to ensure browser makes a GET request
|
||||
# This prevents Turbo from converting it to a TURBO_STREAM request
|
||||
@@ -118,6 +119,8 @@ class SessionsController < ApplicationController
|
||||
return
|
||||
end
|
||||
|
||||
remember_me = session.delete(:pending_remember_me) || false
|
||||
|
||||
# Try TOTP verification first (password + TOTP = 2FA)
|
||||
if user.verify_totp(code)
|
||||
session.delete(:pending_totp_user_id)
|
||||
@@ -125,7 +128,7 @@ class SessionsController < ApplicationController
|
||||
if session[:totp_redirect_url].present?
|
||||
session[:return_to_after_authenticating] = session.delete(:totp_redirect_url)
|
||||
end
|
||||
start_new_session_for user, acr: "2"
|
||||
start_new_session_for user, acr: "2", remember_me: remember_me
|
||||
redirect_to after_authentication_url, notice: "Signed in successfully.", allow_other_host: true
|
||||
return
|
||||
end
|
||||
@@ -137,7 +140,7 @@ class SessionsController < ApplicationController
|
||||
if session[:totp_redirect_url].present?
|
||||
session[:return_to_after_authenticating] = session.delete(:totp_redirect_url)
|
||||
end
|
||||
start_new_session_for user, acr: "2"
|
||||
start_new_session_for user, acr: "2", remember_me: remember_me
|
||||
redirect_to after_authentication_url, notice: "Signed in successfully using backup code.", allow_other_host: true
|
||||
return
|
||||
end
|
||||
@@ -189,6 +192,7 @@ class SessionsController < ApplicationController
|
||||
|
||||
# Store user ID in session for verification
|
||||
session[:pending_webauthn_user_id] = user.id
|
||||
session[:pending_remember_me] = remember_me?
|
||||
|
||||
# Store redirect URL if present
|
||||
if params[:rd].present?
|
||||
@@ -284,12 +288,13 @@ class SessionsController < ApplicationController
|
||||
|
||||
# Clean up session
|
||||
session.delete(:pending_webauthn_user_id)
|
||||
remember_me = session.delete(:pending_remember_me) || false
|
||||
if session[:webauthn_redirect_url].present?
|
||||
session[:return_to_after_authenticating] = session.delete(:webauthn_redirect_url)
|
||||
end
|
||||
|
||||
# Create session (WebAuthn/passkey = phishing-resistant, ACR = "2")
|
||||
start_new_session_for user, acr: "2"
|
||||
start_new_session_for user, acr: "2", remember_me: remember_me
|
||||
|
||||
render json: {
|
||||
success: true,
|
||||
@@ -310,6 +315,10 @@ class SessionsController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def remember_me?
|
||||
ActiveModel::Type::Boolean.new.cast(params[:remember_me]) || false
|
||||
end
|
||||
|
||||
def validate_redirect_url(url)
|
||||
return nil unless url.present?
|
||||
|
||||
|
||||
@@ -180,7 +180,8 @@ export default class extends Controller {
|
||||
"X-CSRF-Token": this.getCSRFToken()
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: this.getUserEmail()
|
||||
email: this.getUserEmail(),
|
||||
remember_me: this.getRememberMe()
|
||||
})
|
||||
});
|
||||
|
||||
@@ -295,6 +296,11 @@ export default class extends Controller {
|
||||
return emailInput ? emailInput.value.trim() : "";
|
||||
}
|
||||
|
||||
getRememberMe() {
|
||||
const checkbox = document.querySelector('input[name="remember_me"][type="checkbox"]');
|
||||
return checkbox ? checkbox.checked : false;
|
||||
}
|
||||
|
||||
isValidEmail(email) {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="mx-auto md:w-2/3 w-full" data-controller="webauthn login-form" data-webauthn-check-url-value="/webauthn/check">
|
||||
<div class="mx-auto max-w-md w-full" data-controller="webauthn login-form" data-webauthn-check-url-value="/webauthn/check">
|
||||
<div class="mb-8">
|
||||
<h1 class="font-bold text-4xl">Sign in to Clinch</h1>
|
||||
<h1 class="font-bold text-4xl text-center">Sign in to Clinch</h1>
|
||||
</div>
|
||||
|
||||
<%= form_with url: signin_path, class: "contents", data: { controller: "form-errors" } do |form| %>
|
||||
@@ -53,6 +53,11 @@
|
||||
class: "block shadow-sm rounded-md border border-gray-400 focus:outline-blue-600 px-3 py-2 mt-2 w-full dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100" %>
|
||||
</div>
|
||||
|
||||
<div class="my-5 flex items-center">
|
||||
<%= form.check_box :remember_me, { class: "rounded border-gray-400 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800" }, "1", "0" %>
|
||||
<%= form.label :remember_me, "Remember me for 30 days", class: "ml-2 text-sm text-gray-600 dark:text-gray-400" %>
|
||||
</div>
|
||||
|
||||
<div class="my-5">
|
||||
<%= form.submit "Sign in",
|
||||
class: "w-full rounded-md px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white font-medium cursor-pointer" %>
|
||||
|
||||
Reference in New Issue
Block a user