# frozen-string-literal: truemoduleRodauthLockout=Feature.define(:lockout)dodepends:login,:email_baseview'unlock-account-request','Request Account Unlock','unlock_account_request'view'unlock-account','Unlock Account','unlock_account'before'unlock_account'before'unlock_account_request'after'unlock_account'after'unlock_account_request'additional_form_tags'unlock_account'additional_form_tags'unlock_account_request'button'Unlock Account','unlock_account'button'Request Account Unlock','unlock_account_request'error_flash"There was an error unlocking your account",'unlock_account'error_flash"This account is currently locked out and cannot be logged in to.","login_lockout"notice_flash"Your account has been unlocked",'unlock_account'notice_flash"An email has been sent to you with a link to unlock your account",'unlock_account_request'redirect:unlock_accountredirect:unlock_account_requestauth_value_method:unlock_account_autologin?,trueauth_value_method:max_invalid_logins,100auth_value_method:account_login_failures_table,:account_login_failuresauth_value_method:account_login_failures_id_column,:idauth_value_method:account_login_failures_number_column,:numberauth_value_method:account_lockouts_table,:account_lockoutsauth_value_method:account_lockouts_id_column,:idauth_value_method:account_lockouts_key_column,:keyauth_value_method:account_lockouts_deadline_column,:deadlineauth_value_method:account_lockouts_deadline_interval,{:days=>1}auth_value_method:no_matching_unlock_account_key_message,'No matching unlock account key'auth_value_method:unlock_account_email_subject,'Unlock Account'auth_value_method:unlock_account_key_param,'key'auth_value_method:unlock_account_requires_password?,falseauth_value_methods(:unlock_account_redirect,:unlock_account_request_redirect)auth_methods(:clear_invalid_login_attempts,:create_unlock_account_email,:generate_unlock_account_key,:get_unlock_account_key,:invalid_login_attempted,:locked_out?,:send_unlock_account_email,:unlock_account_email_body,:unlock_account_email_link,:unlock_account,:unlock_account_key)auth_private_methods:account_from_unlock_keyroute(:unlock_account_request)do|r|check_already_logged_inbefore_unlock_account_request_router.postdoifaccount_from_login(param(login_param))&&get_unlock_account_keytransactiondobefore_unlock_account_requestsend_unlock_account_emailafter_unlock_account_requestendset_notice_flashunlock_account_request_notice_flashelseset_redirect_error_flashno_matching_login_messageendredirectunlock_account_request_redirectendendroute(:unlock_account)do|r|check_already_logged_inbefore_unlock_account_router.getdoifaccount_from_unlock_key(param(unlock_account_key_param))unlock_account_viewelseset_redirect_error_flashno_matching_unlock_account_key_messageredirectrequire_login_redirectendendr.postdokey=param(unlock_account_key_param)unlessaccount_from_unlock_key(key)set_redirect_error_flashno_matching_unlock_account_key_messageredirectunlock_account_request_redirectendif!unlock_account_requires_password?||password_match?(param(password_param))transactiondobefore_unlock_accountunlock_accountafter_unlock_accountifunlock_account_autologin?update_sessionendendset_notice_flashunlock_account_notice_flashredirectunlock_account_redirectelseset_field_error(password_param,invalid_password_message)set_error_flashunlock_account_error_flashunlock_account_viewendendenddeflocked_out?ift=convert_timestamp(account_lockouts_ds.get(account_lockouts_deadline_column))ifTime.now<ttrueelseunlock_accountfalseendelsefalseendenddefunlock_accounttransactiondoremove_lockout_metadataendenddefclear_invalid_login_attemptsunlock_accountenddefinvalid_login_attemptedds=account_login_failures_ds.where(account_login_failures_id_column=>account_id)number=ifdb.database_type==:postgresds.returning(account_login_failures_number_column).with_sql(:update_sql,account_login_failures_number_column=>Sequel.expr(account_login_failures_number_column)+1).single_valueelse# :nocov:ifds.update(account_login_failures_number_column=>Sequel.expr(account_login_failures_number_column)+1)>0ds.get(account_login_failures_number_column)end# :nocov:endunlessnumber# Ignoring the violation is safe here. It may allow slightly more than max_invalid_logins invalid logins before# lockout, but allowing a few extra is OK if the race is lost.ignore_uniqueness_violation{account_login_failures_ds.insert(account_login_failures_id_column=>account_id)}number=1endifnumber>=max_invalid_logins@unlock_account_key_value=generate_unlock_account_keyhash={account_lockouts_id_column=>account_id,account_lockouts_key_column=>unlock_account_key_value}set_deadline_value(hash,account_lockouts_deadline_column,account_lockouts_deadline_interval)ife=raised_uniqueness_violation{account_lockouts_ds.insert(hash)}# If inserting into the lockout table raises a violation, we should just be able to pull the already inserted# key out of it. If that doesn't return a valid key, we should reraise the error.raiseeunless@unlock_account_key_value=account_lockouts_ds.get(account_lockouts_key_column)show_lockout_pageendendenddefget_unlock_account_keyaccount_lockouts_ds.get(account_lockouts_key_column)enddefaccount_from_unlock_key(key)@account=_account_from_unlock_key(key)enddefsend_unlock_account_email@unlock_account_key_value=get_unlock_account_keycreate_unlock_account_email.deliver!enddefunlock_account_email_linktoken_link(unlock_account_route,unlock_account_key_param,unlock_account_key_value)endprivateattr_reader:unlock_account_key_valuedefbefore_login_attemptiflocked_out?show_lockout_pageendsuperenddefafter_loginclear_invalid_login_attemptssuperenddefafter_login_failureinvalid_login_attemptedsuperenddefafter_close_accountremove_lockout_metadatasuperifdefined?(super)enddefgenerate_unlock_account_keyrandom_keyenddefremove_lockout_metadataaccount_login_failures_ds.deleteaccount_lockouts_ds.deleteenddefshow_lockout_pageset_error_flashlogin_lockout_error_flashresponse.writeunlock_account_request_viewrequest.haltenddefcreate_unlock_account_emailcreate_email(unlock_account_email_subject,unlock_account_email_body)enddefunlock_account_email_bodyrender('unlock-account-email')enddefuse_date_arithmetic?db.database_type==:mysqlenddefaccount_login_failures_dsdb[account_login_failures_table].where(account_login_failures_id_column=>account_id)enddefaccount_lockouts_ds(id=account_id)db[account_lockouts_table].where(account_lockouts_id_column=>id)enddef_account_from_unlock_key(token)account_from_key(token){|id|account_lockouts_ds(id).get(account_lockouts_key_column)}endendend