feat: add sign_in and sign_out to authenticatable

This commit is contained in:
dualfroz committed 2026-09-06 18:21:18 +02:00
1 parent 7e18e4f80d
commit 40225e5791
1 file changed
+167
+167
View File
@@ -163,3 +163,170 @@ module Prism
true true
end end
def sign_in(user, remember: false)
session[authenticatable_session_key] = user.id
@current_user = user
if remember
remember_user(user)
elsif authenticatable_remember_sessions_model
session.delete(:login_session_id)
@current_login_session = nil
end
end
# Ends the current login: for `remember_cookie:`, rotates the shared
# token (not just deletes the cookie) so a copy of the old cookie
# value - already sitting in a proxy log, another browser tab,
# wherever - stops working immediately, everywhere, instead of
# remaining valid until it naturally expires. For `remember_sessions:`,
# destroys only THIS ONE login-session row, leaving every other
# remembered browser signed in exactly as before.
def sign_out
if authenticatable_remember_sessions_model
current_login_session&.destroy
forget_current_login_session_cookie
elsif authenticatable_remember_cookie
forget_user(current_user)
end
session.delete(authenticatable_session_key)
@current_user = nil
@current_login_session = nil
end
def require_authentication
return if user_signed_in?
session[:return_to_after_authenticating] = request.path
respond_to_unauthenticated
end
# Where to send the user once they do sign in: the page they were
# trying to reach before `require_authentication` intercepted them, or
# '/' if they arrived at the sign-in page directly.
def after_authentication_url
session.delete(:return_to_after_authenticating) || '/'
end
private
# Accepts either the model class itself or its name as a String/Symbol
# (resolved lazily, the same way Associable resolves `class_name:`), so
# `authenticate_with` can be declared before the model class exists yet
# (e.g. load order, or a model built dynamically in a test).
def authenticatable_model
configured = self.class.authenticatable_model
raise "#{self.class} must call authenticate_with model: SomeModel" unless configured
configured.is_a?(Module) ? configured : Object.const_get(configured.to_s)
end
def authenticatable_session_key
self.class.authenticatable_session_key || :user_id
end
def authenticatable_remember_cookie
self.class.authenticatable_remember_cookie
end
def authenticatable_remember_sessions_model
configured = self.class.authenticatable_remember_sessions_model
return nil unless configured
configured.is_a?(Module) ? configured : Object.const_get(configured.to_s)
end
def authenticatable_remember_sessions_foreign_key
(self.class.authenticatable_remember_sessions_foreign_key || :user_id).to_s
end
def authenticatable_max_remember_sessions
self.class.authenticatable_max_remember_sessions
end
def user_from_remember_cookie
if authenticatable_remember_sessions_model
user_from_remember_session_cookie
elsif authenticatable_remember_cookie
user_from_remember_column_cookie
end
end
def user_from_remember_session_cookie
token = cookies.signed[REMEMBER_SESSION_COOKIE]
return nil unless token
login_session = authenticatable_remember_sessions_model.find_by(token: token)
return nil unless login_session
user = authenticatable_model.find_by(id: login_session[authenticatable_remember_sessions_foreign_key])
return nil unless user
session[authenticatable_session_key] = user.id
session[:login_session_id] = login_session.id
user
end
def user_from_remember_column_cookie
token = cookies.signed[authenticatable_remember_cookie]
return nil unless token
user = authenticatable_model.find_by(authenticatable_remember_cookie => token)
session[authenticatable_session_key] = user.id if user
user
end
def remember_user(user)
if authenticatable_remember_sessions_model
remember_user_with_session(user)
elsif authenticatable_remember_cookie
remember_user_with_column(user)
else
raise "#{self.class} must pass remember_cookie: or remember_sessions: to authenticate_with " \
'to use sign_in(remember: true)'
end
end
def remember_user_with_session(user)
model = authenticatable_remember_sessions_model
attributes = { authenticatable_remember_sessions_foreign_key => user.id }
attributes['user_agent'] = request.user_agent if model.columns.include?('user_agent')
attributes['ip_address'] = request.remote_addr if model.columns.include?('ip_address')
login_session = model.create!(attributes)
session[:login_session_id] = login_session.id
@current_login_session = login_session
cookies.signed[REMEMBER_SESSION_COOKIE] = { value: login_session.token, max_age: REMEMBER_COOKIE_MAX_AGE }
prune_oldest_login_sessions(user)
end
# Destroys the oldest rows for `user` beyond `max_remember_sessions:`, if
# configured. The row just created above is always the newest by id, so
# it is never among the ones pruned here (unless the limit is 0, which
# would prune it too - authenticate_with does not special-case that,
# matching Devise's own `0 means never remembered` behavior for a
# deliberately-set zero).
def prune_oldest_login_sessions(user)
limit = authenticatable_max_remember_sessions
return unless limit
model = authenticatable_remember_sessions_model
sessions = model.where(authenticatable_remember_sessions_foreign_key => user.id).order(:id, :asc).to_a
overflow = sessions.size - limit
return if overflow <= 0
sessions.first(overflow).each(&:destroy)
end
def remember_user_with_column(user)
column = authenticatable_remember_cookie
user.send("regenerate_#{column}!") if user.send(column).to_s.empty?
cookies.signed[column] = { value: user.send(column), max_age: REMEMBER_COOKIE_MAX_AGE }
end
def forget_user(user)
column = authenticatable_remember_cookie
user.send("regenerate_#{column}!") if user && user.persisted?
cookies.set(column, '', max_age: 0)
end
def forget_current_login_session_cookie
cookies.set(REMEMBER_SESSION_COOKIE, '', max_age: 0)
session.delete(:login_session_id)
end
def respond_to_unauthenticated
path = self.class.unauthenticated_redirect_path
path ? redirect(path) : halt(401, 'authentication required')
end
end
end