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
data/lib/booth/errors.rb
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Errors
|
|
3
|
+
class Error < ::StandardError
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
# --------------------
|
|
7
|
+
# Categories of Errors
|
|
8
|
+
# --------------------
|
|
9
|
+
|
|
10
|
+
# Developer integrated Booth in a wrong way.
|
|
11
|
+
class Integration < ::Booth::Errors::Error
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Browser submits invalid data.
|
|
15
|
+
class BadRequest < ::Booth::Errors::Error
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Something inside of Booth went wrong.
|
|
19
|
+
class Internal < ::Booth::Errors::Error
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# ------------------------------
|
|
23
|
+
# Developer integration mistakes
|
|
24
|
+
# ------------------------------
|
|
25
|
+
|
|
26
|
+
class InvalidScopeSyntax < ::Booth::Errors::Integration
|
|
27
|
+
def initialize(scope)
|
|
28
|
+
super("Invalid scope name: #{scope.inspect}")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class NotAuthenticated < ::Booth::Errors::Integration
|
|
33
|
+
def initialize(message = 'This controller action must only be reachable for authenticated users. ' \
|
|
34
|
+
'Protect it for example with `before_action :require_authentication`')
|
|
35
|
+
super
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class MissingRelyingParty < ::Booth::Errors::Integration
|
|
40
|
+
def initialize(message = 'Please configure a name for the relying party in the `webauth` gem. ' \
|
|
41
|
+
"For example `WebAuthn.configuration.rp_name = 'My Homepage'`")
|
|
42
|
+
super
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# -----------
|
|
47
|
+
# Bad Request
|
|
48
|
+
# -----------
|
|
49
|
+
|
|
50
|
+
class MustBeHtml < ::Booth::Errors::BadRequest
|
|
51
|
+
def initialize(message = 'This feature is only reachable in the format HTML')
|
|
52
|
+
super
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class MustBeJson < ::Booth::Errors::BadRequest
|
|
57
|
+
def initialize(message = 'This feature is only reachable in the format JSON')
|
|
58
|
+
super
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class MustBeGet < ::Booth::Errors::BadRequest
|
|
63
|
+
def initialize(message = 'This feature is only reachable via GET')
|
|
64
|
+
super
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class MustBePost < ::Booth::Errors::BadRequest
|
|
69
|
+
def initialize(message = 'This feature is only reachable via POST')
|
|
70
|
+
super
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class MustBePatch < ::Booth::Errors::BadRequest
|
|
75
|
+
def initialize(message = 'This feature is only reachable via PATCH')
|
|
76
|
+
super
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class MustBeDelete < ::Booth::Errors::BadRequest
|
|
81
|
+
def initialize(message = 'This feature is only reachable via DELETE')
|
|
82
|
+
super
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Geolocation
|
|
3
|
+
def self.lookup(ip)
|
|
4
|
+
return 'localhost' if ip.to_s == '127.0.0.1' || ip.to_s == '::1'
|
|
5
|
+
|
|
6
|
+
record = reader&.city(ip.to_s)
|
|
7
|
+
(record&.city || record&.subdivision || record&.country || record&.continent)&.name
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.reader
|
|
11
|
+
return unless defined?(MaxMind::GeoIP2::Reader)
|
|
12
|
+
|
|
13
|
+
@reader ||= MaxMind::GeoIP2::Reader.new(database: database_path.to_s)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.database_path
|
|
17
|
+
Rails.root.join('db/GeoLite2-City.mmdb')
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Hooks
|
|
3
|
+
class AfterFetch
|
|
4
|
+
include ::Booth::MethodObject
|
|
5
|
+
include ::Booth::Logging
|
|
6
|
+
|
|
7
|
+
option :passport
|
|
8
|
+
option :warden
|
|
9
|
+
option :options
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
if passport.revoked_at
|
|
13
|
+
debug { "Your session in scope #{scope.inspect} was revoked at #{passport.revoked_at}. Logging you out." }
|
|
14
|
+
request.authentication.logout
|
|
15
|
+
else
|
|
16
|
+
debug { "Registering activity of legitimate session in scope #{scope.inspect} with IP #{request.ip}..." }
|
|
17
|
+
register_activity
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def register_activity
|
|
26
|
+
::Booth::Models::Session.where(id: passport.id).update_all(
|
|
27
|
+
['activity_at = ?, ' \
|
|
28
|
+
'agent = ?, ' \
|
|
29
|
+
'location = ?, ' \
|
|
30
|
+
'most_recent_ip = ?, ' \
|
|
31
|
+
'historical_locations = historical_locations || hstore(?, ?), ' \
|
|
32
|
+
'historical_ips = historical_ips || hstore(?, ?)',
|
|
33
|
+
Time.current,
|
|
34
|
+
request.agent,
|
|
35
|
+
request.location,
|
|
36
|
+
request.ip,
|
|
37
|
+
request.ip,
|
|
38
|
+
request.location,
|
|
39
|
+
request.ip,
|
|
40
|
+
Time.current.to_i.to_s]
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def request
|
|
45
|
+
@request ||= ::Booth::Request.new(request: warden.request,
|
|
46
|
+
scope:)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def scope
|
|
50
|
+
options[:scope]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Hooks
|
|
3
|
+
class BeforeLogout
|
|
4
|
+
include ::Booth::MethodObject
|
|
5
|
+
include ::Booth::Logging
|
|
6
|
+
|
|
7
|
+
option :passport
|
|
8
|
+
option :warden
|
|
9
|
+
option :options
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
return unless passport # Attempting to logout when already logged out.
|
|
13
|
+
|
|
14
|
+
debug { "Revoking Session with ID #{passport.id} in scope #{scope.inspect} because of logout" }
|
|
15
|
+
|
|
16
|
+
::Booth::Models::Session.where(id: passport.id)
|
|
17
|
+
.update_all(revoked_at: Time.current, revoke_reason: :logout)
|
|
18
|
+
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def scope
|
|
25
|
+
options[:scope]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Hooks
|
|
3
|
+
class SerializeFromSession
|
|
4
|
+
include ::Booth::MethodObject
|
|
5
|
+
include ::Booth::Logging
|
|
6
|
+
|
|
7
|
+
param :session_id
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
::Booth::Syntaxes::Uuid.call(session_id, raise_if_invalid: true)
|
|
11
|
+
|
|
12
|
+
session = ::Booth::Models::Session.active_scope.find_by(id: session_id)
|
|
13
|
+
|
|
14
|
+
unless session
|
|
15
|
+
debug { "Session ID #{session_id.inspect} stored in the cookie, but doesn't exist in the database" }
|
|
16
|
+
return
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
debug { "Deserializing Session #{session_id.inspect} information into Warden..." }
|
|
20
|
+
::Booth::Sessions::ToPassport.call(session)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/booth/logger.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
# TODO: Not sure we need this. Do we need this, or can `Logging` do the delegation
|
|
3
|
+
# to the upstream logger by itself?
|
|
4
|
+
class Logger
|
|
5
|
+
def initialize(thing)
|
|
6
|
+
@name = if thing == Class || thing.is_a?(Module) || thing.is_a?(String)
|
|
7
|
+
thing.to_s
|
|
8
|
+
else
|
|
9
|
+
thing.class.to_s
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def debug(&)
|
|
14
|
+
logger&.debug(name, &)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def info(&)
|
|
18
|
+
logger&.info(name, &)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def warn(&)
|
|
22
|
+
logger&.warn(name, &)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def error(&)
|
|
26
|
+
logger&.error(name, &)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def fatal(&)
|
|
30
|
+
logger&.fatal(name, &)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
attr_reader :name
|
|
36
|
+
|
|
37
|
+
def logger
|
|
38
|
+
::Booth.config.logger
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Logging
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
# TODO: Implement only generic `log` method to avoid cluttering the Object where this was included.
|
|
6
|
+
# We only use `debug` anyway.
|
|
7
|
+
class_methods do
|
|
8
|
+
def debug(&)
|
|
9
|
+
_logger.debug(&)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def info(&)
|
|
13
|
+
_logger.info(&)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def warn(&)
|
|
17
|
+
_logger.warn(&)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def error(&)
|
|
21
|
+
_logger.error(&)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def fatal(&)
|
|
25
|
+
_logger.fatal(&)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def _logger
|
|
29
|
+
::Booth::Logger.new(self)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def debug(&)
|
|
36
|
+
_logger.debug(&)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def info(&)
|
|
40
|
+
_logger.info(&)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def warn(&)
|
|
44
|
+
_logger.warn(&)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def error(&)
|
|
48
|
+
_logger.error(&)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def fatal(&)
|
|
52
|
+
_logger.fatal(&)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def _logger
|
|
56
|
+
@_logger ||= ::Booth::Logger.new(self)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Copyright 2018 Bukowskis https://github.com/bukowskis/method_object
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
# a copy of this software and associated documentation files (the
|
|
5
|
+
# "Software"), to deal in the Software without restriction, including
|
|
6
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
# the following conditions:
|
|
10
|
+
#
|
|
11
|
+
# The above copyright notice and this permission notice shall be
|
|
12
|
+
# included in all copies or substantial portions of the Software.
|
|
13
|
+
#
|
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
21
|
+
|
|
22
|
+
module Booth
|
|
23
|
+
module MethodObject
|
|
24
|
+
def self.included(base)
|
|
25
|
+
base.extend Dry::Initializer
|
|
26
|
+
base.extend ClassMethods
|
|
27
|
+
base.send(:private_class_method, :new)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
module ClassMethods
|
|
31
|
+
def call(*args, **kwargs, &)
|
|
32
|
+
__check_for_unknown_options(*args, **kwargs)
|
|
33
|
+
|
|
34
|
+
if kwargs.empty?
|
|
35
|
+
# Preventing `Passing the keyword argument as the last hash parameter is deprecated`
|
|
36
|
+
new(*args).call(&)
|
|
37
|
+
else
|
|
38
|
+
new(*args, **kwargs).call(&)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Overriding the implementation of `#param` in the `dry-initializer` gem.
|
|
43
|
+
# Because of the positioning of multiple params, params can never be omitted in a method object.
|
|
44
|
+
def param(name, type = nil, **opts, &)
|
|
45
|
+
raise ArgumentError, "Default value for param not allowed - #{name}" if opts.key? :default
|
|
46
|
+
raise ArgumentError, "Optional params not supported - #{name}" if opts.fetch(:optional, false)
|
|
47
|
+
|
|
48
|
+
super
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def __check_for_unknown_options(*args, **kwargs)
|
|
52
|
+
return if __defined_options.empty?
|
|
53
|
+
|
|
54
|
+
# Checking params
|
|
55
|
+
opts = args.drop(__defined_params.length).first || kwargs
|
|
56
|
+
raise ArgumentError, "Unexpected argument #{opts}" unless opts.is_a? Hash
|
|
57
|
+
|
|
58
|
+
# Checking options
|
|
59
|
+
unknown_options = opts.keys - __defined_options
|
|
60
|
+
message = "Key(s) #{unknown_options} not found in #{__defined_options}"
|
|
61
|
+
raise KeyError, message if unknown_options.any?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def __defined_options
|
|
65
|
+
dry_initializer.options.map(&:source)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def __defined_params
|
|
69
|
+
dry_initializer.params.map(&:source)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/booth/mode.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Mode
|
|
3
|
+
def self.find(symbol)
|
|
4
|
+
return if symbol.to_s == 'first_time'
|
|
5
|
+
|
|
6
|
+
all.detect { _1.id.to_s == symbol.to_s } || raise("Unknown Booth Mode: #{symbol.inspect}")
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.wrap(input)
|
|
10
|
+
Array(input).map { find(_1) }.sort
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.all
|
|
14
|
+
[
|
|
15
|
+
::Booth::Modes::UsernameAndWebauth, # Recommended for convenience
|
|
16
|
+
::Booth::Modes::UsernamePasswordAndWebauth, # Most secure
|
|
17
|
+
::Booth::Modes::UsernamePasswordAndOtp, # If you don't have a hardware key
|
|
18
|
+
::Booth::Modes::UsernameAndPassword, # Discouraged
|
|
19
|
+
]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Models
|
|
3
|
+
class Audit < ::Booth::Models::ApplicationRecord
|
|
4
|
+
self.table_name = 'booth_audits'
|
|
5
|
+
|
|
6
|
+
enum event: { added_otp: 'added_otp',
|
|
7
|
+
changed_otp: 'changed_otp',
|
|
8
|
+
completed_onboarding: 'completed_onboarding',
|
|
9
|
+
entered_correct_password: 'entered_correct_password',
|
|
10
|
+
entered_wrong_otp: 'entered_wrong_otp',
|
|
11
|
+
entered_wrong_password: 'entered_wrong_password',
|
|
12
|
+
logout: 'logout',
|
|
13
|
+
queried_unknown_username: 'queried_unknown_username',
|
|
14
|
+
requested_password_reset: 'requested_password_reset' },
|
|
15
|
+
_prefix: true
|
|
16
|
+
|
|
17
|
+
belongs_to :credential, class_name: '::Booth::Models::Credential', optional: true
|
|
18
|
+
|
|
19
|
+
validates :ip, :event, presence: true
|
|
20
|
+
|
|
21
|
+
scope :visible_scope, -> { where(deleted_at: nil) }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Models
|
|
3
|
+
class Authenticator < ::Booth::Models::ApplicationRecord
|
|
4
|
+
include ::Booth::Logging
|
|
5
|
+
|
|
6
|
+
self.table_name = 'booth_authenticators'
|
|
7
|
+
|
|
8
|
+
belongs_to :credential, class_name: '::Booth::Models::Credential'
|
|
9
|
+
|
|
10
|
+
validates :credential_id, uniqueness: { scope: :webauthn_id }
|
|
11
|
+
validates :webauthn_id, presence: true, uniqueness: true
|
|
12
|
+
validates :nickname, length: { minimum: 3, maximum: 40 }, allow_blank: true
|
|
13
|
+
|
|
14
|
+
before_validation :ensure_webauthn_id
|
|
15
|
+
# before_validation :derive_registration_challenge
|
|
16
|
+
# before_destroy :keep_at_least_one_authenticator
|
|
17
|
+
|
|
18
|
+
scope :registered_scope, -> { where.not(confirmed_at: nil) }
|
|
19
|
+
scope :supports_user_verification_scope, -> { where(supports_user_verification: true) }
|
|
20
|
+
scope :sorted_scope, -> { order(:nickname) }
|
|
21
|
+
|
|
22
|
+
def generate_webauth_id
|
|
23
|
+
self.webauthn_id = ::WebAuthn.generate_user_id
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def confirmed?
|
|
27
|
+
confirmed_at.present?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def step
|
|
31
|
+
::Booth::Authenticators::Step.call(self)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# delegate :recommended_user_verification, to: :credential
|
|
37
|
+
|
|
38
|
+
def ensure_webauthn_id
|
|
39
|
+
return if webauthn_id.present?
|
|
40
|
+
|
|
41
|
+
generate_webauth_id
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Models
|
|
3
|
+
module Concerns
|
|
4
|
+
module Modeable
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
enum mode: { first_time: 'first_time',
|
|
9
|
+
username_and_password: 'username_and_password',
|
|
10
|
+
username_password_and_otp: 'username_password_and_otp',
|
|
11
|
+
username_password_and_webauth: 'username_password_and_webauth',
|
|
12
|
+
username_and_webauth: 'username_and_webauth' },
|
|
13
|
+
_prefix: true
|
|
14
|
+
|
|
15
|
+
validates :mode, presence: true, inclusion: { in: modes.keys }
|
|
16
|
+
validates :mode, inclusion: { in: -> { ['first_time'] + _1.allowed_modes } }
|
|
17
|
+
|
|
18
|
+
before_validation :ensure_mode
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def requires_user_verification?
|
|
22
|
+
# Passwordless login should always require user presence
|
|
23
|
+
!mode_username_password_and_webauth?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def allows_mode_username_and_password?
|
|
27
|
+
allowed_modes.include?('username_and_password')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def allows_mode_username_password_and_otp?
|
|
31
|
+
allowed_modes.include?('username_password_and_otp')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def allows_mode_username_password_and_webauth?
|
|
35
|
+
allowed_modes.include?('username_password_and_webauth')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def allows_mode_username_and_webauth?
|
|
39
|
+
allowed_modes.include?('username_and_webauth')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def ensure_mode
|
|
45
|
+
self.mode ||= 'first_time'
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Models
|
|
3
|
+
module Concerns
|
|
4
|
+
module Otpable
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
# See https://github.com/heapsource/active_model_otp/blob/master/lib/active_model/one_time_password.rb
|
|
9
|
+
has_one_time_password length: otp_digits
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class_methods do
|
|
13
|
+
def otp_digits
|
|
14
|
+
6
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def authenticate_otp(code)
|
|
19
|
+
super(code, drift: 30.seconds)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def otp_provisioning_url
|
|
23
|
+
raise "Expected #{self} to respond to #scope" unless respond_to?(:scope)
|
|
24
|
+
|
|
25
|
+
provisioning_uri(username, issuer: ::Booth.config.otp_issuer(scope:), digits: otp_digits)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def otp_provisioning_svg
|
|
29
|
+
qr = ::RQRCode::QRCode.new(otp_provisioning_url, level: :l)
|
|
30
|
+
# See https://whomwah.github.io/rqrcode/
|
|
31
|
+
qr.as_svg use_path: false,
|
|
32
|
+
viewbox: true, fill: 'fff', offset: 5, module_size: 8, svg_attributes: { 'data-booth' => :otpqr }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Models
|
|
3
|
+
module Concerns
|
|
4
|
+
module Passwordable
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
# See https://github.com/rails/rails/blob/master/activemodel/lib/active_model/secure_password.rb
|
|
9
|
+
has_secure_password
|
|
10
|
+
|
|
11
|
+
define_method('password=') do |unencrypted_password|
|
|
12
|
+
# We auto-assign a random password before validation.
|
|
13
|
+
# If a non-empty password was explicitly assigned, then we should not generate a random password.
|
|
14
|
+
@apparently_wants_no_password = unencrypted_password.blank?
|
|
15
|
+
|
|
16
|
+
# See https://github.com/rails/rails/issues/34348#issuecomment-615856794
|
|
17
|
+
super(unencrypted_password.presence)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
validates :password, length: { minimum: password_minlength }, allow_blank: true
|
|
21
|
+
# Don't say anything about breaches if the password was already too short to be accepted.
|
|
22
|
+
validates :password, not_pwned: {
|
|
23
|
+
on_error: :valid,
|
|
24
|
+
request_options: {
|
|
25
|
+
read_timeout: 3,
|
|
26
|
+
open_timeout: 1
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
allow_blank: true,
|
|
30
|
+
if: proc { _1.errors.empty? }
|
|
31
|
+
|
|
32
|
+
before_validation :ensure_initial_password
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class_methods do
|
|
36
|
+
def password_minlength
|
|
37
|
+
8
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# See https://support.1password.com/compatible-website-design
|
|
41
|
+
# See https://developer.apple.com/password-rules
|
|
42
|
+
def passwordrules
|
|
43
|
+
"minlength: #{password_minlength}; maxlength: #{::ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED};"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def ensure_initial_password
|
|
50
|
+
return if password_digest.present?
|
|
51
|
+
return if @apparently_wants_no_password
|
|
52
|
+
|
|
53
|
+
self.password = ::SecureRandom.alphanumeric(::ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|