diff --git a/components/Account.php b/components/Account.php index 9ed31205..adfd7f37 100644 --- a/components/Account.php +++ b/components/Account.php @@ -14,6 +14,7 @@ use October\Rain\Auth\AuthException; use Cms\Classes\Page; use Cms\Classes\ComponentBase; +use RainLab\User\Models\User as UserModel; use RainLab\User\Models\Settings as UserSettings; use Exception; @@ -234,6 +235,13 @@ public function onSignin() throw new AuthException(/*Sorry, this user is currently not activated. Please contact us for further assistance.*/'rainlab.user::lang.account.banned'); } + /* + * Record IP address + */ + if ($ipAddress = Request::ip()) { + $user->touchIpAddress($ipAddress); + } + /* * Redirect */ @@ -257,6 +265,10 @@ public function onRegister() throw new ApplicationException(Lang::get(/*Registrations are currently disabled.*/'rainlab.user::lang.account.registration_disabled')); } + if ($this->isRegisterThrottled()) { + throw new ApplicationException(Lang::get(/*Registration is throttled. Please try again later.*/'rainlab.user::lang.account.registration_throttled')); + } + /* * Validate input */ @@ -280,6 +292,13 @@ public function onRegister() throw new ValidationException($validation); } + /* + * Record IP address + */ + if ($ipAddress = Request::ip()) { + $data['created_ip_address'] = $data['last_ip_address'] = $ipAddress; + } + /* * Register user */ @@ -563,4 +582,17 @@ protected function redirectForceSecure() return Redirect::secure(Request::path()); } + + /** + * Returns true if user is throttled. + * @return bool + */ + protected function isRegisterThrottled() + { + if (!UserSettings::get('use_register_throttle', false)) { + return false; + } + + return UserModel::isRegisterThrottled(Request::ip()); + } } diff --git a/lang/en/lang.php b/lang/en/lang.php index ef4facc3..67a1072f 100644 --- a/lang/en/lang.php +++ b/lang/en/lang.php @@ -89,6 +89,8 @@ 'require_activation_comment' => 'Users must have an activated account to sign in.', 'use_throttle' => 'Throttle attempts', 'use_throttle_comment' => 'Repeat failed sign in attempts will temporarily suspend the user.', + 'use_register_throttle' => 'Throttle registration', + 'use_register_throttle_comment' => 'Prevent multiple registrations from the same IP in short succession.', 'block_persistence' => 'Prevent concurrent sessions', 'block_persistence_comment' => 'When enabled users cannot sign in to multiple devices at the same time.', 'login_attribute' => 'Login attribute', @@ -132,6 +134,8 @@ 'status_guest' => 'Guest', 'status_activated' => 'Activated', 'status_registered' => 'Registered', + 'created_ip_address' => 'Created IP Address', + 'last_ip_address' => 'Last IP Address', ], 'group' => [ 'label' => 'Group', @@ -183,6 +187,7 @@ 'already_active' => 'Your account is already activated!', 'activation_email_sent' => 'An activation email has been sent to your email address.', 'registration_disabled' => 'Registrations are currently disabled.', + 'registration_throttled' => 'Registration is throttled. Please try again later.', 'sign_in' => 'Sign in', 'register' => 'Register', 'full_name' => 'Full Name', diff --git a/models/Settings.php b/models/Settings.php index 89274268..0feb1d59 100644 --- a/models/Settings.php +++ b/models/Settings.php @@ -38,6 +38,7 @@ public function initSettingsData() $this->update_requires_password = false; $this->remember_login = self::REMEMBER_ALWAYS; $this->min_password_length = self::MIN_PASSWORD_LENGTH_DEFAULT; + $this->use_register_throttle = true; } public function getActivateModeOptions() diff --git a/models/User.php b/models/User.php index 999ae36a..38760a9d 100644 --- a/models/User.php +++ b/models/User.php @@ -4,6 +4,7 @@ use Auth; use Mail; use Event; +use Carbon\Carbon; use October\Rain\Auth\Models\User as UserBase; use RainLab\User\Models\Settings as UserSettings; use October\Rain\Auth\AuthException; @@ -49,7 +50,9 @@ class User extends UserBase 'username', 'email', 'password', - 'password_confirmation' + 'password_confirmation', + 'created_ip_address', + 'last_ip_address' ]; /** @@ -354,6 +357,46 @@ public function isBanned() return $throttle ? $throttle->is_banned : false; } + // + // IP Recording and Throttle + // + + /** + * Records the last_ip_address to reflect the last known IP for this user. + * @param string|null $ipAddress + * @return void + */ + public function touchIpAddress($ipAddress) + { + $this + ->newQuery() + ->where('id', $this->id) + ->update(['last_ip_address' => $ipAddress]) + ; + } + + /** + * Returns true if IP address is throttled and cannot register + * again. Maximum 3 registrations every 60 minutes. + * @param string|null $ipAddress + * @return bool + */ + public static function isRegisterThrottled($ipAddress) + { + if (!$ipAddress) { + return false; + } + + $timeLimit = Carbon::now()->subMinutes(60); + $count = static::make() + ->where('created_ip_address', $ipAddress) + ->where('created_at', '>', $timeLimit) + ->count() + ; + + return $count > 2; + } + // // Last Seen // diff --git a/models/settings/fields.yaml b/models/settings/fields.yaml index 470ec100..735a6e54 100644 --- a/models/settings/fields.yaml +++ b/models/settings/fields.yaml @@ -30,7 +30,7 @@ tabs: # Remeber Login Mode remember_login: - span: left + span: right label: rainlab.user::lang.settings.remember_login commentAbove: rainlab.user::lang.settings.remember_login_comment type: radio @@ -44,6 +44,14 @@ tabs: type: switch tab: rainlab.user::lang.settings.registration_tab + # Enable registration throttling + use_register_throttle: + span: right + label: rainlab.user::lang.settings.use_register_throttle + comment: rainlab.user::lang.settings.use_register_throttle_comment + type: switch + tab: rainlab.user::lang.settings.registration_tab + # Minimum password length min_password_length: span: left diff --git a/models/user/columns.yaml b/models/user/columns.yaml index 9b091b77..f3c27be9 100644 --- a/models/user/columns.yaml +++ b/models/user/columns.yaml @@ -38,3 +38,13 @@ columns: label: rainlab.user::lang.user.is_guest type: switch invisible: true + + created_ip_address: + label: rainlab.user::lang.user.created_ip_address + searchable: true + invisible: true + + last_ip_address: + label: rainlab.user::lang.user.last_ip_address + searchable: true + invisible: true diff --git a/models/user/fields.yaml b/models/user/fields.yaml index 78a0e078..7b9d394a 100644 --- a/models/user/fields.yaml +++ b/models/user/fields.yaml @@ -70,6 +70,20 @@ tabs: type: relation emptyOption: rainlab.user::lang.user.empty_groups + created_ip_address: + label: rainlab.user::lang.user.created_ip_address + span: auto + disabled: true + tab: rainlab.user::lang.user.account + context: preview + + last_ip_address: + label: rainlab.user::lang.user.last_ip_address + span: auto + disabled: true + tab: rainlab.user::lang.user.account + context: preview + secondaryTabs: fields: diff --git a/updates/users_add_ip_address.php b/updates/users_add_ip_address.php new file mode 100644 index 00000000..8b307cce --- /dev/null +++ b/updates/users_add_ip_address.php @@ -0,0 +1,28 @@ +string('created_ip_address')->nullable(); + $table->string('last_ip_address')->nullable(); + }); + } + + public function down() + { + if (Schema::hasColumn('users', 'created_ip_address')) { + Schema::table('users', function($table) + { + $table->dropColumn('created_ip_address'); + $table->dropColumn('last_ip_address'); + }); + } + } +} diff --git a/updates/version.yaml b/updates/version.yaml index 9a7ce8e1..980d2185 100644 --- a/updates/version.yaml +++ b/updates/version.yaml @@ -68,3 +68,6 @@ 1.4.7: Fixes redirect bug in Account component / Update translations and separate user and group management. 1.4.8: Fixes a bug where calling MailBlocker::removeBlock could remove all mail blocks for the user. 1.5.0: !!! Required password length is now a minimum of 8 characters. Previous passwords will not be affected until the next password change. +1.5.1: + - User IP addresses are now logged. Introduce registration throttle and "remember login" functionality. + - users_add_ip_address.php