booth 0.0.1
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/CHANGELOG.md +4 -0
- data/LICENSE.md +22 -0
- data/README.md +372 -0
- data/app/assets/config/booth_manifest.js +15 -0
- data/app/assets/images/booth/browsers/README.md +2 -0
- data/app/assets/images/booth/browsers/chrome.svg +1 -0
- data/app/assets/images/booth/browsers/edge.svg +1 -0
- data/app/assets/images/booth/browsers/firefox.svg +1 -0
- data/app/assets/images/booth/browsers/internet_explorer.svg +1 -0
- data/app/assets/images/booth/browsers/opera.svg +1 -0
- data/app/assets/images/booth/browsers/safari.svg +1 -0
- data/app/assets/images/booth/browsers/unknown.svg +1 -0
- data/app/assets/images/booth/platforms/README.md +2 -0
- data/app/assets/images/booth/platforms/android.svg +6 -0
- data/app/assets/images/booth/platforms/apple.svg +6 -0
- data/app/assets/images/booth/platforms/linux.svg +6 -0
- data/app/assets/images/booth/platforms/unknown.svg +1 -0
- data/app/assets/images/booth/platforms/windows.svg +6 -0
- data/app/assets/javascripts/booth/all.js +162 -0
- data/app/assets/javascripts/booth/all.js.map +1 -0
- data/app/assets/javascripts/booth/booth.ts +194 -0
- data/app/assets/javascripts/booth/webauthn-json.ts +99 -0
- data/config/locales/de.yml +84 -0
- data/config/locales/en.yml +79 -0
- data/lib/booth/adminland/credentials/create.rb +30 -0
- data/lib/booth/adminland/onboardings/create.rb +63 -0
- data/lib/booth/adminland/onboardings/destroy.rb +50 -0
- data/lib/booth/adminland/onboardings/find.rb +93 -0
- data/lib/booth/adminland/onboardings/index.rb +23 -0
- data/lib/booth/adminland/periodic_cleanup.rb +11 -0
- data/lib/booth/adminland/recoveries/consume.rb +70 -0
- data/lib/booth/adminland.rb +48 -0
- data/lib/booth/audits/register/added_otp.rb +22 -0
- data/lib/booth/audits/register/changed_otp.rb +22 -0
- data/lib/booth/audits/register/completed_onboarding.rb +22 -0
- data/lib/booth/audits/register/correct_otp.rb +42 -0
- data/lib/booth/audits/register/correct_password.rb +43 -0
- data/lib/booth/audits/register/logout.rb +22 -0
- data/lib/booth/audits/register/requested_password_reset.rb +22 -0
- data/lib/booth/audits/register/wrong_otp.rb +22 -0
- data/lib/booth/audits/register/wrong_password.rb +25 -0
- data/lib/booth/authenticators/confirm.rb +34 -0
- data/lib/booth/authenticators/credential_mode_after_confirmation.rb +25 -0
- data/lib/booth/authenticators/step.rb +19 -0
- data/lib/booth/concerns/action.rb +58 -0
- data/lib/booth/concerns/transition.rb +17 -0
- data/lib/booth/configuration.rb +116 -0
- data/lib/booth/configure.rb +37 -0
- data/lib/booth/contests/get.rb +36 -0
- data/lib/booth/contests/respond.rb +78 -0
- data/lib/booth/contests/set_for_login.rb +28 -0
- data/lib/booth/cooldowns/distance_of_time.rb +46 -0
- data/lib/booth/cooldowns/otp.rb +22 -0
- data/lib/booth/cooldowns/password.rb +44 -0
- data/lib/booth/cooldowns/password_reset.rb +24 -0
- data/lib/booth/cooldowns/strategies/exponential.rb +82 -0
- data/lib/booth/cooldowns/strategies/global.rb +62 -0
- data/lib/booth/cooldowns/strategies/result.rb +22 -0
- data/lib/booth/credentials/create.rb +28 -0
- data/lib/booth/credentials/create_with_onboarding.rb +26 -0
- data/lib/booth/credentials/find_by_username.rb +45 -0
- data/lib/booth/credentials/mode.rb +69 -0
- data/lib/booth/credentials/modes/otp_addable.rb +23 -0
- data/lib/booth/credentials/modes/otp_changeable.rb +23 -0
- data/lib/booth/credentials/modes/otp_manageable.rb +17 -0
- data/lib/booth/credentials/modes/otp_removable.rb +23 -0
- data/lib/booth/credentials/modes/password_addable.rb +29 -0
- data/lib/booth/credentials/modes/password_changeable.rb +31 -0
- data/lib/booth/credentials/modes/password_manageable.rb +17 -0
- data/lib/booth/credentials/modes/password_removable.rb +24 -0
- data/lib/booth/credentials/modes/password_removal_requires_user_verifiable_webauth.rb +16 -0
- data/lib/booth/credentials/modes/webauth_addable.rb +26 -0
- data/lib/booth/credentials/modes/webauth_manageable.rb +16 -0
- data/lib/booth/credentials/modes/webauth_removable.rb +25 -0
- data/lib/booth/credentials/otp_authentication.rb +59 -0
- data/lib/booth/credentials/password_authentication.rb +72 -0
- data/lib/booth/credentials/webauth_challenge.rb +28 -0
- data/lib/booth/engine.rb +25 -0
- data/lib/booth/errors.rb +86 -0
- data/lib/booth/geolocation.rb +20 -0
- data/lib/booth/hooks/after_fetch.rb +54 -0
- data/lib/booth/hooks/before_logout.rb +29 -0
- data/lib/booth/hooks/serialize_from_session.rb +24 -0
- data/lib/booth/hooks/serialize_into_session.rb +14 -0
- data/lib/booth/logger.rb +41 -0
- data/lib/booth/logging.rb +59 -0
- data/lib/booth/method_object.rb +73 -0
- data/lib/booth/mode.rb +22 -0
- data/lib/booth/models/application_record.rb +7 -0
- data/lib/booth/models/audit.rb +24 -0
- data/lib/booth/models/authenticator.rb +45 -0
- data/lib/booth/models/concerns/modeable.rb +50 -0
- data/lib/booth/models/concerns/otpable.rb +37 -0
- data/lib/booth/models/concerns/passwordable.rb +58 -0
- data/lib/booth/models/contest.rb +55 -0
- data/lib/booth/models/contests/scopes/recently_created.rb +23 -0
- data/lib/booth/models/contests/scopes/recently_responded.rb +32 -0
- data/lib/booth/models/credential.rb +61 -0
- data/lib/booth/models/onboarding.rb +61 -0
- data/lib/booth/models/password_reset.rb +41 -0
- data/lib/booth/models/recovery.rb +32 -0
- data/lib/booth/models/registration.rb +10 -0
- data/lib/booth/models/session.rb +47 -0
- data/lib/booth/models/user_agent.rb +50 -0
- data/lib/booth/modes/base.rb +25 -0
- data/lib/booth/modes/username_and_password.rb +7 -0
- data/lib/booth/modes/username_and_webauth.rb +7 -0
- data/lib/booth/modes/username_password_and_otp.rb +7 -0
- data/lib/booth/modes/username_password_and_webauth.rb +7 -0
- data/lib/booth/onboardings/find.rb +35 -0
- data/lib/booth/onboardings/propagate_to_credential.rb +63 -0
- data/lib/booth/onboardings/step.rb +68 -0
- data/lib/booth/password_resets/create.rb +57 -0
- data/lib/booth/password_resets/find.rb +36 -0
- data/lib/booth/password_resets/propagate_to_credential.rb +36 -0
- data/lib/booth/password_resets/step.rb +18 -0
- data/lib/booth/recoveries/create.rb +45 -0
- data/lib/booth/request.rb +106 -0
- data/lib/booth/requests/agent.rb +14 -0
- data/lib/booth/requests/authentication.rb +47 -0
- data/lib/booth/requests/ip.rb +28 -0
- data/lib/booth/requests/return_path.rb +34 -0
- data/lib/booth/requests/session.rb +106 -0
- data/lib/booth/requests/storage.rb +62 -0
- data/lib/booth/requests/storages/login.rb +108 -0
- data/lib/booth/requests/storages/otp.rb +54 -0
- data/lib/booth/requests/storages/password.rb +49 -0
- data/lib/booth/requests/storages/password_reset.rb +35 -0
- data/lib/booth/requests/storages/recovery.rb +35 -0
- data/lib/booth/requests/storages/registration.rb +27 -0
- data/lib/booth/requests/storages/webauth.rb +38 -0
- data/lib/booth/requests/sudo.rb +110 -0
- data/lib/booth/routes/userland.rb +80 -0
- data/lib/booth/sessions/create_and_login.rb +46 -0
- data/lib/booth/sessions/historical_locations.rb +18 -0
- data/lib/booth/sessions/index.rb +59 -0
- data/lib/booth/sessions/revoke.rb +51 -0
- data/lib/booth/sessions/revoke_all_others.rb +43 -0
- data/lib/booth/sessions/to_passport.rb +51 -0
- data/lib/booth/syntaxes/contest_code.rb +58 -0
- data/lib/booth/syntaxes/email.rb +97 -0
- data/lib/booth/syntaxes/ip.rb +37 -0
- data/lib/booth/syntaxes/otp.rb +57 -0
- data/lib/booth/syntaxes/scope.rb +21 -0
- data/lib/booth/syntaxes/scope_comparison.rb +28 -0
- data/lib/booth/syntaxes/secret_key.rb +64 -0
- data/lib/booth/syntaxes/username.rb +85 -0
- data/lib/booth/syntaxes/uuid.rb +23 -0
- data/lib/booth/test/helpers.rb +63 -0
- data/lib/booth/test/support/assert_all_partials_were_covered.rb +63 -0
- data/lib/booth/test/support/assert_logged_in.rb +49 -0
- data/lib/booth/test/support/assert_logged_out.rb +30 -0
- data/lib/booth/test/support/assert_partial.rb +29 -0
- data/lib/booth/test/support/force_login.rb +26 -0
- data/lib/booth/test/support/get_session_value.rb +35 -0
- data/lib/booth/test/support/otp_code_from_session.rb +30 -0
- data/lib/booth/test/support/soft_reset_session.rb +22 -0
- data/lib/booth/test/userland/logins/missing_authenticators.rb +72 -0
- data/lib/booth/test/userland/logins/missing_onboarding.rb +35 -0
- data/lib/booth/test/userland/logins/username_and_password.rb +40 -0
- data/lib/booth/test/userland/logins/username_and_webauth.rb +75 -0
- data/lib/booth/test/userland/logins/username_password_and_otp.rb +45 -0
- data/lib/booth/test/userland/logins/username_password_and_webauth.rb +86 -0
- data/lib/booth/test/userland/onboardings/already_logged_in.rb +64 -0
- data/lib/booth/test/userland/onboardings/otp.rb +63 -0
- data/lib/booth/test/userland/onboardings/password.rb +49 -0
- data/lib/booth/test/userland/onboardings/timeout.rb +47 -0
- data/lib/booth/test/userland/otps/manage.rb +86 -0
- data/lib/booth/test/userland/password_resets/reset.rb +102 -0
- data/lib/booth/test/userland.rb +38 -0
- data/lib/booth/test/webauthn/disable.rb +17 -0
- data/lib/booth/test/webauthn/enable.rb +19 -0
- data/lib/booth/test/webauthn/virtual_authenticators/create.rb +38 -0
- data/lib/booth/test/webauthn/virtual_authenticators/destroy.rb +20 -0
- data/lib/booth/test.rb +53 -0
- data/lib/booth/to_struct.rb +11 -0
- data/lib/booth/userland/extract_flash_messages.rb +35 -0
- data/lib/booth/userland/logins/create.rb +28 -0
- data/lib/booth/userland/logins/destroy.rb +37 -0
- data/lib/booth/userland/logins/new.rb +70 -0
- data/lib/booth/userland/logins/transitions/create/choose_username.rb +41 -0
- data/lib/booth/userland/logins/transitions/create/enter_otp.rb +70 -0
- data/lib/booth/userland/logins/transitions/create/skip_remotes.rb +24 -0
- data/lib/booth/userland/logins/transitions/create/verify_password.rb +70 -0
- data/lib/booth/userland/logins/transitions/create/webauth_authentication_initiation.rb +55 -0
- data/lib/booth/userland/logins/transitions/create/webauth_authentication_verification.rb +80 -0
- data/lib/booth/userland/logins/transitions/new/already_logged_in.rb +21 -0
- data/lib/booth/userland/logins/transitions/new/fallible.rb +27 -0
- data/lib/booth/userland/logins/transitions/new/mode_first_time.rb +20 -0
- data/lib/booth/userland/logins/transitions/new/mode_username_and_password.rb +20 -0
- data/lib/booth/userland/logins/transitions/new/mode_username_and_webauth.rb +26 -0
- data/lib/booth/userland/logins/transitions/new/mode_username_password_and_otp.rb +24 -0
- data/lib/booth/userland/logins/transitions/new/mode_username_password_and_webauth.rb +24 -0
- data/lib/booth/userland/logins/transitions/new/no_username_chosen.rb +19 -0
- data/lib/booth/userland/logins/transitions/new/remote_session_available.rb +52 -0
- data/lib/booth/userland/logins/transitions/new/timed_out.rb +25 -0
- data/lib/booth/userland/onboardings/show.rb +74 -0
- data/lib/booth/userland/onboardings/transitions/update/choose_mode.rb +58 -0
- data/lib/booth/userland/onboardings/transitions/update/choose_password.rb +41 -0
- data/lib/booth/userland/onboardings/transitions/update/choose_webauth_nickname.rb +50 -0
- data/lib/booth/userland/onboardings/transitions/update/confirm_otp.rb +58 -0
- data/lib/booth/userland/onboardings/transitions/update/confirm_password.rb +49 -0
- data/lib/booth/userland/onboardings/transitions/update/register_otp.rb +31 -0
- data/lib/booth/userland/onboardings/transitions/update/reset_otp.rb +40 -0
- data/lib/booth/userland/onboardings/transitions/update/reset_password.rb +35 -0
- data/lib/booth/userland/onboardings/transitions/update/reset_webauth.rb +46 -0
- data/lib/booth/userland/onboardings/transitions/update/webauth_authentication_initiation.rb +40 -0
- data/lib/booth/userland/onboardings/transitions/update/webauth_authentication_verification.rb +59 -0
- data/lib/booth/userland/onboardings/transitions/update/webauth_registration_initiation.rb +46 -0
- data/lib/booth/userland/onboardings/transitions/update/webauth_registration_verification.rb +56 -0
- data/lib/booth/userland/onboardings/update.rb +68 -0
- data/lib/booth/userland/otps/destroy.rb +42 -0
- data/lib/booth/userland/otps/edit.rb +72 -0
- data/lib/booth/userland/otps/guards/manageable.rb +21 -0
- data/lib/booth/userland/otps/guards/sudo.rb +23 -0
- data/lib/booth/userland/otps/show.rb +36 -0
- data/lib/booth/userland/otps/sudo.rb +51 -0
- data/lib/booth/userland/otps/transitions/update/confirm.rb +84 -0
- data/lib/booth/userland/otps/transitions/update/register.rb +40 -0
- data/lib/booth/userland/otps/transitions/update/reset.rb +31 -0
- data/lib/booth/userland/otps/update.rb +34 -0
- data/lib/booth/userland/password_resets/create.rb +73 -0
- data/lib/booth/userland/password_resets/guards/logged_out.rb +21 -0
- data/lib/booth/userland/password_resets/new.rb +57 -0
- data/lib/booth/userland/password_resets/show.rb +77 -0
- data/lib/booth/userland/password_resets/transitions/update/choose_password.rb +48 -0
- data/lib/booth/userland/password_resets/transitions/update/confirm_password.rb +54 -0
- data/lib/booth/userland/password_resets/transitions/update/reset_password.rb +29 -0
- data/lib/booth/userland/password_resets/update.rb +65 -0
- data/lib/booth/userland/passwords/destroy.rb +41 -0
- data/lib/booth/userland/passwords/edit.rb +54 -0
- data/lib/booth/userland/passwords/guards/manageable.rb +21 -0
- data/lib/booth/userland/passwords/guards/removable.rb +21 -0
- data/lib/booth/userland/passwords/guards/sudo.rb +21 -0
- data/lib/booth/userland/passwords/remove.rb +34 -0
- data/lib/booth/userland/passwords/show.rb +32 -0
- data/lib/booth/userland/passwords/sudo.rb +55 -0
- data/lib/booth/userland/passwords/transitions/remove/step.rb +27 -0
- data/lib/booth/userland/passwords/transitions/update/choose_password.rb +62 -0
- data/lib/booth/userland/passwords/transitions/update/confirm_password.rb +82 -0
- data/lib/booth/userland/passwords/update.rb +33 -0
- data/lib/booth/userland/personal_contests/show.rb +60 -0
- data/lib/booth/userland/personal_contests/update.rb +37 -0
- data/lib/booth/userland/recoveries/create.rb +48 -0
- data/lib/booth/userland/recoveries/new.rb +35 -0
- data/lib/booth/userland/registrations/create.rb +56 -0
- data/lib/booth/userland/registrations/new.rb +39 -0
- data/lib/booth/userland/sessions/destroy_one_or_other.rb +41 -0
- data/lib/booth/userland/sessions/index.rb +27 -0
- data/lib/booth/userland/sessions/show.rb +31 -0
- data/lib/booth/userland/sessions/transitions/destroy/enter_password.rb +50 -0
- data/lib/booth/userland/sessions/transitions/destroy/enter_webauth.rb +56 -0
- data/lib/booth/userland/sessions/transitions/destroy/verify_password.rb +83 -0
- data/lib/booth/userland/sessions/transitions/destroy/webauth_authentication_initiation.rb +38 -0
- data/lib/booth/userland/sessions/transitions/destroy/webauth_authentication_verification.rb +61 -0
- data/lib/booth/userland/sessions/transitions/show/enter_webauth.rb +56 -0
- data/lib/booth/userland/webauths/create.rb +83 -0
- data/lib/booth/userland/webauths/destroy.rb +60 -0
- data/lib/booth/userland/webauths/guards/manageable.rb +21 -0
- data/lib/booth/userland/webauths/guards/sudo.rb +22 -0
- data/lib/booth/userland/webauths/index.rb +43 -0
- data/lib/booth/userland/webauths/new.rb +70 -0
- data/lib/booth/userland/webauths/sudo.rb +25 -0
- data/lib/booth/userland/webauths/transitions/create/authentication_initiation.rb +52 -0
- data/lib/booth/userland/webauths/transitions/create/authentication_verification.rb +64 -0
- data/lib/booth/userland/webauths/transitions/create/choose_nickname.rb +50 -0
- data/lib/booth/userland/webauths/transitions/create/registration_initiation.rb +61 -0
- data/lib/booth/userland/webauths/transitions/create/registration_verification.rb +68 -0
- data/lib/booth/userland/webauths/transitions/create/reset.rb +36 -0
- data/lib/booth/userland/webauths/transitions/new/step.rb +23 -0
- data/lib/booth/userland/webauths/transitions/sudo/authentication_initiation.rb +47 -0
- data/lib/booth/userland/webauths/transitions/sudo/authentication_verification.rb +34 -0
- data/lib/booth/userland.rb +192 -0
- data/lib/booth/version.rb +3 -0
- data/lib/booth/webauth/authentication_verification.rb +68 -0
- data/lib/booth/webauth/demand_user_verification.rb +29 -0
- data/lib/booth/webauth/options_for_create.rb +46 -0
- data/lib/booth/webauth/options_for_get.rb +29 -0
- data/lib/booth.rb +267 -0
- data/lib/generators/booth/migration/migration_generator.rb +25 -0
- data/lib/generators/booth/migration/templates/add_credential_to_users.erb +18 -0
- data/lib/generators/booth/migration/templates/create_booth_mode_types.erb +20 -0
- data/lib/generators/booth/migration/templates/create_booth_tables.erb +135 -0
- metadata +861 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Authenticators
|
|
3
|
+
class Step
|
|
4
|
+
include ::Booth::MethodObject
|
|
5
|
+
|
|
6
|
+
param :authenticator
|
|
7
|
+
|
|
8
|
+
def call
|
|
9
|
+
return :register if authenticator.device_id.blank? ||
|
|
10
|
+
authenticator.public_key.blank? ||
|
|
11
|
+
authenticator.sign_count.blank?
|
|
12
|
+
return :choose_nickname if authenticator.nickname.blank?
|
|
13
|
+
return :confirm if authenticator.confirmed_at.blank?
|
|
14
|
+
|
|
15
|
+
:completed
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Concerns
|
|
3
|
+
# An "Action" is something that is called from a Rails controller.#
|
|
4
|
+
# It contains all the logic that the controller action is supposed to execute.
|
|
5
|
+
# By convention the "authentication scope" and the "request object" are passed in.
|
|
6
|
+
module Action
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
include ::Booth::Logging
|
|
11
|
+
include ::Booth::MethodObject
|
|
12
|
+
|
|
13
|
+
option :scope, ->(scope) { ::Booth::Syntaxes::Scope.call(scope).normalized_scope }
|
|
14
|
+
option :request, ::Booth::Request
|
|
15
|
+
|
|
16
|
+
delegate :params, to: :request, private: true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# ----------------
|
|
22
|
+
# May be overriden
|
|
23
|
+
# ----------------
|
|
24
|
+
|
|
25
|
+
def initialize_transition
|
|
26
|
+
transition.call(request:)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def after_transition; end
|
|
30
|
+
|
|
31
|
+
def transitions
|
|
32
|
+
raise "Implement `#transitions` in #{self}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# ---------------
|
|
36
|
+
# Never overriden
|
|
37
|
+
# ---------------
|
|
38
|
+
|
|
39
|
+
# I found this to be a repetitive pattern, so I added this method in this concern.
|
|
40
|
+
# It makes the code a little harder to read but probably still more robust.
|
|
41
|
+
def do_transition
|
|
42
|
+
if transition
|
|
43
|
+
# debug { "Calling Transition #{transition}" }
|
|
44
|
+
result = initialize_transition
|
|
45
|
+
after_transition
|
|
46
|
+
return result
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
debug { 'No transition applies to these params' }
|
|
50
|
+
Tron.failure :unknown_transition
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def transition
|
|
54
|
+
@transition ||= transitions.detect { _1.applicable?(params:) }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Concerns
|
|
3
|
+
# A `Booth::Action` usually consists of several `Booth::Transition`s.
|
|
4
|
+
module Transition
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
include ::Booth::Logging
|
|
9
|
+
include ::Booth::MethodObject
|
|
10
|
+
|
|
11
|
+
option :request, ::Booth::Request
|
|
12
|
+
|
|
13
|
+
delegate :params, to: :request, private: true
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
# Holds global configuration parameters.
|
|
3
|
+
class Configuration
|
|
4
|
+
def otp_issuer(scope: :default)
|
|
5
|
+
@otp_issuers ||= {}
|
|
6
|
+
|
|
7
|
+
@otp_issuers[scope.to_sym].to_s.presence || 'Login'
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def set_otp_issuer(new_issuer, scope: :default)
|
|
11
|
+
@otp_issuers ||= {}
|
|
12
|
+
|
|
13
|
+
@otp_issuers[scope.to_sym] = new_issuer
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def otp_issuer=(new_issuer)
|
|
17
|
+
set_otp_issuer(new_issuer)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def logger
|
|
21
|
+
return if @no_logger
|
|
22
|
+
|
|
23
|
+
@logger || rails_logger
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def logger=(new_logger)
|
|
27
|
+
@no_logger = new_logger.nil?
|
|
28
|
+
@logger = new_logger
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def log_to_rails_and_stdout!
|
|
32
|
+
self.logger = Class.new do
|
|
33
|
+
def debug(...)
|
|
34
|
+
::Booth.config.send(:rails_logger).debug(...)
|
|
35
|
+
::Booth.config.send(:stdout_logger).debug(...)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def warn(...)
|
|
39
|
+
::Booth.config.send(:rails_logger).warn(...)
|
|
40
|
+
::Booth.config.send(:stdout_logger).warn(...)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def error(...)
|
|
44
|
+
::Booth.config.send(:rails_logger).error(...)
|
|
45
|
+
::Booth.config.send(:stdout_logger).error(...)
|
|
46
|
+
end
|
|
47
|
+
end.new
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def interaction_timeout
|
|
51
|
+
20.minutes
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def session_inactivity_lifetime
|
|
55
|
+
@session_inactivity_lifetime ||= 3.months
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def session_inactivity_lifetime=(new_lifetime)
|
|
59
|
+
@session_inactivity_lifetime = new_lifetime
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def password_reset_window
|
|
63
|
+
2.hours
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def onboarding_window
|
|
67
|
+
1.week
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def otp_digits
|
|
71
|
+
6
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def contest_digits
|
|
75
|
+
6
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# The standard Rails logger does not show `progname`.
|
|
81
|
+
# But it's a helpful feature, so we add it manually to the message.
|
|
82
|
+
def rails_logger
|
|
83
|
+
@rails_logger ||= Class.new do
|
|
84
|
+
def debug(progname, &block)
|
|
85
|
+
::Rails.logger.debug { "#{ActiveSupport::LogSubscriber.new.send(:color, progname, :blue)} - #{block.call}" }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def info(progname, &block)
|
|
89
|
+
::Rails.logger.info { "#{ActiveSupport::LogSubscriber.new.send(:color, progname, :blue)} - #{block.call}" }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def warn(progname, &block)
|
|
93
|
+
::Rails.logger.warn { "#{ActiveSupport::LogSubscriber.new.send(:color, progname, :blue)} - #{block.call}" }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def error(progname, &block)
|
|
97
|
+
::Rails.logger.error { "#{ActiveSupport::LogSubscriber.new.send(:color, progname, :blue)} - #{block.call}" }
|
|
98
|
+
end
|
|
99
|
+
end.new
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def stdout_logger
|
|
103
|
+
return @stdout_logger if defined?(@stdout_logger)
|
|
104
|
+
|
|
105
|
+
@stdout_logger = ::Logger.new($stdout)
|
|
106
|
+
@stdout_logger.formatter = stdout_logger_formatter
|
|
107
|
+
@stdout_logger
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def stdout_logger_formatter
|
|
111
|
+
proc do |severity, _, progname, message|
|
|
112
|
+
[severity.rjust(5), progname, '-', message, "\n"].join(' ')
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
# Lazy-loads and returns the global configuration instance.
|
|
3
|
+
#
|
|
4
|
+
# @example
|
|
5
|
+
# Booth.config.logger = MyLogger.new
|
|
6
|
+
#
|
|
7
|
+
# @return [Booth::Configuration]
|
|
8
|
+
# @see .configure
|
|
9
|
+
#
|
|
10
|
+
def self.config
|
|
11
|
+
@config ||= ::Booth::Configuration.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Yields the configuration instance.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# Booth.configure do |config|
|
|
18
|
+
# config.logger = MyLogger.new
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# @yieldparam [Booth::Configuration] config global configuration instance.
|
|
22
|
+
# @see .config
|
|
23
|
+
#
|
|
24
|
+
def self.configure
|
|
25
|
+
yield config
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Resets the configuration.
|
|
29
|
+
#
|
|
30
|
+
# @note This is useful for testing, since the configuration is global
|
|
31
|
+
# and persists across tests.
|
|
32
|
+
# @api private
|
|
33
|
+
#
|
|
34
|
+
def self.reset!
|
|
35
|
+
@configs = nil
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Contests
|
|
3
|
+
class Get
|
|
4
|
+
include ::Booth::Logging
|
|
5
|
+
include ::Booth::MethodObject
|
|
6
|
+
|
|
7
|
+
option :credential_id
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
return Tron.failure :contest_not_found unless contest
|
|
11
|
+
|
|
12
|
+
Tron.success :found_recent_contest,
|
|
13
|
+
formatted_code: contest.formatted_code,
|
|
14
|
+
normalized_code: contest.code,
|
|
15
|
+
reason: contest.reason.to_sym,
|
|
16
|
+
ip: contest.ip,
|
|
17
|
+
agent: contest.agent.presence,
|
|
18
|
+
location: contest.location.presence,
|
|
19
|
+
recently_responded: contest.recently_responded?,
|
|
20
|
+
browser_name: contest.browser_name,
|
|
21
|
+
platform_name: contest.platform_name,
|
|
22
|
+
browser_image_path: contest.browser_image_path,
|
|
23
|
+
platform_image_path: contest.platform_image_path
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def contest
|
|
29
|
+
return @contest if defined?(@contest)
|
|
30
|
+
|
|
31
|
+
@contest = ::Booth::Models::Contest.recently_created_scope
|
|
32
|
+
.find_by(credential_id:)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Contests
|
|
3
|
+
class Respond
|
|
4
|
+
include ::Booth::Logging
|
|
5
|
+
include ::Booth::MethodObject
|
|
6
|
+
|
|
7
|
+
option :scope
|
|
8
|
+
option :contest
|
|
9
|
+
option :request
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
do_find_contest
|
|
13
|
+
.on_success { do_check_timeout }
|
|
14
|
+
.on_success { do_check_scope }
|
|
15
|
+
.on_success { do_check_already_responded }
|
|
16
|
+
.on_success { do_check_code_syntax }
|
|
17
|
+
.on_success { do_respond }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
delegate :credential, to: :contest, private: true
|
|
23
|
+
|
|
24
|
+
def do_find_contest
|
|
25
|
+
return Tron.success :contest_exists if contest
|
|
26
|
+
|
|
27
|
+
Tron.failure :missing_contest
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def do_check_timeout
|
|
31
|
+
return Tron.success :contested_recently if contest.recently_created?
|
|
32
|
+
|
|
33
|
+
debug { 'This contest timed out' }
|
|
34
|
+
Tron.failure :contest_timed_out,
|
|
35
|
+
lifespan: contest.lifespan,
|
|
36
|
+
public_message: I18n.t('booth.contest_timed_out', lifespan_minutes: contest.lifespan.seconds / 60)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def do_check_scope
|
|
40
|
+
::Booth::Syntaxes::ScopeComparison.call this: scope, that: credential.scope
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def do_check_already_responded
|
|
44
|
+
return Tron.success :ok_waiting_for_response if contest.responded_at.blank?
|
|
45
|
+
|
|
46
|
+
debug { "This contest has already been responded to #{contest.responded_at.inspect}" }
|
|
47
|
+
Tron.failure :already_responded, public_message: I18n.t('booth.already_responded_to_contest')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def do_check_code_syntax
|
|
51
|
+
check = ::Booth::Syntaxes::ContestCode.call(code_param)
|
|
52
|
+
|
|
53
|
+
check.on_success do
|
|
54
|
+
@normalized_code = check.normalized_contest_code
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
check
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def do_respond
|
|
61
|
+
return Tron.failure :no_response_needed unless contest.reason.to_sym == :login
|
|
62
|
+
|
|
63
|
+
if @normalized_code == contest.code
|
|
64
|
+
debug { "The code #{@normalized_code} was accepted, persisting positive response..." }
|
|
65
|
+
contest.update!(responded_at: Time.current)
|
|
66
|
+
return Tron.success :response_code_accepted,
|
|
67
|
+
public_message: I18n.t('booth.contest_response_accepted')
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
Tron.failure :wrong_code, public_message: I18n.t('booth.wrong_response_code')
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def code_param
|
|
74
|
+
request.params.require(:response).permit(:code)[:code]
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Contests
|
|
3
|
+
class SetForLogin
|
|
4
|
+
include ::Booth::Logging
|
|
5
|
+
include ::Booth::MethodObject
|
|
6
|
+
|
|
7
|
+
option :credential_id
|
|
8
|
+
option :request
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
contest = nil
|
|
12
|
+
|
|
13
|
+
::Booth::Models::ApplicationRecord.transaction do
|
|
14
|
+
::Booth::Models::Contest.where(credential_id:).delete_all
|
|
15
|
+
|
|
16
|
+
contest = ::Booth::Models::Contest.create!(
|
|
17
|
+
credential_id:,
|
|
18
|
+
reason: :login,
|
|
19
|
+
ip: request.ip,
|
|
20
|
+
agent: request.agent
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Tron.success :contest_created, contest:
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Cooldowns
|
|
3
|
+
class DistanceOfTime
|
|
4
|
+
include ::Booth::MethodObject
|
|
5
|
+
|
|
6
|
+
option :from
|
|
7
|
+
option :till
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
result = []
|
|
11
|
+
result.push("#{distance_in_hours} h") if show_hours?
|
|
12
|
+
result.push("#{distance_in_minutes} min") if show_minutes?
|
|
13
|
+
result.push("#{distance_in_seconds} s") if show_seconds?
|
|
14
|
+
result.join(' ')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def show_hours?
|
|
18
|
+
distance_in_hours.positive?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def show_minutes?
|
|
22
|
+
return false if (((till - from).abs % 3600) / 60) < 1
|
|
23
|
+
|
|
24
|
+
distance_in_minutes.nonzero?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def show_seconds?
|
|
28
|
+
return true if distance_in_seconds < 60
|
|
29
|
+
|
|
30
|
+
distance_in_hours.zero? && distance_in_minutes.zero?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def distance_in_hours
|
|
34
|
+
((till - from).abs / 3600).floor
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def distance_in_minutes
|
|
38
|
+
(((till - from).abs % 3600) / 60).round
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def distance_in_seconds
|
|
42
|
+
(till - from).abs.round
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Cooldowns
|
|
3
|
+
class Otp
|
|
4
|
+
include ::Booth::MethodObject
|
|
5
|
+
|
|
6
|
+
option :credential
|
|
7
|
+
|
|
8
|
+
def call
|
|
9
|
+
::Booth::Cooldowns::Strategies::Global.call scope:,
|
|
10
|
+
max_attempts: 10
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def scope
|
|
16
|
+
::Booth::Models::Audit.visible_scope
|
|
17
|
+
.event_entered_wrong_otp
|
|
18
|
+
.where(credential:)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Cooldowns
|
|
3
|
+
class Password
|
|
4
|
+
include ::Booth::MethodObject
|
|
5
|
+
|
|
6
|
+
option :ip
|
|
7
|
+
option :credential
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
# No limit for logins where hardware tokens are required.
|
|
11
|
+
return Tron.success :cool_for_webauth if credential.mode_username_and_webauth?
|
|
12
|
+
return Tron.success :cool_for_password_and_webauth if credential.mode_username_password_and_webauth?
|
|
13
|
+
|
|
14
|
+
if credential.mode_username_password_and_otp?
|
|
15
|
+
do_check_exponentially
|
|
16
|
+
else
|
|
17
|
+
do_check_globally
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def do_check_exponentially
|
|
24
|
+
::Booth::Cooldowns::Strategies::Exponential.call scope: base_scope
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def do_check_globally
|
|
28
|
+
::Booth::Cooldowns::Strategies::Global.call scope: ip_range_scope, max_attempts: 10
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Scopes
|
|
32
|
+
|
|
33
|
+
def ip_range_scope
|
|
34
|
+
base_scope.where('ip << ?', "#{ip}/24")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def base_scope
|
|
38
|
+
::Booth::Models::Audit.visible_scope
|
|
39
|
+
.event_entered_wrong_password
|
|
40
|
+
.where(credential:)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Cooldowns
|
|
3
|
+
# Throttles how often you can generate a password reset link.
|
|
4
|
+
# Note that this does not reveal whether we know the email address or not,
|
|
5
|
+
# because it throttles the attempts per Credential (i.e. username).
|
|
6
|
+
class PasswordReset
|
|
7
|
+
include ::Booth::MethodObject
|
|
8
|
+
|
|
9
|
+
option :credential
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
::Booth::Cooldowns::Strategies::Exponential.call scope:
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def scope
|
|
18
|
+
::Booth::Models::Audit.visible_scope
|
|
19
|
+
.event_requested_password_reset
|
|
20
|
+
.where(credential:)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Cooldowns
|
|
3
|
+
module Strategies
|
|
4
|
+
class Exponential
|
|
5
|
+
include ::Booth::MethodObject
|
|
6
|
+
include ::Booth::Logging
|
|
7
|
+
|
|
8
|
+
option :scope
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
return limit_not_yet_reached! if seconds_to_wait.zero?
|
|
12
|
+
|
|
13
|
+
limit_reached!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def limit_reached!
|
|
19
|
+
debug { "Wait #{seconds_to_wait}/#{waiting_period} sec for #{number_of_incidents} incidents" }
|
|
20
|
+
public_message = I18n.t('booth.try_again_cooldown', distance_of_time_until_cooldown:)
|
|
21
|
+
|
|
22
|
+
::Booth::Cooldowns::Strategies::Result.failure(
|
|
23
|
+
public_message:,
|
|
24
|
+
attempts_left: 999_999,
|
|
25
|
+
cooldown_at:,
|
|
26
|
+
number_of_incidents:
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def limit_not_yet_reached!
|
|
31
|
+
debug { 'No need to wait' }
|
|
32
|
+
|
|
33
|
+
::Booth::Cooldowns::Strategies::Result.success(
|
|
34
|
+
public_message: nil,
|
|
35
|
+
attempts_left: 999_999,
|
|
36
|
+
number_of_incidents:
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Calculation Helpers
|
|
41
|
+
|
|
42
|
+
def cooldown_at
|
|
43
|
+
seconds_to_wait.from_now
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def seconds_to_wait
|
|
47
|
+
return 0 unless newest_timestamp
|
|
48
|
+
|
|
49
|
+
candidate = newest_timestamp.to_i + waiting_period - Time.current.to_i
|
|
50
|
+
return 0 if candidate.negative?
|
|
51
|
+
|
|
52
|
+
candidate
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def waiting_period
|
|
56
|
+
return 2.years if number_of_incidents > 9
|
|
57
|
+
|
|
58
|
+
# This effectively implies less than 10 attempts.
|
|
59
|
+
(5**number_of_incidents).seconds
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Queries
|
|
63
|
+
|
|
64
|
+
def number_of_incidents
|
|
65
|
+
@number_of_incidents ||= scope.count
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def newest_timestamp
|
|
69
|
+
return @newest_timestamp if defined? @newest_timestamp
|
|
70
|
+
|
|
71
|
+
@newest_timestamp = scope.maximum(:created_at)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Helpers
|
|
75
|
+
|
|
76
|
+
def distance_of_time_until_cooldown
|
|
77
|
+
::Booth::Cooldowns::DistanceOfTime.call(from: Time.current, till: cooldown_at)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Cooldowns
|
|
3
|
+
module Strategies
|
|
4
|
+
class Global
|
|
5
|
+
include ::Booth::MethodObject
|
|
6
|
+
include ::Booth::Logging
|
|
7
|
+
|
|
8
|
+
option :scope
|
|
9
|
+
option :max_attempts
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
if number_of_incidents >= max_attempts
|
|
13
|
+
limit_reached!
|
|
14
|
+
else
|
|
15
|
+
limit_not_yet_reached!
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def limit_reached!
|
|
22
|
+
debug { "Limited globally #{number_of_incidents}/#{max_attempts}" }
|
|
23
|
+
|
|
24
|
+
::Booth::Cooldowns::Strategies::Result.failure(
|
|
25
|
+
public_message: I18n.t('booth.permanently_blocked'),
|
|
26
|
+
attempts_left:,
|
|
27
|
+
cooldown_at: nil,
|
|
28
|
+
number_of_incidents:
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def limit_not_yet_reached!
|
|
33
|
+
debug { "Not yet globally limited #{number_of_incidents}/#{max_attempts}" }
|
|
34
|
+
|
|
35
|
+
::Booth::Cooldowns::Strategies::Result.success(
|
|
36
|
+
public_message:,
|
|
37
|
+
attempts_left:,
|
|
38
|
+
number_of_incidents:
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def public_message
|
|
43
|
+
return if number_of_incidents.zero?
|
|
44
|
+
|
|
45
|
+
if attempts_left == 1
|
|
46
|
+
I18n.t 'booth.last_attempt'
|
|
47
|
+
else
|
|
48
|
+
I18n.t 'booth.attempts_left', attempts_left:
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def attempts_left
|
|
53
|
+
max_attempts - number_of_incidents
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def number_of_incidents
|
|
57
|
+
@number_of_incidents ||= scope.count
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Cooldowns
|
|
3
|
+
module Strategies
|
|
4
|
+
# All strategies quack the same way.
|
|
5
|
+
module Result
|
|
6
|
+
def self.failure(number_of_incidents:, public_message:, cooldown_at:, attempts_left:)
|
|
7
|
+
Tron.failure :hot, public_message:,
|
|
8
|
+
cooldown_at:,
|
|
9
|
+
attempts_left:,
|
|
10
|
+
number_of_incidents:
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.success(public_message:, number_of_incidents:, attempts_left:)
|
|
14
|
+
Tron.success :cool, number_of_incidents:,
|
|
15
|
+
cooldown_at: nil,
|
|
16
|
+
public_message:,
|
|
17
|
+
attempts_left:
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|