├── .gitignore ├── LICENCE.md ├── Plugin.php ├── README.md ├── UPGRADE.md ├── assets └── images │ ├── no-user-avatar.png │ └── user-icon.svg ├── classes ├── Agent.php ├── AuthManager.php ├── AuthMiddleware.php ├── PasswordRule.php ├── SessionGuard.php ├── TwoFactorManager.php ├── UserProvider.php └── sessionguard │ ├── HasBearerToken.php │ ├── HasImpersonation.php │ ├── HasLegacyApi.php │ └── HasPersistence.php ├── components ├── Account.php ├── Authentication.php ├── Registration.php ├── ResetPassword.php ├── Session.php ├── account │ ├── ActionBrowserSessions.php │ ├── ActionDeleteUser.php │ ├── ActionTwoFactor.php │ ├── ActionVerifyEmail.php │ └── default.htm ├── authentication │ ├── ActionLogin.php │ ├── ActionRecoverPassword.php │ ├── ActionTwoFactorLogin.php │ └── default.htm ├── registration │ └── default.htm └── resetpassword │ ├── ActionChangePassword.php │ ├── ActionResetPassword.php │ └── default.htm ├── composer.json ├── config ├── auth.php └── config.php ├── console └── MigrateV1Command.php ├── contentfields ├── UsersField.php └── usersfield │ └── partials │ ├── _column_multi.php │ └── _column_single.php ├── controllers ├── Timelines.php ├── UserGroups.php ├── Users.php ├── timelines │ ├── _list_toolbar.php │ ├── config_list.yaml │ └── index.php ├── usergroups │ ├── _list_toolbar.php │ ├── config_form.yaml │ ├── config_list.yaml │ ├── create.php │ ├── index.php │ └── update.php └── users │ ├── HasBulkActions.php │ ├── HasEditActions.php │ ├── _convert_guest_form.php │ ├── _hint_activate.php │ ├── _hint_banned.php │ ├── _hint_guest.php │ ├── _hint_trashed.php │ ├── _list_toolbar.php │ ├── _scoreboard_preview.php │ ├── config_form.yaml │ ├── config_import_export.yaml │ ├── config_list.yaml │ ├── config_relation.yaml │ ├── create.php │ ├── export.php │ ├── import.php │ ├── index.php │ ├── preview.php │ └── update.php ├── docs ├── auth-bearer-tokens.md ├── auth-impersonation.md ├── auth-manager.md ├── component-account.md ├── component-authentication.md ├── component-registration.md ├── component-reset-password.md ├── component-session.md ├── docs-lock.json ├── docs.yaml ├── events.md ├── introduction.md └── tailor.md ├── helpers └── User.php ├── lang ├── ar.json ├── cs.json ├── de.json ├── en.json ├── es-ar.json ├── es.json ├── fa.json ├── fr.json ├── hu.json ├── id.json ├── it.json ├── kr.json ├── nb-no.json ├── nl.json ├── pl.json ├── pt-br.json ├── pt-pt.json ├── ru.json ├── si.json ├── sk.json ├── sl.json ├── sv.json ├── tr.json ├── zh-cn.json └── zh-tw.json ├── models ├── Setting.php ├── User.php ├── UserGroup.php ├── UserLog.php ├── UserPreference.php ├── setting │ └── fields.yaml ├── user │ ├── HasAuthenticatable.php │ ├── HasEmailVerification.php │ ├── HasModelAttributes.php │ ├── HasModelScopes.php │ ├── HasPasswordReset.php │ ├── HasPersistCode.php │ ├── HasTwoFactor.php │ ├── UserExport.php │ ├── UserImport.php │ ├── columns-export.yaml │ ├── columns-import.yaml │ ├── columns.yaml │ ├── fields-import.yaml │ ├── fields-preview.yaml │ ├── fields.yaml │ └── scopes.yaml ├── usergroup │ ├── columns.yaml │ ├── fields.yaml │ └── scopes.yaml └── userlog │ ├── HasModelAttributes.php │ ├── _column_detail.php │ ├── _detail_new_user.php │ ├── _detail_self_delete.php │ ├── _detail_self_login.php │ ├── _detail_self_verify.php │ ├── _detail_set_email.php │ ├── _detail_set_password.php │ ├── _detail_set_two_factor.php │ ├── columns-global.yaml │ ├── columns.yaml │ └── fields.yaml ├── phpunit.xml ├── tests └── AuthManagerTest.php ├── traits └── ConfirmsPassword.php ├── updates ├── 000001_create_users.php ├── 000002_create_password_resets.php ├── 000003_create_user_groups.php ├── 000004_create_user_preferences.php ├── 000005_create_user_logs.php ├── migrate_v3_0_0.php ├── migrate_v3_1_0.php ├── seed_tables.php └── version.yaml └── views └── mail ├── invite_email.htm ├── new_user_internal.htm ├── recover_password.htm ├── verify_email.htm └── welcome_email.htm /.gitignore: -------------------------------------------------------------------------------- 1 | .phpunit.result.cache 2 | composer.lock 3 | vendor 4 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | This plugin is an official extension of the October CMS platform and is free to use if you have a platform license. 4 | 5 | See End User License Agreement at https://octobercms.com/eula for more details. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Front-end user plugin 2 | 3 | Front-end user management for October CMS. View this plugin on the October CMS marketplace: 4 | 5 | - https://octobercms.com/plugin/rainlab-user 6 | 7 | ## Requirements 8 | 9 | - October CMS 3.6 or above 10 | - The [AJAX Framework](https://docs.octobercms.com/3.x/cms/ajax/introduction.html) to be included in your layout/page 11 | 12 | ## Installation Instructions 13 | 14 | Run the following to install this plugin: 15 | 16 | ```bash 17 | php artisan plugin:install RainLab.User 18 | ``` 19 | 20 | To uninstall this plugin: 21 | 22 | ```bash 23 | php artisan plugin:remove RainLab.User 24 | ``` 25 | 26 | ### Older Versions 27 | 28 | If you are using **October CMS v3.0 - v3.5**, install v2.1 with the following commands: 29 | 30 | ```bash 31 | composer require rainlab/user-plugin "^2.1" 32 | ``` 33 | 34 | If you are using **October CMS v1.0 - v2.3**, install v1.7 with the following commands: 35 | 36 | ```bash 37 | composer require rainlab/user-plugin "^1.7" 38 | ``` 39 | 40 | ### Sample Theme 41 | 42 | We recommend installing this plugin with the `RainLab.Vanilla` theme to demonstrate its functionality. 43 | 44 | - https://github.com/rainlab/vanilla-theme 45 | 46 | For extra functionality, consider also installing the `RainLab.UserPlus` plugin. 47 | 48 | - https://octobercms.com/plugin/rainlab-userplus 49 | 50 | ## Managing Users 51 | 52 | Users are managed on the Users tab found in the admin panel. Each user provides minimal data fields - **First Name**, **Last Name**, **Email** and **Password**. The Name can represent either the person's first name or their full name, making the Last Name field optional, depending on the complexity of your site. 53 | 54 | Below the **Email** field is an checkbox to block all outgoing mail sent to the user. This is a useful feature for accounts with an email address that is bouncing mail or has reported spam. When checked, no mail will ever be sent to this address, except for the mail template used for resetting the password. 55 | 56 | ## Plugin Settings 57 | 58 | This plugin creates a Settings menu item, found by navigating to **Settings > Users > User Settings**. This page allows the setting of common features, described in more detail below. 59 | 60 | #### Sign In 61 | 62 | By default a User will sign in to the site using their email address as a unique identifier. You may use a unique login name instead by changing the **Login Attribute** value to Username. This will introduce a new field called **Username** for each user, allowing them to specify their own short name or alias for identification. Both the Email address and Username must be unique to the user. 63 | 64 | As a security precaution, you may restrict users from having sessions across multiple devices at the same time. Enable the **Prevent Concurrent Sessions** setting to use this feature. When a user signs in to their account, it will automatically sign out the user for all other sessions. 65 | 66 | #### Registration 67 | 68 | Registration to the site is allowed by default. If you are running a closed site, or need to temporarily disable registration, you may disable this feature by switching **Allow user registration** to the OFF setting. 69 | 70 | #### Notifications 71 | 72 | When a user is first activated, they can be sent a welcome email. To activate the welcome email, select "Notify User" and an email template from the **User Message Template** dropdown. The default message template used is `user:welcome_email` and you can customize this by selecting **Mail > Mail Templates** from the settings menu. 73 | 74 | The same applies for notifying the system administrators when a new user joins, with the "Notify Administrators" checkbox. The administrators to notify are selected using **Notify Admin Group** dropdown. 75 | 76 | ## Documentation 77 | 78 | ### Getting Started 79 | 80 | - [Introduction](./docs/introduction.md) 81 | - [Events](./docs/events.md) 82 | 83 | ### Components 84 | 85 | - [Session](./docs/component-session.md) 86 | - [Account](./docs/component-account.md) 87 | - [Authentication](./docs/component-authentication.md) 88 | - [Registration](./docs/component-registration.md) 89 | - [Reset Password](./docs/component-reset-password.md) 90 | 91 | ### Services 92 | 93 | - [Auth Manager](./docs/auth-manager.md) 94 | - [Impersonation](./docs/auth-impersonation.md) 95 | - [Bearer Tokens](./docs/auth-bearer-tokens.md) 96 | 97 | ### License 98 | 99 | This plugin is an official extension of the October CMS platform and is free to use if you have a platform license. See [EULA license](LICENSE.md) for more details. 100 | -------------------------------------------------------------------------------- /assets/images/no-user-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rainlab/user-plugin/42d39617da6f0d1b768ad978c5e817b20c38f5ee/assets/images/no-user-avatar.png -------------------------------------------------------------------------------- /assets/images/user-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /classes/AuthManager.php: -------------------------------------------------------------------------------- 1 | createUserProvider($config['provider'] ?? null); 26 | 27 | $guard = new SessionGuard( 28 | $name, 29 | $provider, 30 | $this->app['session.store'], 31 | ); 32 | 33 | if (method_exists($guard, 'setCookieJar')) { 34 | $guard->setCookieJar($this->app['cookie']); 35 | } 36 | 37 | if (method_exists($guard, 'setDispatcher')) { 38 | $guard->setDispatcher($this->app['events']); 39 | } 40 | 41 | if (method_exists($guard, 'setRequest')) { 42 | $guard->setRequest($this->app->refresh('request', $guard, 'setRequest')); 43 | } 44 | 45 | if (isset($config['remember'])) { 46 | $guard->setRememberDuration($config['remember']); 47 | } 48 | 49 | return $guard; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /classes/AuthMiddleware.php: -------------------------------------------------------------------------------- 1 | bearerToken()) { 18 | Auth::loginUsingBearerToken($jwtToken); 19 | } 20 | 21 | if (!Auth::check()) { 22 | return Response::make('Forbidden', 403); 23 | } 24 | 25 | return $next($request); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /classes/PasswordRule.php: -------------------------------------------------------------------------------- 1 | min = $length; 19 | 20 | return $this; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /classes/SessionGuard.php: -------------------------------------------------------------------------------- 1 | user(); 35 | 36 | if (!Hash::check($password, $user->{$attribute})) { 37 | throw new InvalidArgumentException('The given password does not match the current password.'); 38 | } 39 | 40 | // Hashed by Hashable trait 41 | $user->{$attribute} = $password; 42 | $user->save(['force' => true]); 43 | 44 | return $user; 45 | } 46 | 47 | /** 48 | * login user to the application. 49 | * 50 | * @param \Illuminate\Contracts\Auth\Authenticatable $user 51 | * @param bool $remember 52 | */ 53 | public function login(Authenticatable $user, $remember = false) 54 | { 55 | if ($user->is_banned) { 56 | throw new ValidationException(['password' => __("Your account is locked. Please contact the site administrator.")]); 57 | } 58 | 59 | $this->preventConcurrentSessions($user); 60 | 61 | parent::login($user, $remember); 62 | } 63 | 64 | /** 65 | * Update the session with the given ID. 66 | * 67 | * @param string $id 68 | * @return void 69 | */ 70 | protected function updateSession($id) 71 | { 72 | if ($this->viaRemember && $this->user) { 73 | $this->updatePersistSession($this->user); 74 | } 75 | 76 | parent::updateSession($id); 77 | } 78 | 79 | /** 80 | * loginQuietly logs a user into the application without firing the Login event. 81 | */ 82 | public function loginQuietly(Authenticatable $user) 83 | { 84 | $this->updatePersistSession($user); 85 | 86 | $this->updateSession($user->getAuthIdentifier()); 87 | 88 | $this->setUser($user); 89 | } 90 | 91 | /** 92 | * user gets the currently authenticated user. 93 | */ 94 | public function user(): ?User 95 | { 96 | $user = parent::user(); 97 | 98 | if (!$user) { 99 | return null; 100 | } 101 | 102 | if ($user->is_banned) { 103 | return null; 104 | } 105 | 106 | if (!$this->viaBearerToken && !$this->hasValidPersistCode($user)) { 107 | return null; 108 | } 109 | 110 | return $user; 111 | } 112 | 113 | /** 114 | * logoutQuietly logs out the user without updating remember_token and 115 | * without firing the Logout event. 116 | */ 117 | public function logoutQuietly() 118 | { 119 | $this->clearUserDataFromStorage(); 120 | 121 | $this->user = null; 122 | 123 | $this->loggedOut = true; 124 | } 125 | 126 | /** 127 | * clearUserDataFromStorage removes the user data from the session and cookies. 128 | */ 129 | protected function clearUserDataFromStorage() 130 | { 131 | $this->session->remove($this->getPersistCodeName()); 132 | 133 | parent::clearUserDataFromStorage(); 134 | } 135 | 136 | /** 137 | * getName as a unique identifier for the auth session value. 138 | */ 139 | public function getName() 140 | { 141 | return 'user_login'; 142 | } 143 | 144 | /** 145 | * getRecallerName of the cookie used to store the "recaller". 146 | */ 147 | public function getRecallerName() 148 | { 149 | return 'user_auth'; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /classes/TwoFactorManager.php: -------------------------------------------------------------------------------- 1 | engine = new Google2FA; 27 | } 28 | 29 | /** 30 | * instance creates a new instance of this singleton 31 | */ 32 | public static function instance(): static 33 | { 34 | return App::make('user.twofactor'); 35 | } 36 | 37 | /** 38 | * generateSecretKey 39 | */ 40 | public function generateSecretKey(): string 41 | { 42 | return $this->engine->generateSecretKey(); 43 | } 44 | 45 | /** 46 | * qrCodeUrl returns the two factor authentication QR code URL. 47 | */ 48 | public function qrCodeUrl(string $companyName, string $companyEmail, string $secret): string 49 | { 50 | return $this->engine->getQRCodeUrl($companyName, $companyEmail, $secret); 51 | } 52 | 53 | /** 54 | * verify the given code. 55 | */ 56 | public function verify(string $secret, string $code): bool 57 | { 58 | if (is_int($customWindow = Config::get('user.twofactor.window'))) { 59 | $this->engine->setWindow($customWindow); 60 | } 61 | 62 | $timestamp = $this->engine->verifyKeyNewer( 63 | $secret, $code, Cache::get($key = 'user.2fa_codes.'.md5($code)) 64 | ); 65 | 66 | if ($timestamp !== false) { 67 | if ($timestamp === true) { 68 | $timestamp = $this->engine->getTimestamp(); 69 | } 70 | 71 | Cache::put($key, $timestamp, ($this->engine->getWindow() ?: 1) * 60); 72 | 73 | return true; 74 | } 75 | 76 | return false; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /classes/UserProvider.php: -------------------------------------------------------------------------------- 1 | applyRegistered(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /classes/sessionguard/HasImpersonation.php: -------------------------------------------------------------------------------- 1 | user(); 19 | 20 | /** 21 | * @event model.auth.beforeImpersonate 22 | * 23 | * Example usage: 24 | * 25 | * $model->bindEvent('model.auth.beforeImpersonate', function (\October\Rain\Database\Model|null $oldUser) use (\October\Rain\Database\Model $model) { 26 | * traceLog($oldUser->full_name . ' is now impersonating ' . $model->full_name); 27 | * }); 28 | * 29 | */ 30 | $user->fireEvent('model.auth.beforeImpersonate', [$oldUser]); 31 | 32 | // Replace session with impersonated user 33 | $this->loginQuietly($user); 34 | 35 | // If this is the first time impersonating, capture the original user 36 | if (!$this->isImpersonator()) { 37 | $this->session->put($this->getImpersonateName(), $oldUser?->getKey() ?: 'NaN'); 38 | } 39 | } 40 | 41 | /** 42 | * stopImpersonate stops impersonating a user 43 | */ 44 | public function stopImpersonate() 45 | { 46 | // Determine current and previous user 47 | $currentUser = $this->user(); 48 | $oldUser = $this->getImpersonator(); 49 | 50 | if ($currentUser) { 51 | /** 52 | * @event model.auth.afterImpersonate 53 | * 54 | * Example usage: 55 | * 56 | * $model->bindEvent('model.auth.afterImpersonate', function (\October\Rain\Database\Model|null $oldUser) use (\October\Rain\Database\Model $model) { 57 | * traceLog($oldUser->full_name . ' has stopped impersonating ' . $model->full_name); 58 | * }); 59 | * 60 | */ 61 | $currentUser->fireEvent('model.auth.afterImpersonate', [$oldUser]); 62 | } 63 | 64 | // Restore previous user, if possible 65 | if ($oldUser) { 66 | $this->loginQuietly($oldUser); 67 | } 68 | else { 69 | $this->logoutQuietly(); 70 | } 71 | 72 | $this->session->remove($this->getImpersonateName()); 73 | } 74 | 75 | /** 76 | * getImpersonator returns the underlying user impersonating 77 | */ 78 | public function getImpersonator(): ?User 79 | { 80 | if (!$this->session->has($this->getImpersonateName())) { 81 | return null; 82 | } 83 | 84 | $oldUserId = $this->session->get($this->getImpersonateName()); 85 | if ((!is_string($oldUserId) && !is_int($oldUserId)) || $oldUserId === 'NaN') { 86 | return null; 87 | } 88 | 89 | return $this->provider->retrieveById($oldUserId); 90 | } 91 | 92 | /** 93 | * getRealUser gets the "real" user to bypass impersonation. 94 | */ 95 | public function getRealUser(): ?User 96 | { 97 | if ($user = $this->getImpersonator()) { 98 | return $user; 99 | } 100 | 101 | return $this->getUser(); 102 | } 103 | 104 | /** 105 | * isImpersonator checks to see if the current session is being impersonated. 106 | * @return bool 107 | */ 108 | public function isImpersonator() 109 | { 110 | return $this->session->has($this->getImpersonateName()); 111 | } 112 | 113 | /** 114 | * getImpersonationName gets the name of the session used to store the impersonator. 115 | */ 116 | public function getImpersonateName() 117 | { 118 | return 'user_impersonate'; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /classes/sessionguard/HasLegacyApi.php: -------------------------------------------------------------------------------- 1 | provider->retrieveById($id); 19 | } 20 | 21 | /** 22 | * @deprecated use `getProvider()->retrieveByCredentials(['email' => $email])` 23 | */ 24 | public function findUserByLogin($login) 25 | { 26 | return $this->provider->retrieveByCredentials([UserHelper::username() => $login]); 27 | } 28 | 29 | /** 30 | * @deprecated use `getProvider()->retrieveByCredentials` 31 | */ 32 | public function findUserByCredentials(array $credentials) 33 | { 34 | $user = $this->provider->retrieveByCredentials($credentials); 35 | if (!$user) { 36 | return null; 37 | } 38 | 39 | if ( 40 | array_key_exists('password', $credentials) && 41 | !$this->provider->validateCredentials($user, $credentials) 42 | ) { 43 | return null; 44 | } 45 | 46 | return $user; 47 | } 48 | 49 | /** 50 | * @deprecated create User model manually 51 | */ 52 | public function register(array $credentials, $activate = false, $autoLogin = true) 53 | { 54 | $user = \RainLab\User\Models\User::create($credentials); 55 | 56 | if ($activate) { 57 | $user->markEmailAsVerified(); 58 | } 59 | 60 | if ($autoLogin) { 61 | $this->loginQuietly($user); 62 | } 63 | 64 | return $user; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /classes/sessionguard/HasPersistence.php: -------------------------------------------------------------------------------- 1 | validationForced; 21 | 22 | $user->validationForced = true; 23 | 24 | $user->setPersistCode($user->generatePersistCode()); 25 | 26 | // Expected save() call in here 27 | $this->cycleRememberToken($user); 28 | 29 | $user->validationForced = $validationForced; 30 | } 31 | 32 | /** 33 | * preventConcurrentSessions for the supplied user, if configured 34 | */ 35 | protected function preventConcurrentSessions(User $user) 36 | { 37 | if (UserSetting::get('block_persistence', false)) { 38 | $this->logoutOtherDevicesForcefully($user); 39 | } 40 | 41 | $this->updatePersistSession($user); 42 | } 43 | 44 | /** 45 | * updatePersistSession 46 | */ 47 | protected function updatePersistSession(User $user) 48 | { 49 | return $this->session->put($this->getPersistCodeName(), $user->getPersistCode()); 50 | } 51 | 52 | /** 53 | * hasValidPersistCode 54 | */ 55 | protected function hasValidPersistCode(User $user) 56 | { 57 | return $this->session->get($this->getPersistCodeName()) === $user->getPersistCode(); 58 | } 59 | 60 | /** 61 | * getPersistCodeName gets the name of the session used to store the checksum. 62 | */ 63 | public function getPersistCodeName() 64 | { 65 | return 'user_persist_code'; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /components/Registration.php: -------------------------------------------------------------------------------- 1 | "Registration", 26 | 'description' => "Provides services for registering a user." 27 | ]; 28 | } 29 | 30 | /** 31 | * onRegister 32 | */ 33 | public function onRegister() 34 | { 35 | if (!$this->canRegister()) { 36 | throw new NotFoundException; 37 | } 38 | 39 | $input = post(); 40 | 41 | /** 42 | * @event rainlab.user.beforeRegister 43 | * Provides custom logic for creating a new user during registration. 44 | * 45 | * Example usage: 46 | * 47 | * Event::listen('rainlab.user.beforeRegister', function ($component, &$input) { 48 | * return User::create(...); 49 | * }); 50 | * 51 | * Or 52 | * 53 | * $component->bindEvent('user.beforeRegister', function (&$input) { 54 | * return User::create(...); 55 | * }); 56 | * 57 | */ 58 | if ($event = $this->fireSystemEvent('rainlab.user.beforeRegister', [&$input])) { 59 | $user = $event; 60 | } 61 | else { 62 | $user = $this->createNewUser($input); 63 | } 64 | 65 | Auth::login($user); 66 | 67 | /** 68 | * @event rainlab.user.register 69 | * Modify the return response after registration. 70 | * 71 | * Example usage: 72 | * 73 | * Event::listen('rainlab.user.register', function ($component, $user) { 74 | * // Fire logic 75 | * }); 76 | * 77 | * Or 78 | * 79 | * $component->bindEvent('user.register', function ($user) { 80 | * // Fire logic 81 | * }); 82 | * 83 | */ 84 | if ($event = $this->fireSystemEvent('rainlab.user.register', [$user])) { 85 | return $event; 86 | } 87 | 88 | // Redirect to the intended page after successful registration 89 | if ($redirect = Cms::redirectIntendedFromPost()) { 90 | return $redirect; 91 | } 92 | } 93 | 94 | /** 95 | * createNewUser implements the logic for creating a new user 96 | */ 97 | protected function createNewUser(array $input): User 98 | { 99 | // If the password confirmation field is absent from the request payload, 100 | // skip it here for a smoother registration process. Every second counts! 101 | if (!array_key_exists('password_confirmation', $input)) { 102 | $input['password_confirmation'] = $input['password'] ?? ''; 103 | } 104 | 105 | Validator::make($input, [ 106 | 'first_name' => ['required', 'string', 'max:255'], 107 | 'last_name' => ['required', 'string', 'max:255'], 108 | 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 109 | 'password' => UserHelper::passwordRules(), 110 | ])->validate(); 111 | 112 | $user = User::create([ 113 | 'first_name' => $input['first_name'], 114 | 'last_name' => $input['last_name'], 115 | 'email' => $input['email'], 116 | 'password' => $input['password'], 117 | 'password_confirmation' => $input['password_confirmation'], 118 | ]); 119 | 120 | UserLog::createRecord($user->getKey(), UserLog::TYPE_NEW_USER, [ 121 | 'user_full_name' => $user->full_name, 122 | ]); 123 | 124 | return $user; 125 | } 126 | 127 | /** 128 | * canRegister checks if the registration is allowed 129 | */ 130 | public function canRegister(): bool 131 | { 132 | return Setting::get('allow_registration'); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /components/ResetPassword.php: -------------------------------------------------------------------------------- 1 | "Reset Password", 31 | 'description' => 'Confirms and resets the user with a new password.' 32 | ]; 33 | } 34 | 35 | /** 36 | * defineProperties 37 | */ 38 | public function defineProperties() 39 | { 40 | return [ 41 | 'isDefault' => [ 42 | 'title' => 'Default View', 43 | 'type' => 'checkbox', 44 | 'description' => 'Use this page as the default entry point when recovering a password.', 45 | 'showExternalParam' => false 46 | ], 47 | ]; 48 | } 49 | 50 | /** 51 | * onConfirmPassword 52 | */ 53 | public function onConfirmPassword() 54 | { 55 | if ($response = $this->actionResetPassword()) { 56 | return $response; 57 | } 58 | 59 | if ($flash = Cms::flashFromPost(__("Your password has been created and you may now sign in to your account"))) { 60 | Flash::success($flash); 61 | } 62 | 63 | if ($redirect = Cms::redirectFromPost()) { 64 | return $redirect; 65 | } 66 | } 67 | 68 | /** 69 | * onResetPassword 70 | */ 71 | public function onResetPassword() 72 | { 73 | if ($response = $this->actionResetPassword()) { 74 | return $response; 75 | } 76 | 77 | if ($flash = Cms::flashFromPost(__("Your password has been reset"))) { 78 | Flash::success($flash); 79 | } 80 | 81 | if ($redirect = Cms::redirectFromPost()) { 82 | return $redirect; 83 | } 84 | } 85 | 86 | /** 87 | * onChangePassword 88 | */ 89 | public function onChangePassword() 90 | { 91 | if ($response = $this->actionChangePassword()) { 92 | return $response; 93 | } 94 | 95 | if ($flash = Cms::flashFromPost(__("Your password has been changed"))) { 96 | Flash::success($flash); 97 | } 98 | 99 | if ($redirect = Cms::redirectFromPost()) { 100 | return $redirect; 101 | } 102 | } 103 | 104 | /** 105 | * canReset returns true if the user can reset their password 106 | */ 107 | public function canReset(): bool 108 | { 109 | return $this->hasToken() || $this->user(); 110 | } 111 | 112 | /** 113 | * hasToken checks if a reset token state is requested by the user 114 | */ 115 | public function hasToken() 116 | { 117 | return $this->token() && $this->email(); 118 | } 119 | 120 | /** 121 | * hasInvite 122 | */ 123 | public function hasInvite(): bool 124 | { 125 | return (bool) get('new'); 126 | } 127 | 128 | /** 129 | * user to check for a change password 130 | */ 131 | public function user(): ?User 132 | { 133 | return Auth::user(); 134 | } 135 | 136 | /** 137 | * email to match with a password reset token 138 | */ 139 | public function email() 140 | { 141 | return get('email'); 142 | } 143 | 144 | /** 145 | * token returns a password reset token 146 | */ 147 | public function token() 148 | { 149 | return get('reset'); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /components/account/ActionBrowserSessions.php: -------------------------------------------------------------------------------- 1 | table(Config::get('session.table', 'sessions')) 31 | ->where('user_id', Auth::user()->getAuthIdentifier()) 32 | ->orderBy('last_activity', 'desc') 33 | ->get() 34 | )->map(function ($session) { 35 | $agent = $this->createAgent($session); 36 | 37 | return (object) [ 38 | 'agent' => [ 39 | 'is_desktop' => $agent->isDesktop(), 40 | 'platform' => $agent->platform(), 41 | 'browser' => $agent->browser() 42 | ], 43 | 'ip_address' => $session->ip_address, 44 | 'is_current_device' => $session->id === Request::session()->getId(), 45 | 'last_active' => Carbon::createFromTimestamp($session->last_activity)->diffForHumans() 46 | ]; 47 | })->all(); 48 | } 49 | 50 | /** 51 | * actionDeleteOtherSessions 52 | */ 53 | protected function actionDeleteOtherSessions() 54 | { 55 | $password = (string) post('password'); 56 | 57 | if (!$this->isUserPasswordValid($password)) { 58 | throw new ValidationException([ 59 | 'password' => __('This password does not match our records.'), 60 | ]); 61 | } 62 | 63 | Auth::logoutOtherDevices($password); 64 | 65 | $this->deleteOtherSessionRecords(); 66 | 67 | Request::session()->put([ 68 | 'password_hash_'.Auth::getDefaultDriver() => Auth::user()->getAuthPassword(), 69 | ]); 70 | } 71 | 72 | /** 73 | * createAgent instance from the given session. 74 | */ 75 | protected function createAgent($session): Agent 76 | { 77 | $agent = new Agent; 78 | $agent->setUserAgent($session->user_agent); 79 | return $agent; 80 | } 81 | 82 | /** 83 | * deleteOtherSessionRecords deletes the other browser session records from storage. 84 | */ 85 | protected function deleteOtherSessionRecords() 86 | { 87 | if (Config::get('session.driver') !== 'database') { 88 | return; 89 | } 90 | 91 | Db::connection(config('session.connection'))->table(config('session.table', 'sessions')) 92 | ->where('user_id', Auth::user()->getAuthIdentifier()) 93 | ->where('id', '!=', Request::session()->getId()) 94 | ->delete(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /components/account/ActionDeleteUser.php: -------------------------------------------------------------------------------- 1 | isUserPasswordValid(post('password'))) { 23 | throw new ValidationException([ 24 | 'password' => __('This password does not match our records.'), 25 | ]); 26 | } 27 | 28 | $this->deleteUser($this->user()->fresh()); 29 | 30 | Auth::logout(); 31 | Request::session()->invalidate(); 32 | Request::session()->regenerateToken(); 33 | } 34 | 35 | /** 36 | * deleteUser 37 | */ 38 | protected function deleteUser(User $user) 39 | { 40 | UserLog::createRecord($user->getKey(), UserLog::TYPE_SELF_DELETE, [ 41 | 'user_full_name' => $user->full_name, 42 | ]); 43 | 44 | $user->smartDelete(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /components/account/ActionTwoFactor.php: -------------------------------------------------------------------------------- 1 | user(); 24 | 25 | return $user && $user->hasEnabledTwoFactorAuthentication(); 26 | } 27 | 28 | /** 29 | * fetchTwoFactorRecoveryCodes 30 | */ 31 | protected function fetchTwoFactorRecoveryCodes(): array 32 | { 33 | $user = $this->user(); 34 | 35 | if (!$user) { 36 | return []; 37 | } 38 | 39 | return json_decode($user->two_factor_recovery_codes, true) ?: []; 40 | } 41 | 42 | /** 43 | * actionEnableTwoFactor 44 | */ 45 | protected function actionEnableTwoFactor() 46 | { 47 | $user = $this->user(); 48 | 49 | if (!$user) { 50 | throw new ForbiddenException; 51 | } 52 | 53 | $user->enableTwoFactorAuthentication(); 54 | } 55 | 56 | /** 57 | * actionRegenerateTwoFactorRecoveryCodes 58 | */ 59 | protected function actionRegenerateTwoFactorRecoveryCodes() 60 | { 61 | $user = $this->user(); 62 | 63 | if (!$user) { 64 | throw new ForbiddenException; 65 | } 66 | 67 | $user->generateNewRecoveryCodes(); 68 | } 69 | 70 | /** 71 | * actionConfirmTwoFactor 72 | */ 73 | protected function actionConfirmTwoFactor() 74 | { 75 | $user = $this->user(); 76 | $code = post('code'); 77 | 78 | if ( 79 | !$user || 80 | !$user->two_factor_secret || 81 | !$code || 82 | !TwoFactorManager::instance()->verify($user->two_factor_secret, $code) 83 | ) { 84 | throw new ValidationException([ 85 | 'code' => [__('The provided two factor authentication code was invalid.')], 86 | ]); 87 | } 88 | 89 | $user->forceFill([ 90 | 'two_factor_confirmed_at' => Carbon::now() 91 | ]); 92 | 93 | $user->save(); 94 | 95 | UserLog::createRecord($user->getKey(), UserLog::TYPE_SET_TWO_FACTOR, [ 96 | 'user_full_name' => $user->full_name, 97 | 'is_two_factor_enabled' => true 98 | ]); 99 | } 100 | 101 | /** 102 | * actionDisableTwoFactor 103 | */ 104 | protected function actionDisableTwoFactor() 105 | { 106 | $user = $this->user(); 107 | 108 | if (!$user) { 109 | throw new ForbiddenException; 110 | } 111 | 112 | $user->disableTwoFactorAuthentication(); 113 | 114 | UserLog::createRecord($user->getKey(), UserLog::TYPE_SET_TWO_FACTOR, [ 115 | 'user_full_name' => $user->full_name, 116 | 'is_two_factor_enabled' => false 117 | ]); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /components/account/ActionVerifyEmail.php: -------------------------------------------------------------------------------- 1 | user(); 27 | 28 | if (!$user) { 29 | throw new ForbiddenException; 30 | } 31 | 32 | $limiter = $this->makeVerifyRateLimiter(); 33 | 34 | if ($limiter->tooManyAttempts(1)) { 35 | $seconds = $limiter->availableIn(); 36 | 37 | throw new ApplicationException(__("Too many verification attempts. Please try again in :seconds seconds.", [ 38 | 'seconds' => $seconds, 39 | 'minutes' => ceil($seconds / 60), 40 | ])); 41 | } 42 | 43 | $limiter->increment(); 44 | 45 | $user->sendEmailVerificationNotification(); 46 | } 47 | 48 | /** 49 | * actionConfirmEmail 50 | */ 51 | protected function actionConfirmEmail($verifyCode = null) 52 | { 53 | if ($verifyCode === null) { 54 | $verifyCode = post('verify'); 55 | } 56 | 57 | // Locate user from bearer code 58 | $user = User::findUserForEmailVerification($verifyCode); 59 | if (!$user) { 60 | throw new ApplicationException(__('The provided email verification code was invalid.')); 61 | } 62 | 63 | // Ensure verification is for the logged in user 64 | if ($sessionUser = $this->user()) { 65 | $user = $sessionUser; 66 | } 67 | // Make the bearer available the current page cycle 68 | else { 69 | Auth::setUserViaBearerToken($user); 70 | } 71 | 72 | // Verify the bearer/user 73 | if (!$user->hasVerifiedEmail()) { 74 | $user->markEmailAsVerified(); 75 | 76 | UserLog::createRecord($user->getKey(), UserLog::TYPE_SELF_VERIFY, [ 77 | 'user_full_name' => $user->full_name, 78 | 'user_email' => $user->email, 79 | ]); 80 | } 81 | } 82 | 83 | /** 84 | * checkVerifyEmailRedirect 85 | */ 86 | protected function checkVerifyEmailRedirect() 87 | { 88 | $verifyCode = get('verify'); 89 | if (!$verifyCode) { 90 | return; 91 | } 92 | 93 | try { 94 | $this->actionConfirmEmail($verifyCode); 95 | 96 | if ($flash = Cms::flashFromPost(__("Thank you for verifying your email."))) { 97 | Flash::success($flash); 98 | } 99 | } 100 | catch (ApplicationException $ex) { 101 | Flash::error($ex->getMessage()); 102 | } 103 | 104 | if (in_array(get('redirect'), ['0', 'false'])) { 105 | return; 106 | } 107 | 108 | $redirectUrl = rtrim(Request::fullUrlWithQuery(['verify' => null]), '?'); 109 | return Redirect::to($redirectUrl); 110 | } 111 | 112 | /** 113 | * makeVerifyRateLimiter 114 | */ 115 | protected function makeVerifyRateLimiter() 116 | { 117 | return new \System\Classes\RateLimiter('verify:'.$this->user()->getKey()); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /components/account/default.htm: -------------------------------------------------------------------------------- 1 | {# Placeholder #} 2 |
3 | Please see the documentation for detailed instructions on setting up the account component 4 |
5 | -------------------------------------------------------------------------------- /components/authentication/ActionLogin.php: -------------------------------------------------------------------------------- 1 | ensureLoginIsNotThrottled(); 24 | 25 | if (($event = $this->fireBeforeAuthenticateEvent()) !== null) { 26 | if ($event === false || !$event instanceof Authenticatable) { 27 | $this->throwFailedAuthenticationException(); 28 | } 29 | 30 | Auth::login($event, $this->useRememberMe()); 31 | } 32 | elseif (!$this->attemptAuthentication(post())) { 33 | $this->throwFailedAuthenticationException(); 34 | } 35 | 36 | $this->prepareAuthenticatedSession(); 37 | 38 | // Trigger login event 39 | if ($user = Auth::user()) { 40 | Event::fire('rainlab.user.login', [$user]); 41 | 42 | $this->recordUserLogAuthenticated($user); 43 | } 44 | 45 | if ($event = $this->fireAuthenticateEvent()) { 46 | return $event; 47 | } 48 | } 49 | 50 | /** 51 | * ensureLoginIsNotThrottled 52 | */ 53 | protected function ensureLoginIsNotThrottled() 54 | { 55 | $limiter = $this->makeLoginRateLimiter(); 56 | 57 | if (!$limiter->tooManyAttempts()) { 58 | return; 59 | } 60 | 61 | /** 62 | * @event rainlab.user.lockout 63 | * Provides custom logic when a login attempt has been rate limited. 64 | * 65 | * Example usage: 66 | * 67 | * Event::listen('rainlab.user.lockout', function () { 68 | * // ... 69 | * }); 70 | * 71 | * Or 72 | * 73 | * $component->bindEvent('user.lockout', function () { 74 | * // ... 75 | * }); 76 | * 77 | */ 78 | $this->fireSystemEvent('rainlab.user.lockout'); 79 | 80 | $seconds = $limiter->availableIn(); 81 | 82 | $message = __("Too many login attempts. Please try again in :seconds seconds.", [ 83 | 'seconds' => $seconds, 84 | 'minutes' => ceil($seconds / 60), 85 | ]); 86 | 87 | throw new ValidationException([UserHelper::username() => $message]); 88 | } 89 | 90 | /** 91 | * attemptAuthentication 92 | */ 93 | protected function attemptAuthentication(array $input): bool 94 | { 95 | $credentials = array_only($input, [UserHelper::username(), 'password']); 96 | 97 | Validator::make($input, [ 98 | UserHelper::username() => 'required|string', 99 | 'password' => 'required|string', 100 | ])->validate(); 101 | 102 | return Auth::attempt($credentials, $this->useRememberMe()); 103 | } 104 | 105 | /** 106 | * throwFailedAuthenticationException 107 | */ 108 | protected function throwFailedAuthenticationException() 109 | { 110 | $this->makeLoginRateLimiter()->increment(); 111 | 112 | throw new ValidationException([UserHelper::username() => __("These credentials do not match our records.")]); 113 | } 114 | 115 | /** 116 | * makeLoginRateLimiter 117 | */ 118 | protected function makeLoginRateLimiter() 119 | { 120 | return new \System\Classes\RateLimiter('login:'.post(UserHelper::username())); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /components/authentication/ActionRecoverPassword.php: -------------------------------------------------------------------------------- 1 | 'required|email']); 28 | 29 | $status = $this->makePasswordBroker()->sendResetLink([ 30 | 'email' => post('email') 31 | ]); 32 | 33 | if ($status === PasswordBroker::RESET_THROTTLED) { 34 | throw new ValidationException([UserHelper::username() => __("Please wait before retrying.")]); 35 | } 36 | 37 | if ($status !== PasswordBroker::RESET_LINK_SENT) { 38 | throw new ValidationException([UserHelper::username() => __("We can't find a user with that email address.")]); 39 | } 40 | } 41 | 42 | /** 43 | * makePasswordBroker to be used during password reset. 44 | */ 45 | protected function makePasswordBroker(): PasswordBroker 46 | { 47 | return App::make('auth.password')->broker('users'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /components/registration/default.htm: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | {% if __SELF__.canRegister %} 5 |
6 |

Create a new account

7 |

Enter your information below

8 |
9 | 10 |
11 | 19 | 20 |
21 | 22 |
23 | 31 | 32 |
33 | 34 |
35 | 43 | 44 |
45 | 46 |
47 | 55 | 56 |
57 | 58 | 64 | {% else %} 65 |
66 |

Create a new account

67 |

Registrations are currently disabled

68 |
69 | {% endif %} 70 | 71 |
72 |

73 | Already have an account? Sign in here 74 |

75 |
76 |
77 | -------------------------------------------------------------------------------- /components/resetpassword/ActionChangePassword.php: -------------------------------------------------------------------------------- 1 | updateUserPassword($user, post()); 29 | 30 | if (Request::hasSession()) { 31 | Request::session()->put([ 32 | 'password_hash_'.Auth::getDefaultDriver() => $user->getAuthPassword(), 33 | ]); 34 | } 35 | } 36 | 37 | /** 38 | * updateUserPassword 39 | */ 40 | protected function updateUserPassword(User $user, array $input) 41 | { 42 | Validator::make($input, [ 43 | 'current_password' => ['required', 'string', 'current_password:web'], 44 | 'password' => UserHelper::passwordRules(), 45 | ], [ 46 | 'current_password.current_password' => __("The provided password does not match your current password."), 47 | ])->validate(); 48 | 49 | $user->password = $input['password']; 50 | $user->password_confirmation = $input['password_confirmation'] ?? null; 51 | $user->save(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /components/resetpassword/ActionResetPassword.php: -------------------------------------------------------------------------------- 1 | 'required', 27 | 'email' => ['required', 'email'], 28 | 'password' => 'required', 29 | ]); 30 | 31 | $status = $this->makePasswordBroker()->reset(array_only(post(), [ 32 | 'email', 'password', 'password_confirmation', 'token' 33 | ]), function($user) { 34 | $this->resetUserPassword($user, post()); 35 | $this->completePasswordReset($user); 36 | }); 37 | 38 | if ($status === PasswordBroker::RESET_THROTTLED) { 39 | throw new ValidationException(['password' => __("Please wait before retrying.")]); 40 | } 41 | 42 | if ($status !== PasswordBroker::PASSWORD_RESET) { 43 | throw new ValidationException(['password' => __("This password reset token is invalid. Please try recovering your password again.")]); 44 | } 45 | } 46 | 47 | /** 48 | * resetUserPassword updates the user password 49 | */ 50 | protected function resetUserPassword(User $user, array $input): void 51 | { 52 | Validator::make($input, [ 53 | 'password' => UserHelper::passwordRules(), 54 | ])->validate(); 55 | 56 | $user->forceFill([ 57 | 'password' => $input['password'], 58 | ])->save(); 59 | } 60 | 61 | /** 62 | * completePasswordReset 63 | */ 64 | protected function completePasswordReset(User $user) 65 | { 66 | $user->setRememberToken(Str::random(60)); 67 | 68 | $user->save(); 69 | 70 | /** 71 | * @event rainlab.user.passwordReset 72 | * Provides custom logic for resetting a user password. 73 | * 74 | * Example usage: 75 | * 76 | * Event::listen('rainlab.user.passwordReset', function ($component, $user) { 77 | * // Fire logic 78 | * }); 79 | * 80 | * Or 81 | * 82 | * $component->bindEvent('user.passwordReset', function ($user) { 83 | * // Fire logic 84 | * }); 85 | * 86 | */ 87 | $this->fireSystemEvent('rainlab.user.passwordReset', [$user]); 88 | } 89 | 90 | /** 91 | * makePasswordBroker to be used during password reset. 92 | */ 93 | protected function makePasswordBroker(): PasswordBroker 94 | { 95 | return App::make('auth.password')->broker('users'); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /components/resetpassword/default.htm: -------------------------------------------------------------------------------- 1 | {# User invited to set a password via an invite link #} 2 | {% if __SELF__.hasInvite %} 3 | {% set redirectPage = 'account/index' %} 4 | {% set passwordHandler = 'onConfirmPassword' %} 5 | 6 | {# User has a valid token to reset their password #} 7 | {% elseif __SELF__.hasToken %} 8 | {% set redirectPage = 'account/login' %} 9 | {% set passwordHandler = 'onResetPassword' %} 10 | 11 | {# User is changing their password normally #} 12 | {% else %} 13 | {% set redirectPage = 'account/index' %} 14 | {% set passwordHandler = 'onChangePassword' %} 15 | {% endif %} 16 | 17 |
18 | 19 | 20 |
21 | {% if __SELF__.hasInvite %} 22 |

Welcome!

23 | {% elseif __SELF__.hasToken %} 24 |

Reset password

25 | {% else %} 26 |

Change password

27 | {% endif %} 28 |

Enter a new password.

29 |
30 | 31 | {% if __SELF__.hasToken %} 32 | 33 | 34 | {% else %} 35 |
36 | 43 | 44 |
45 | {% endif %} 46 | 47 |
48 | 56 | 57 |
58 | 59 |
60 | 68 | 69 |
70 | 71 | 81 |
82 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rainlab/user-plugin", 3 | "type": "october-plugin", 4 | "description": "User plugin for October CMS", 5 | "homepage": "https://octobercms.com/plugin/rainlab-user", 6 | "keywords": ["october", "octobercms", "user"], 7 | "license": "proprietary", 8 | "authors": [ 9 | { 10 | "name": "Alexey Bobkov", 11 | "email": "aleksey.bobkov@gmail.com", 12 | "role": "Co-founder" 13 | }, 14 | { 15 | "name": "Samuel Georges", 16 | "email": "daftspunky@gmail.com", 17 | "role": "Co-founder" 18 | } 19 | ], 20 | "require": { 21 | "php": "^8.0.2", 22 | "october/rain": ">=3.0", 23 | "mobiledetect/mobiledetectlib": "^4.8", 24 | "pragmarx/google2fa": "^8.0", 25 | "bacon/bacon-qr-code": "^2.0", 26 | "firebase/php-jwt": "^6.4", 27 | "composer/installers": "~1.0" 28 | }, 29 | "minimum-stability": "dev" 30 | } 31 | -------------------------------------------------------------------------------- /config/auth.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'guard' => 'web', 18 | 'passwords' => 'users', 19 | ], 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Authentication Guards 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Next, you may define every authentication guard for your application. 27 | | Of course, a great default configuration has been defined for you 28 | | here which uses session storage and the Eloquent user provider. 29 | | 30 | | All authentication drivers have a user provider. This defines how the 31 | | users are actually retrieved out of your database or other storage 32 | | mechanisms used by this application to persist your user's data. 33 | | 34 | | Supported: "session" 35 | | 36 | */ 37 | 38 | 'guards' => [ 39 | 'web' => [ 40 | 'driver' => 'session', 41 | 'provider' => 'users', 42 | ], 43 | ], 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | User Providers 48 | |-------------------------------------------------------------------------- 49 | | 50 | | All authentication drivers have a user provider. This defines how the 51 | | users are actually retrieved out of your database or other storage 52 | | mechanisms used by this application to persist your user's data. 53 | | 54 | | If you have multiple user tables or models you may configure multiple 55 | | sources which represent each model / table. These sources may then 56 | | be assigned to any extra authentication guards you have defined. 57 | | 58 | | Supported: "database", "eloquent", "user" 59 | | 60 | */ 61 | 62 | 'providers' => [ 63 | 'users' => [ 64 | 'driver' => 'user', 65 | 'model' => RainLab\User\Models\User::class, 66 | ], 67 | ], 68 | 69 | /* 70 | |-------------------------------------------------------------------------- 71 | | Resetting Passwords 72 | |-------------------------------------------------------------------------- 73 | | 74 | | You may specify multiple password reset configurations if you have more 75 | | than one user table or model in the application and you want to have 76 | | separate password reset settings based on the specific user types. 77 | | 78 | | The expiry time is the number of minutes that each reset token will be 79 | | considered valid. This security feature keeps tokens short-lived so 80 | | they have less time to be guessed. You may change this as needed. 81 | | 82 | | The throttle setting is the number of seconds a user must wait before 83 | | generating more password reset tokens. This prevents the user from 84 | | quickly generating a very large amount of password reset tokens. 85 | | 86 | */ 87 | 88 | 'passwords' => [ 89 | 'users' => [ 90 | 'provider' => 'users', 91 | 'table' => 'user_password_resets', 92 | 'expire' => 60, 93 | 'throttle' => 60, 94 | ], 95 | ], 96 | 97 | /* 98 | |-------------------------------------------------------------------------- 99 | | Password Confirmation Timeout 100 | |-------------------------------------------------------------------------- 101 | | 102 | | Here you may define the amount of seconds before a password confirmation 103 | | times out and the user is prompted to re-enter their password via the 104 | | confirmation screen. By default, the timeout lasts for three hours. 105 | | 106 | */ 107 | 108 | 'password_timeout' => 10800, 109 | 110 | ]; 111 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | null, 13 | 14 | /* 15 | |-------------------------------------------------------------------------- 16 | | Allow user registration 17 | |-------------------------------------------------------------------------- 18 | | 19 | | If this is disabled users can only be created by administrators. 20 | | 21 | */ 22 | 23 | 'allow_registration' => true, 24 | 25 | /* 26 | |-------------------------------------------------------------------------- 27 | | Prevent concurrent sessions 28 | |-------------------------------------------------------------------------- 29 | | 30 | | When enabled users cannot sign in to multiple devices at the same time. 31 | | 32 | */ 33 | 34 | 'block_persistence' => false, 35 | 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | Login attribute 39 | |-------------------------------------------------------------------------- 40 | | 41 | | Select what primary user detail should be used for signing in. 42 | | 43 | | email Authenticate users by email. 44 | | username Authenticate users by username. 45 | | 46 | */ 47 | 48 | 'login_attribute' => 'email', 49 | 50 | /* 51 | |-------------------------------------------------------------------------- 52 | | Password Policy 53 | |-------------------------------------------------------------------------- 54 | | 55 | | Specify the password policy for backend administrators. 56 | | 57 | | min_length - Password minimum length between 4 - 128 chars 58 | | require_mixed_case - Require at least one uppercase and lowercase letter 59 | | require_uncompromised - Require a password not found in a leaked password database 60 | | require_number - Require at least one number 61 | | require_symbol - Require at least one non-alphanumeric character 62 | | 63 | */ 64 | 65 | 'password_policy' => [ 66 | 'min_length' => 8, 67 | 'require_mixed_case' => false, 68 | 'require_uncompromised' => false, 69 | 'require_number' => true, 70 | 'require_symbol' => false, 71 | ], 72 | 73 | /* 74 | |-------------------------------------------------------------------------- 75 | | JWT Config 76 | |-------------------------------------------------------------------------- 77 | | 78 | | The token might be consumed in other systems. perhaps we could have a 79 | | few variables here and there to have control over the token variables. 80 | | Setting the value to null will leave it as the internal default value. 81 | | 82 | | - algorithm: see https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-algorithms-40 83 | | - key: to be used instead of the app.key value 84 | | - ttl: in minutes 85 | | - leeway: in seconds 86 | | 87 | */ 88 | 89 | 'bearer_token' => [ 90 | 'algorithm' => null, 91 | 'key' => null, 92 | 'ttl' => null, 93 | 'leeway' => null, 94 | ] 95 | ]; 96 | -------------------------------------------------------------------------------- /console/MigrateV1Command.php: -------------------------------------------------------------------------------- 1 | info("Table [rainlab_user_mail_blockers] is not found, nothing to migrate."); 28 | return; 29 | } 30 | 31 | Schema::dropIfExists('rainlab_user_mail_blockers'); 32 | 33 | $columnsToPrune = [ 34 | 'name', 35 | 'surname', 36 | 'reset_password_code', 37 | 'permissions', 38 | 'is_superuser', 39 | 'is_activated', 40 | 'last_login', 41 | ]; 42 | 43 | foreach ($columnsToPrune as $column) { 44 | if (Schema::hasColumn('users', $column)) { 45 | Schema::table('users', function($table) use ($column) { 46 | $table->dropColumn($column); 47 | }); 48 | } 49 | } 50 | 51 | $this->info("Successfully cleaned up user table data"); 52 | } 53 | 54 | /** 55 | * getArguments 56 | */ 57 | protected function getArguments() 58 | { 59 | return []; 60 | } 61 | 62 | /** 63 | * getOptions 64 | */ 65 | protected function getOptions() 66 | { 67 | return []; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /contentfields/usersfield/partials/_column_multi.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /contentfields/usersfield/partials/_column_single.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /controllers/Timelines.php: -------------------------------------------------------------------------------- 1 | listRefresh(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /controllers/UserGroups.php: -------------------------------------------------------------------------------- 1 | 2 | loadingMessage("Updating Activity Timeline...") 4 | ->icon('icon-refresh') 5 | ->secondary() ?> 6 | 7 | -------------------------------------------------------------------------------- /controllers/timelines/config_list.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # List Behavior Config 3 | # =================================== 4 | 5 | # Model List Column configuration 6 | list: $/rainlab/user/models/userlog/columns-global.yaml 7 | 8 | # Model Class name 9 | modelClass: RainLab\User\Models\UserLog 10 | 11 | # List Title 12 | title: Manage Timelines 13 | 14 | # Link URL for each record 15 | # recordUrl: rainlab/user/timelines/update/:id 16 | 17 | # Message to display if the list is empty 18 | noRecordsMessage: backend::lang.list.no_records 19 | 20 | # Records to display per page 21 | recordsPerPage: 100 22 | 23 | # Display page numbers with pagination, disable to improve performance 24 | showPageNumbers: true 25 | 26 | # Displays the list column set up button 27 | showSetup: false 28 | 29 | # Displays the sorting link on each column 30 | showSorting: false 31 | 32 | # Default sorting column 33 | defaultSort: 34 | column: created_at 35 | direction: desc 36 | 37 | # Display checkboxes next to each record 38 | showCheckboxes: false 39 | 40 | # Toolbar widget configuration 41 | toolbar: 42 | # Partial for toolbar buttons 43 | buttons: list_toolbar 44 | 45 | # Search widget configuration 46 | search: 47 | prompt: backend::lang.list.search_prompt 48 | -------------------------------------------------------------------------------- /controllers/timelines/index.php: -------------------------------------------------------------------------------- 1 | listRender() ?> 2 | -------------------------------------------------------------------------------- /controllers/usergroups/_list_toolbar.php: -------------------------------------------------------------------------------- 1 |
2 | icon('icon-arrow-left') ?> 3 | primary()->icon('icon-plus') ?> 4 |
5 | -------------------------------------------------------------------------------- /controllers/usergroups/config_form.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # Form Behavior Config 3 | # =================================== 4 | 5 | # Record name 6 | name: Group 7 | 8 | # Model Form Field configuration 9 | form: $/rainlab/user/models/usergroup/fields.yaml 10 | 11 | # Model Class name 12 | modelClass: RainLab\User\Models\UserGroup 13 | 14 | # Default redirect location 15 | defaultRedirect: rainlab/user/usergroups 16 | 17 | # Create page 18 | create: 19 | title: Create User Group 20 | redirect: rainlab/user/usergroups/update/:id 21 | redirectClose: rainlab/user/usergroups 22 | 23 | # Update page 24 | update: 25 | title: Edit User Group 26 | redirect: rainlab/user/usergroups 27 | redirectClose: rainlab/user/usergroups 28 | 29 | # Preview page 30 | preview: 31 | title: Edit User Group 32 | -------------------------------------------------------------------------------- /controllers/usergroups/config_list.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # List Behavior Config 3 | # =================================== 4 | 5 | # Model List Column configuration 6 | list: $/rainlab/user/models/usergroup/columns.yaml 7 | 8 | # Model Class name 9 | modelClass: RainLab\User\Models\UserGroup 10 | 11 | # Filter widget configuration 12 | filter: $/rainlab/user/models/usergroup/scopes.yaml 13 | 14 | # List Title 15 | title: Manage Groups 16 | 17 | # Link URL for each record 18 | recordUrl: rainlab/user/usergroups/update/:id 19 | 20 | # Message to display if the list is empty 21 | noRecordsMessage: backend::lang.list.no_records 22 | 23 | # Records to display per page 24 | recordsPerPage: 20 25 | 26 | # Displays the list column set up button 27 | showSetup: true 28 | 29 | # Displays the sorting link on each column 30 | showSorting: true 31 | 32 | # Default sorting column 33 | # defaultSort: 34 | # column: created_at 35 | # direction: desc 36 | 37 | # Display checkboxes next to each record 38 | # showCheckboxes: true 39 | 40 | # Toolbar widget configuration 41 | toolbar: 42 | # Partial for toolbar buttons 43 | buttons: list_toolbar 44 | 45 | # Search widget configuration 46 | search: 47 | prompt: backend::lang.list.search_prompt 48 | -------------------------------------------------------------------------------- /controllers/usergroups/create.php: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | formRenderDesign() ?> 10 | -------------------------------------------------------------------------------- /controllers/usergroups/index.php: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | listRender() ?> 9 | -------------------------------------------------------------------------------- /controllers/usergroups/update.php: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | formRenderDesign() ?> 10 | -------------------------------------------------------------------------------- /controllers/users/HasBulkActions.php: -------------------------------------------------------------------------------- 1 | find($objectId)) { 21 | $object->smartDelete(); 22 | } 23 | } 24 | catch (Exception $ex) { 25 | Flash::error(__("Error with user :id - :message", ['id' => $object->email, 'message' => $ex->getMessage()])); 26 | return $this->listRefresh(); 27 | } 28 | } 29 | } 30 | 31 | Flash::success(__("Deleted the selected users")); 32 | return $this->listRefresh(); 33 | } 34 | 35 | /** 36 | * onRestoreSelected 37 | */ 38 | public function onRestoreSelected() 39 | { 40 | if (($checkedIds = post('checked')) && is_array($checkedIds) && count($checkedIds)) { 41 | foreach ($checkedIds as $objectId) { 42 | try { 43 | if ($object = User::withTrashed()->find($objectId)) { 44 | $object->restore(); 45 | } 46 | } 47 | catch (Exception $ex) { 48 | Flash::error(__("Error with user :id - :message", ['id' => $object->email, 'message' => $ex->getMessage()])); 49 | return $this->listRefresh(); 50 | } 51 | } 52 | } 53 | 54 | Flash::success(__("Restored the selected users")); 55 | return $this->listRefresh(); 56 | } 57 | 58 | /** 59 | * onActivateSelected 60 | */ 61 | public function onActivateSelected() 62 | { 63 | if (($checkedIds = post('checked')) && is_array($checkedIds) && count($checkedIds)) { 64 | foreach ($checkedIds as $objectId) { 65 | try { 66 | if ($object = User::withTrashed()->find($objectId)) { 67 | $object->markEmailAsVerified(); 68 | } 69 | } 70 | catch (Exception $ex) { 71 | Flash::error(__("Error with user :id - :message", ['id' => $object->email, 'message' => $ex->getMessage()])); 72 | return $this->listRefresh(); 73 | } 74 | } 75 | } 76 | 77 | Flash::success(__("Activated the selected users")); 78 | return $this->listRefresh(); 79 | } 80 | 81 | /** 82 | * onBanSelected 83 | */ 84 | public function onBanSelected() 85 | { 86 | if (($checkedIds = post('checked')) && is_array($checkedIds) && count($checkedIds)) { 87 | foreach ($checkedIds as $objectId) { 88 | try { 89 | if ($object = User::withTrashed()->find($objectId)) { 90 | $object->ban(); 91 | } 92 | } 93 | catch (Exception $ex) { 94 | Flash::error(__("Error with user :id - :message", ['id' => $object->email, 'message' => $ex->getMessage()])); 95 | return $this->listRefresh(); 96 | } 97 | } 98 | } 99 | 100 | Flash::success(__("Banned the selected users")); 101 | return $this->listRefresh(); 102 | } 103 | 104 | /** 105 | * onUnbanSelected 106 | */ 107 | public function onUnbanSelected() 108 | { 109 | if (($checkedIds = post('checked')) && is_array($checkedIds) && count($checkedIds)) { 110 | foreach ($checkedIds as $objectId) { 111 | try { 112 | if ($object = User::withTrashed()->find($objectId)) { 113 | $object->unban(); 114 | } 115 | } 116 | catch (Exception $ex) { 117 | Flash::error(__("Error with user :id - :message", ['id' => $object->email, 'message' => $ex->getMessage()])); 118 | return $this->listRefresh(); 119 | } 120 | } 121 | } 122 | 123 | Flash::success(__("Unbanned the selected users")); 124 | return $this->listRefresh(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /controllers/users/_convert_guest_form.php: -------------------------------------------------------------------------------- 1 | 'convertGuestForm']) ?> 2 | 6 | 31 | 32 | 48 | 49 | -------------------------------------------------------------------------------- /controllers/users/_hint_activate.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |

6 |

7 | 8 | " 11 | data-stripe-load-indicator 12 | >. 13 |

14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /controllers/users/_hint_banned.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |

6 |

7 | 8 | " 11 | data-stripe-load-indicator 12 | >. 13 |

14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /controllers/users/_hint_guest.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |

6 |

7 | 8 |

9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /controllers/users/_hint_trashed.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |

6 |

7 | 8 | 9 | 10 |

11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /controllers/users/_list_toolbar.php: -------------------------------------------------------------------------------- 1 |
2 | icon('icon-plus') 4 | ->primary() ?> 5 | 6 | listCheckedTrigger() 8 | ->listCheckedRequest() 9 | ->icon('icon-delete') 10 | ->secondary() 11 | ->confirmMessage("Are you sure?") ?> 12 | 13 | user->hasAccess('rainlab.users.access_groups')): ?> 14 |
15 | 16 | icon('icon-group') 18 | ->secondary() ?> 19 | 20 | 21 | makePartial('~/path/to/partial'); 32 | * }); 33 | * 34 | */ 35 | $this->fireViewEvent('rainlab.user.view.extendListToolbar'); 36 | ?> 37 | 38 | 97 |
98 | -------------------------------------------------------------------------------- /controllers/users/_scoreboard_preview.php: -------------------------------------------------------------------------------- 1 |
2 | getAvatarThumb(144)): ?> 3 |
4 | <?= e($formModel->full_name) ?> 11 |
12 | 13 | 14 |
15 |

16 | full_name): ?> 17 |

full_name) ?>

18 | 19 |

20 | 21 |

22 | : email) ?> 23 |

24 |
25 | 26 |
27 |

28 |

is_guest ? __("Guest") : __("Registered") ?>

29 |

30 | : created_at->toFormattedDateString() ?> 31 |

32 |
33 | 34 | last_seen): ?> 35 |
36 |

37 |

last_seen->diffForHumans() ?>

38 |

39 | isOnline() ? __("Online now") : __("Currently offline") ?> 40 |

41 |
42 | 43 | 44 | deleted_at): ?> 45 |
46 |

47 |

deleted_at->toFormattedDateString() ?>

48 |

" 52 | > 53 |

54 |
55 | 56 |
57 | -------------------------------------------------------------------------------- /controllers/users/config_form.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # Form Behavior Config 3 | # =================================== 4 | 5 | # Record name 6 | name: User 7 | 8 | # Model Form Field configuration 9 | form: $/rainlab/user/models/user/fields.yaml 10 | 11 | # Model Class name 12 | modelClass: RainLab\User\Models\User 13 | 14 | # Default redirect location 15 | defaultRedirect: rainlab/user/users 16 | 17 | # Form Design 18 | design: 19 | displayMode: sidebar 20 | sidebarSize: 500 21 | 22 | # Create page 23 | create: 24 | redirect: rainlab/user/users/preview/:id 25 | redirectClose: rainlab/user/users 26 | 27 | # Update page 28 | update: 29 | redirect: rainlab/user/users/update/:id 30 | redirectClose: rainlab/user/users/preview/:id 31 | 32 | # Preview Page 33 | preview: 34 | title: View User 35 | form: $/rainlab/user/models/user/fields-preview.yaml 36 | design: 37 | displayMode: basic 38 | size: auto 39 | -------------------------------------------------------------------------------- /controllers/users/config_import_export.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # Import/Export Behavior Config 3 | # =================================== 4 | 5 | import: 6 | # Page title 7 | title: Import Users 8 | 9 | # Import List Column configuration 10 | list: $/rainlab/user/models/user/columns-import.yaml 11 | 12 | # Import Form Field configuration 13 | form: $/rainlab/user/models/user/fields-import.yaml 14 | 15 | # Import Model class 16 | modelClass: RainLab\User\Models\User\UserImport 17 | 18 | # Redirect when finished 19 | redirect: user/users 20 | 21 | # Required permissions 22 | # permissions: user.access_import_export 23 | 24 | export: 25 | # Page title 26 | title: Export Users 27 | 28 | # Output file name 29 | fileName: users.csv 30 | 31 | # Export List Column configuration 32 | list: $/rainlab/user/models/user/columns-export.yaml 33 | 34 | # Export Model class 35 | modelClass: RainLab\User\Models\User\UserExport 36 | 37 | # Redirect when finished 38 | redirect: user/users 39 | 40 | # Required permissions 41 | # permissions: user.access_import_export 42 | -------------------------------------------------------------------------------- /controllers/users/config_list.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # List Behavior Config 3 | # =================================== 4 | 5 | # List Title 6 | title: Manage Users 7 | 8 | # Model List Column configuration 9 | list: $/rainlab/user/models/user/columns.yaml 10 | 11 | # Filter widget configuration 12 | filter: $/rainlab/user/models/user/scopes.yaml 13 | 14 | # Model Class name 15 | modelClass: RainLab\User\Models\User 16 | 17 | # Link URL for each record 18 | recordUrl: rainlab/user/users/preview/:id 19 | 20 | # Message to display if the list is empty 21 | noRecordsMessage: backend::lang.list.no_records 22 | 23 | # Records to display per page 24 | recordsPerPage: 20 25 | 26 | # Display checkboxes next to each record 27 | showCheckboxes: true 28 | 29 | # Displays the list column set up button 30 | showSetup: true 31 | 32 | # Toolbar widget configuration 33 | toolbar: 34 | 35 | # Partial for toolbar buttons 36 | buttons: list_toolbar 37 | 38 | # Search widget configuration 39 | search: 40 | prompt: backend::lang.list.search_prompt 41 | -------------------------------------------------------------------------------- /controllers/users/config_relation.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # Relation Behavior Config 3 | # =================================== 4 | 5 | activity_log: 6 | label: User Log 7 | emptyMessage: User has no activity to show 8 | list: $/rainlab/user/models/userlog/columns.yaml 9 | readOnly: true 10 | view: 11 | showSorting: false 12 | defaultSort: 13 | column: created_at 14 | direction: desc 15 | -------------------------------------------------------------------------------- /controllers/users/create.php: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | formRenderDesign() ?> 9 | -------------------------------------------------------------------------------- /controllers/users/export.php: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 'd-flex flex-column h-100']) ?> 9 | 10 |
11 | exportRender() ?> 12 |
13 | 14 |
15 |
16 | keyboard(false)->primary()->icon('icon-cloud-download') ?> 17 |
18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /controllers/users/import.php: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 'd-flex flex-column h-100']) ?> 9 | 10 |
11 | importRender() ?> 12 |
13 | 14 |
15 | keyboard(false)->primary()->icon('icon-cloud-upload') ?> 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /controllers/users/index.php: -------------------------------------------------------------------------------- 1 | 2 | listRender() ?> 3 | -------------------------------------------------------------------------------- /controllers/users/update.php: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | formRenderDesign() ?> 10 | -------------------------------------------------------------------------------- /docs/auth-bearer-tokens.md: -------------------------------------------------------------------------------- 1 | # Auth Bearer Tokens 2 | 3 | The `Auth` implements a native bearer token implementation (JWT). 4 | 5 | ## Generating a Token 6 | 7 | When working with authentication via bearer tokens, the `getBearerToken` method can be used to obtain a bearer token (JWT) for the current user. It expires after 1 hour by default. 8 | 9 | ```php 10 | $token = Auth::getBearerToken(); 11 | ``` 12 | 13 | You may also pass a user to this method to get a token for a specified user. 14 | 15 | ```php 16 | $token = Auth::getBearerToken($user); 17 | ``` 18 | 19 | When using the [Session component](./component-session.md), the `token` variable is available on this object. 20 | 21 | ```twig 22 | {{ session.key}} 23 | ``` 24 | 25 | ## Verifying a Token 26 | 27 | When verifying a token, use the `checkBearerToken` method that will return a valid user who is associated token, or false if the token is invalid or that user is no longer found. 28 | 29 | ```php 30 | $user = Auth::checkBearerToken($token); 31 | ``` 32 | 33 | The `loginUsingBearerToken` method is used to verify a supplied token and authenticate the user. The method returns the user if the verification was successful. 34 | 35 | ```php 36 | if ($jwtToken = Request::bearerToken()) { 37 | Auth::loginUsingBearerToken($jwtToken); 38 | } 39 | ``` 40 | 41 | > **Note**: Further configuration for this functionality can be found in the **rainlab.user::config.bearer_token** configuration value. 42 | 43 | ## Working with APIs 44 | 45 | When [building API endpoints using CMS pages](https://docs.octobercms.com/3.x/cms/resources/building-apis.html) it can be useful to use a page for handling the authentication logic. The following is a simple example that includes various API endpoints. 46 | 47 | ```twig 48 | title = "User API Page" 49 | url = "/api/user/:action" 50 | 51 | [resetPassword] 52 | [account] 53 | [session] 54 | checkToken = 1 55 | == 56 | {% if this.param.action == 'signin' %} 57 | {% do response( 58 | ajaxHandler('onLogin').withVars({ 59 | token: session.token() 60 | }) 61 | ) %} 62 | {% endif %} 63 | 64 | {% if this.param.action == 'register' %} 65 | {% do response(ajaxHandler('onRegister')) %} 66 | {% endif %} 67 | 68 | {% if this.param.action == 'logout' %} 69 | {% do response(ajaxHandler('onLogout')) %} 70 | {% endif %} 71 | 72 | {% if this.param.action == 'refresh' %} 73 | {% do response({ data: { 74 | token: session.token() 75 | }}) %} 76 | {% endif %} 77 | ``` 78 | 79 | An API layout to verify the user can be used for other API endpoints. 80 | 81 | ```twig 82 | description = "Auth API Layout" 83 | is_priority = 1 84 | 85 | [session] 86 | checkToken = 1 87 | == 88 | {% if session.user %} 89 | {% page %} 90 | {% else %} 91 | {% do abort(403, 'Access Denied') %} 92 | {% endif %} 93 | ``` 94 | 95 | ### Email Verification Example 96 | 97 | The following example shows how to implement the email verification process as an API, with the supported actions of **request** (default) and **confirm**. 98 | 99 | The `setUrlForEmailVerification` method call overrides the verification URL in the email sent to the user. The `verify` code in the URL acts as a one-time bearer token that authenticates the user (`session.user`) for a single page cycle. The standard redirect is disabled by adding `redirect=0` to the verification URL. 100 | 101 | ```twig 102 | title = "User Email Verification API" 103 | url = "/api/user/verify/:action?request" 104 | 105 | [account] 106 | [session] 107 | checkToken = 1 108 | == 109 | {% if not session.user %} 110 | {% do response({ 111 | error: 'Access Denied' 112 | }, 403) %} 113 | {% endif %} 114 | 115 | {% if this.param.action == 'request' %} 116 | {% do session.user.setUrlForEmailVerification( 117 | this|page({ action: 'confirm' }) ~ '?redirect=0' 118 | ) %} 119 | 120 | {% do response(ajaxHandler('onVerifyEmail')) %} 121 | {% endif %} 122 | 123 | {% if this.param.action == 'confirm' %} 124 | {% if session.user.hasVerifiedEmail %} 125 | {% do response({ 126 | success: "Thank you for verifying your email." 127 | }, 201) %} 128 | {% else %} 129 | {% do response({ 130 | error: "The provided email verification code was invalid." 131 | }, 400) %} 132 | {% endif %} 133 | {% endif %} 134 | ``` 135 | -------------------------------------------------------------------------------- /docs/auth-impersonation.md: -------------------------------------------------------------------------------- 1 | # Auth Impersonation 2 | 3 | The `Auth` facade supports user impersonation. Use the `impersonate` method to impersonate another user. 4 | 5 | ```php 6 | Auth::impersonate($user); 7 | ``` 8 | 9 | To stop impersonating, use the `stopImpersonate` method. This will restore the account that was previously logged in, if applicable. 10 | 11 | ```php 12 | Auth::stopImpersonate(); 13 | ``` 14 | 15 | The `isImpersonator` method can be used to check if the user is currently impersonating. 16 | 17 | ```php 18 | if (Auth::isImpersonator()) { 19 | // User is currently impersonating another user 20 | } 21 | ``` 22 | 23 | Use the `getRealUser` method to return the underlying user they are impersonating someone else, or it will return the active user if they are not impersonating. 24 | 25 | ```php 26 | $user = Auth::getRealUser(); 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/auth-manager.md: -------------------------------------------------------------------------------- 1 | # Auth Manager 2 | 3 | There is an `Auth` facade you may use for common tasks, it primarily inherits the `October\Rain\Auth\Manager` class for functionality. 4 | 5 | You may use `create` method on the `User` model to register an account. 6 | 7 | ```php 8 | $user = \RainLab\User\Models\User::create([ 9 | 'first_name' => 'Some', 10 | 'last_name' => 'User', 11 | 'email' => 'some@website.tld', 12 | 'password' => 'ChangeMe888', 13 | 'password_confirmation' => 'ChangeMe888', 14 | ]); 15 | ``` 16 | 17 | The `markEmailAsVerified` method can be used to activate an existing user. 18 | 19 | ```php 20 | // Auto activate this user 21 | $user->markEmailAsVerified(); 22 | ``` 23 | 24 | The `check` method is a quick way to check if the user is signed in. 25 | 26 | ```php 27 | // Returns true if signed in. 28 | $loggedIn = Auth::check(); 29 | ``` 30 | 31 | To return the user model that is signed in, use `user` method instead. 32 | 33 | ```php 34 | // Returns the signed in user 35 | $user = Auth::user(); 36 | ``` 37 | 38 | You may authenticate a user by providing their login and password with the `attempt` method. 39 | 40 | ```php 41 | // Authenticate user by credentials 42 | $user = Auth::attempt([ 43 | 'email' => post('email'), 44 | 'password' => post('password') 45 | ]); 46 | ``` 47 | 48 | The second argument is used to store a non-expire cookie for the user. 49 | 50 | ```php 51 | $user = Auth::attempt([...], true); 52 | ``` 53 | 54 | You can also authenticate as a user simply by passing the user model along with the `login` method. 55 | 56 | ```php 57 | // Sign in as a specific user 58 | Auth::login($user); 59 | ``` 60 | 61 | The second argument will store the non-expire cookie for the user. 62 | 63 | ```php 64 | // Sign in and remember the user 65 | Auth::login($user, true); 66 | ``` 67 | 68 | You may look up a user by their email or login name using the `retrieveByCredentials` method via the provider class. 69 | 70 | ```php 71 | $user = Auth::getProvider()->retrieveByCredentials([ 72 | 'email' => 'some@email.tld' 73 | ]); 74 | ``` 75 | 76 | ## Guest Users 77 | 78 | Creating a guest user allows the registration process to be deferred. For example, making a purchase without needing to register first. Guest users are not able to sign in and will be added to the user group with the code `guest`. 79 | 80 | > **Note**: If you are upgrading from an older version of this plugin, to enable guest users you may need to remove the UNIQUE index on the `email` column in the `users` table. 81 | 82 | Use the `is_guest` attribute to create a guest user, it will return a user object and can be called multiple times. The unique identifier is the email address, which is a required field. 83 | 84 | ```php 85 | $user = \RainLab\User\Models\User::create([ 86 | 'first_name' => 'Some', 87 | 'last_name' => 'User', 88 | 'email' => 'person@acme.tld', 89 | 'is_guest' => true 90 | ]); 91 | ``` 92 | 93 | When a user registers with the same email address using the `create` method, another account is created and they will not inherit the existing guest user account. 94 | 95 | ```php 96 | // This will not throw an "Email already taken" error 97 | $user = \RainLab\User\Models\User::create([ 98 | 'first_name' => 'Some', 99 | 'last_name' => 'User', 100 | 'email' => 'person@acme.tld', 101 | 'password' => 'ChangeMe888', 102 | 'password_confirmation' => 'ChangeMe888', 103 | ]); 104 | ``` 105 | 106 | You may convert a guest to a registered user with the `convertToRegistered` method. This will send them an invitation using the `user:invite_email` template to set up a new password. When a user is converted they will be added to the user group with the code `registered`. 107 | 108 | ```php 109 | User::where('email', 'person@acme.tld')->first(); 110 | $user->convertToRegistered(); 111 | ``` 112 | 113 | To disable the notification and password reset, pass the first argument as false. 114 | 115 | ```php 116 | $user->convertToRegistered(false); 117 | ``` 118 | -------------------------------------------------------------------------------- /docs/component-account.md: -------------------------------------------------------------------------------- 1 | # Account Component 2 | 3 | The account component provides a method to update the logged in user profile, verify email address, enable two-factor authentication, clear browser sessions and delete their account. 4 | 5 | ```ini 6 | title = "Account" 7 | url = "/account/:code?" 8 | 9 | [account] 10 | isDefault = 1 11 | == 12 | ... 13 | ``` 14 | 15 | For displaying and clearing other browser sessions for the user, the session driver must be set to `database`. Open the **config/session.php** file and change the driver, this can also be set in the **.env** file with the `SESSION_DRIVER` variable. 16 | 17 | ```php 18 | 'driver' => env('SESSION_DRIVER', 'database'), 19 | ``` 20 | 21 | ## API 22 | 23 | These AJAX handlers are available. 24 | 25 | Handler | Description 26 | ------- | ------------- 27 | **onUpdateProfile** | Updates the user profile 28 | **onVerifyEmail** | Verifies the user email address 29 | **onEnableTwoFactor** | Enables two-factor authentication 30 | **onConfirmTwoFactor** | Confirms two-factor authentication using a valid code 31 | **onShowTwoFactorRecoveryCodes** | Displays the two-factor recovery codes 32 | **onRegenerateTwoFactorRecoveryCodes** | Deletes and recreates the recovery codes 33 | **onDisableTwoFactor** | Disables two-factor authentication 34 | **onDeleteOtherSessions** | Logs out other user sessions 35 | **onDeleteUser** | Deletes the user account 36 | 37 | These variables are available on the component object. 38 | 39 | Variable | Description 40 | -------- | ------------- 41 | `user` | returns the logged in user 42 | `sessions` | returns browser sessions for the user 43 | `twoFactorEnabled` | returns true if the user has two factor enabled 44 | `twoFactorRecoveryCodes` | returns an array of recovery codes, if available 45 | 46 | ## Examples 47 | 48 | The following example shows how to update the user profile using the `onUpdateProfile` handler. 49 | 50 | ```html 51 |
56 | 57 | 64 | 65 | 72 | 73 | 79 |
80 | ``` 81 | -------------------------------------------------------------------------------- /docs/component-authentication.md: -------------------------------------------------------------------------------- 1 | # Authentication Component 2 | 3 | ## Overriding Functionality 4 | 5 | Here is how you would override the `onLogin()` handler to log any error messages. Inside the page code, define this method: 6 | 7 | ```php 8 | function onLogin() 9 | { 10 | try { 11 | return $this->account->onLogin(); 12 | } 13 | catch (Exception $ex) { 14 | Log::error($ex); 15 | } 16 | } 17 | ``` 18 | 19 | Here the local handler method will take priority over the **account** component's event handler. Then we simply inherit the logic by calling the parent handler manually, via the component object (`$this->account`). 20 | -------------------------------------------------------------------------------- /docs/component-registration.md: -------------------------------------------------------------------------------- 1 | # Registration Component 2 | 3 | ## Using a Login Name 4 | 5 | By default the User plugin will use the email address as the login name. To switch to using a user defined login name, navigate to the backend under System > Users > User Settings and change the Login attribute under the Sign in tab to be **Username**. Then simply ask for a username upon registration by adding the username field: 6 | 7 | ```twig 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | ``` 24 | 25 | We can add any other additional fields here too, such as `phone`, `company`, etc. 26 | 27 | 28 | 29 | ## Password Requirements 30 | 31 | ### Password Length 32 | 33 | By default, the User plugin requires a minimum password length of 8 characters for all users when registering or changing their password. You can change this length requirement by going to backend and navigating to System > Users > User Settings. Inside the Registration tab, a **Minimum password length** field is provided, allowing you to increase or decrease this limit to your preferred length. 34 | 35 | ### Password Confirmation 36 | 37 | The `password_confirmation` field can be used to prompt the user to enter their password a second time. This input name is optional, if it is found in the postback data, then it will be validated. The following is an example. 38 | 39 | ```twig 40 | 41 | 42 | ``` 43 | 44 | ## Error Handling 45 | 46 | ### Flash Messages 47 | 48 | This plugin makes use of October's [`Flash API`](https://octobercms.com/docs/markup/tag-flash). In order to display the error messages, you need to place the following snippet in your layout or page. 49 | 50 | ```twig 51 | {% flash %} 52 |
{{ message }}
53 | {% endflash %} 54 | ``` 55 | 56 | ### AJAX Errors 57 | 58 | The User plugin displays AJAX error messages in a simple ``alert()``-box by default. However, this might scare non-technical users. You can change the default behavior of an AJAX error from displaying an ``alert()`` message, like this: 59 | 60 | ```js 61 | 72 | ``` 73 | 74 | ### Checking Email/Username Availability 75 | 76 | Here is a simple example of how you can quickly check if an email address / username is available in your registration forms. First, inside the page code, define the following AJAX handler to check the login name, here we are using the email address: 77 | 78 | ```php 79 | public function onCheckEmail() 80 | { 81 | $user = Auth::getProvider()->retrieveByCredentials([ 82 | 'email' => post('email') 83 | ]); 84 | 85 | return ['isTaken' => $user ? 1 : 0]; 86 | } 87 | ``` 88 | 89 | For the email input we use the `data-request` and `data-track-input` attributes to call the `onCheckEmail` handler any time the field is updated. The `data-request-success` attribute will call some jQuery code to toggle the alert box. 90 | 91 | ```html 92 |
93 | 94 | 101 |
102 | 103 | 106 | ``` 107 | -------------------------------------------------------------------------------- /docs/component-reset-password.md: -------------------------------------------------------------------------------- 1 | # Reset Password Component 2 | 3 | The reset password component allows a user to reset their password if they have forgotten it. 4 | 5 | ```ini 6 | title = "Forgotten your password?" 7 | url = "/forgot-password/:code?" 8 | 9 | [resetPassword] 10 | paramCode = "code" 11 | == 12 | {% component 'resetPassword' %} 13 | ``` 14 | 15 | This will display the initial restoration request form and also the password reset form used after the verification email has been received by the user. The `paramCode` is the URL routing code used for resetting the password. 16 | -------------------------------------------------------------------------------- /docs/component-session.md: -------------------------------------------------------------------------------- 1 | # Session Component 2 | 3 | The session component should be added to a layout that has registered users. It has no default markup. 4 | 5 | ## User Variable 6 | 7 | You can check the logged in user by accessing the **{{ user }}** Twig variable: 8 | 9 | ```twig 10 | {% if user %} 11 |

Hello {{ user.first_name }}

12 | {% else %} 13 |

Nobody is logged in

14 | {% endif %} 15 | ``` 16 | 17 | ## Signing Out 18 | 19 | The Session component allows a user to sign out of their session. 20 | 21 | ```html 22 | Sign out 23 | ``` 24 | 25 | ## Page Restriction 26 | 27 | The Session component allows the restriction of a page or layout by allowing only signed in users, only guests or no restriction. This example shows how to restrict a page to users only: 28 | 29 | ```ini 30 | title = "Restricted page" 31 | url = "/users-only" 32 | 33 | [session] 34 | security = "user" 35 | redirect = "home" 36 | ``` 37 | 38 | The `security` property can be user, guest or all. The `redirect` property refers to a page name to redirect to when access is restricted. 39 | 40 | ## Route Restriction 41 | 42 | Access to routes can be restricted by applying the `AuthMiddleware`. 43 | 44 | ```php 45 | Route::group(['middleware' => \RainLab\User\Classes\AuthMiddleware::class], function () { 46 | // All routes here will require authentication 47 | }); 48 | ``` 49 | 50 | ## Token Variable 51 | 52 | The `token` Twig variable can be used for generating a new bearer token for the signed in user. 53 | 54 | ```twig 55 | {% do response( 56 | ajaxHandler('onLogin').withVars({ 57 | token: session.token 58 | }) 59 | ) %} 60 | ``` 61 | 62 | The `checkToken` property of the component is used to verify a supplied token in the request headers `(Authorization: Bearer TOKEN)`. 63 | 64 | ```ini 65 | [session] 66 | checkToken = 1 67 | ``` 68 | -------------------------------------------------------------------------------- /docs/docs.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # JSON Documentation 3 | # 4 | # Compile in console: 5 | # 6 | # php artisan october:util compile docs --value=rainlab.user 7 | # 8 | navigation: 9 | - 10 | title: "Introduction" 11 | link: "./introduction.md" 12 | description: "explains how to get started using the User plugin" 13 | children: 14 | - 15 | title: "Events" 16 | link: "./events.md" 17 | description: "lists all the available events found in this plugin" 18 | - 19 | title: "Tailor Integration" 20 | link: "./tailor.md" 21 | description: "guide on integration with TAilor" 22 | - 23 | title: "Components" 24 | description: "lists all the available components found in this plugin" 25 | children: 26 | - 27 | title: "Session" 28 | link: "./component-session.md" 29 | description: "checks the user session and includes the user object on the page" 30 | - 31 | title: "Account" 32 | link: "./component-account.md" 33 | description: "user management form for updating profile and security details" 34 | - 35 | title: "Authentication" 36 | link: "./component-authentication.md" 37 | description: "provides services for logging a user in" 38 | - 39 | title: "Registration" 40 | link: "./component-registration.md" 41 | description: "provides services for registering a user" 42 | - 43 | title: "Reset Password" 44 | link: "./component-reset-password.md" 45 | description: "confirms and resets the user with a new password" 46 | - 47 | title: "Services" 48 | children: 49 | - 50 | title: "Auth Manager" 51 | link: "./auth-manager.md" 52 | description: "services for managing the user session" 53 | - 54 | title: "Impersonation" 55 | link: "./auth-impersonation.md" 56 | description: "extra services for impersonating users" 57 | - 58 | title: "Bearer Tokens" 59 | link: "./auth-bearer-tokens.md" 60 | description: "extra services for JWT authentication" 61 | -------------------------------------------------------------------------------- /docs/events.md: -------------------------------------------------------------------------------- 1 | # Events 2 | 3 | This plugin will fire some global events that can be useful for interacting with other plugins. 4 | 5 | Events | Description 6 | ------ | --------------- 7 | **rainlab.user.beforeAuthenticate** | Before the user is attempting to authenticate using the Authentication component. 8 | **rainlab.user.authenticate** | Provides custom response logic after authentication. 9 | **rainlab.user.login** | The user has successfully signed in. 10 | **rainlab.user.logout** | The user has successfully signed out. 11 | **rainlab.user.lockout** | Provides custom logic when a login attempt has been rate limited. 12 | **rainlab.user.activate** | The user has verified their email address. 13 | **rainlab.user.deactivate** | The user has opted-out of the site by deactivating their account. This should be used to disable any content the user may want removed. 14 | **rainlab.user.beforeRegister** | Before the user's registration is processed. Passed the `$input` variable by reference to enable direct modifications to the user input. 15 | **rainlab.user.register** | Provides custom response logic after registration. 16 | **rainlab.user.passwordReset** | Provides custom logic for resetting a user password. 17 | **rainlab.user.beforeUpdate** | Before the user updates their profile from the Account component. 18 | **rainlab.user.update** | The user has updated their profile information. 19 | **rainlab.user.canDeleteUser** | Triggered before a user is deleted. This event should return true if the user has dependencies and should be soft deleted to retain those relationships and allow the user to be restored. Otherwise, it will be deleted forever. 20 | **rainlab.user.getNotificationVars** | Fires when sending a user notification to enable passing more variables to the email templates. Passes the `$user` model the template will be for. 21 | **rainlab.user.view.extendListToolbar** | Fires when the user listing page's toolbar is rendered. 22 | **rainlab.user.view.extendPreviewToolbar** | Fires when the user preview page's toolbar is rendered. 23 | **rainlab.user.view.extendPreviewTabs** | Provides an opportunity to add tabs to the user preview page in the admin panel. The event should return an array of `[Tab Name => ~/path/to/partial.php]` 24 | 25 | Here is an example of hooking an event: 26 | 27 | ```php 28 | Event::listen('rainlab.user.deactivate', function($user) { 29 | // Hide all posts by the user 30 | }); 31 | ``` 32 | 33 | A common requirement is to adapt another to a legacy authentication system. In the example below, the `WordPressLogin::check` method would check the user password using an alternative hashing method, and if successful, update to the new one used by October. 34 | 35 | ```php 36 | Event::listen('rainlab.user.beforeAuthenticate', function($component, $credentials) { 37 | $email = $credentials['email'] ?? null; 38 | $password = $credentials['password'] ?? null; 39 | 40 | // Check that the user exists with the provided email 41 | $user = Auth::getProvider()->retrieveByCredentials(['email' => $email]); 42 | if (!$user) { 43 | return; 44 | } 45 | 46 | // The user is logging in with their old WordPress account 47 | // for the first time. Rehash their password using the new 48 | // October system. 49 | if (WordPressLogin::check($user->password, $password)) { 50 | $user->password = $user->password_confirmation = $password; 51 | $user->forceSave(); 52 | } 53 | }); 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | The User plugin brings frontend users to the CMS, allowing your users to register and sign in to their account. 4 | 5 | To get started, we recommend installing this plugin with the `RainLab.Vanilla` theme to demonstrate its functionality. 6 | 7 | - https://github.com/rainlab/vanilla-theme 8 | -------------------------------------------------------------------------------- /docs/tailor.md: -------------------------------------------------------------------------------- 1 | # Tailor Integration 2 | 3 | This plugin includes integration with Tailor by providing content fields. 4 | 5 | ## Users Field 6 | 7 | The `users` field type allows association to one or more users via a Tailor Blueprint. The functionality is introduced by the `RainLab\User\ContentFields\UsersField` PHP class. 8 | 9 | The simplest example is to associate to a single user (belongs to relationship). 10 | 11 | ```yaml 12 | users: 13 | label: Users 14 | type: users 15 | maxItems: 1 16 | ``` 17 | 18 | Set the `maxItems` to **0** to associate to an unlimited number of users (belongs to many relationship). 19 | 20 | ```yaml 21 | users: 22 | label: Users 23 | type: users 24 | maxItems: 0 25 | ``` 26 | 27 | Set the `displayMode` to **controller** to show a more advanced user interface for user management. 28 | 29 | ```yaml 30 | users: 31 | label: Users 32 | type: users 33 | maxItems: 0 34 | displayMode: controller 35 | ``` 36 | 37 | You may also set the `displayMode` to **taglist** to select users by their email address. 38 | 39 | ```yaml 40 | users: 41 | label: Users 42 | type: users 43 | maxItems: 0 44 | displayMode: taglist 45 | ``` 46 | -------------------------------------------------------------------------------- /helpers/User.php: -------------------------------------------------------------------------------- 1 | AdminGroup::class, 51 | ]; 52 | 53 | /** 54 | * initSettingsData 55 | */ 56 | public function initSettingsData() 57 | { 58 | $this->block_persistence = Config::get('rainlab.user::block_persistence', false); 59 | $this->allow_registration = Config::get('rainlab.user::allow_registration', true); 60 | $this->login_attribute = Config::get('rainlab.user::login_attribute', self::LOGIN_EMAIL); 61 | 62 | $this->password_min_length = Config::get('rainlab.user::password_policy.min_length', 8); 63 | $this->password_require_mixed_case = Config::get('rainlab.user::password_policy.require_mixed_case', false); 64 | $this->password_require_uncompromised = Config::get('rainlab.user::password_policy.require_uncompromised', false); 65 | $this->password_require_number = Config::get('rainlab.user::password_policy.require_number', true); 66 | $this->password_require_symbol = Config::get('rainlab.user::password_policy.require_symbol', false); 67 | 68 | $this->user_message_template = 'user:welcome_email'; 69 | $this->system_message_template = 'user:new_user_internal'; 70 | } 71 | 72 | /** 73 | * getLoginAttributeOptions 74 | */ 75 | public function getLoginAttributeOptions() 76 | { 77 | return [ 78 | self::LOGIN_EMAIL => ["Email"], 79 | self::LOGIN_USERNAME => ["Username"] 80 | ]; 81 | } 82 | 83 | /** 84 | * makePasswordRule 85 | */ 86 | public static function makePasswordRule(): PasswordRule 87 | { 88 | $setting = static::instance(); 89 | $rule = PasswordRule::default(); 90 | 91 | if ($setting->password_min_length) { 92 | $rule->length($setting->password_min_length); 93 | } 94 | 95 | if ($setting->password_require_mixed_case) { 96 | $rule->mixedCase(); 97 | } 98 | 99 | if ($setting->password_require_uncompromised) { 100 | $rule->uncompromised(); 101 | } 102 | 103 | if ($setting->password_require_number) { 104 | $rule->numbers(); 105 | } 106 | 107 | if ($setting->password_require_symbol) { 108 | $rule->symbols(); 109 | } 110 | 111 | return $rule; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /models/UserGroup.php: -------------------------------------------------------------------------------- 1 | 'required|between:3,64', 36 | 'code' => 'required|regex:/^[a-zA-Z0-9_\-]+$/|unique:user_groups', 37 | ]; 38 | 39 | /** 40 | * @var array Relations 41 | */ 42 | public $belongsToMany = [ 43 | 'users' => [ 44 | User::class, 45 | 'table' => 'users_groups' 46 | ], 47 | 'users_count' => [ 48 | User::class, 49 | 'table' => 'users_groups', 50 | 'count' => true 51 | ] 52 | ]; 53 | 54 | /** 55 | * @var array The attributes that are mass assignable. 56 | */ 57 | protected $fillable = [ 58 | 'name', 59 | 'code', 60 | 'description' 61 | ]; 62 | 63 | /** 64 | * delete the group 65 | * @return bool 66 | */ 67 | public function delete() 68 | { 69 | $this->users()->detach(); 70 | 71 | return parent::delete(); 72 | } 73 | 74 | /** 75 | * getGuestGroup returns the default guest user group. 76 | */ 77 | public static function getGuestGroup(): ?static 78 | { 79 | return static::findByCode(self::GROUP_GUEST); 80 | } 81 | 82 | /** 83 | * getRegisteredGroup returns the default registered user group. 84 | */ 85 | public static function getRegisteredGroup(): ?static 86 | { 87 | return static::findByCode(self::GROUP_REGISTERED); 88 | } 89 | 90 | /** 91 | * scopeWithoutGuest 92 | */ 93 | public function scopeWithoutGuest($query) 94 | { 95 | return $query->where('code', '<>', self::GROUP_GUEST); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /models/UserLog.php: -------------------------------------------------------------------------------- 1 | [ 69 | User::class, 70 | 'scope' => 'withTrashed' 71 | ] 72 | ]; 73 | 74 | /** 75 | * createRecord adds a log for a user 76 | */ 77 | public static function createRecord($userId, $type, $data = []) 78 | { 79 | $data['ip_address'] = Request::ip(); 80 | 81 | $obj = new static; 82 | $obj->user_id = $userId; 83 | $obj->type = $type; 84 | $obj->data = $data; 85 | $obj->is_comment = false; 86 | $obj->is_system = false; 87 | $obj->setExpandoAttributes($data); 88 | $obj->save(); 89 | 90 | return $obj; 91 | } 92 | 93 | /** 94 | * createSystemRecord adds a log for a user, generated by an administrator 95 | */ 96 | public static function createSystemRecord($userId, $type, $data = []) 97 | { 98 | $data['ip_address'] = Request::ip(); 99 | 100 | $obj = new static; 101 | $obj->user_id = $userId; 102 | $obj->type = $type; 103 | $obj->data = $data; 104 | $obj->is_comment = false; 105 | $obj->is_system = true; 106 | $obj->setExpandoAttributes($data); 107 | $obj->save(); 108 | 109 | return $obj; 110 | } 111 | 112 | /** 113 | * createSystemComment adds a comment log for a user, generated by an administrator 114 | */ 115 | public static function createSystemComment($userId, $comment) 116 | { 117 | $data = []; 118 | $data['ip_address'] = Request::ip(); 119 | 120 | $obj = new static; 121 | $obj->user_id = $userId; 122 | $obj->type = self::TYPE_INTERNAL_COMMENT; 123 | $obj->comment = $comment; 124 | $obj->is_comment = true; 125 | $obj->is_system = true; 126 | $obj->data = $data; 127 | $obj->save(); 128 | 129 | return $obj; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /models/setting/fields.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # Field Definitions 3 | # =================================== 4 | 5 | tabs: 6 | fields: 7 | 8 | # Sign In tab 9 | 10 | login_attribute: 11 | label: Login Attribute 12 | span: auto 13 | commentAbove: Select what primary user detail should be used for signing in. 14 | type: radio 15 | tab: Sign In 16 | 17 | block_persistence: 18 | label: Prevent Concurrent Sessions 19 | span: full 20 | comment: When enabled users cannot sign in to multiple devices at the same time. 21 | type: checkbox 22 | tab: Sign In 23 | 24 | # Registration tab 25 | 26 | allow_registration: 27 | label: Allow User Registration 28 | span: full 29 | comment: If this is disabled users can only be created by administrators. 30 | type: switch 31 | tab: Registration 32 | 33 | _registration_ruler: 34 | type: ruler 35 | tab: Registration 36 | 37 | password_min_length: 38 | label: Password Length 39 | span: left 40 | type: number 41 | commentAbove: Minimum password length required for users 42 | tab: Registration 43 | 44 | password_require_mixed_case: 45 | label: Require Mixed Case 46 | span: left 47 | type: checkbox 48 | comment: Require at least one uppercase and lowercase letter 49 | tab: Registration 50 | 51 | password_require_uncompromised: 52 | label: Require Uncompromised 53 | span: right 54 | type: checkbox 55 | comment: Require a password not found in a leaked password database 56 | tab: Registration 57 | 58 | password_require_number: 59 | label: Require Number 60 | span: left 61 | type: checkbox 62 | comment: Require at least one number 63 | tab: Registration 64 | 65 | password_require_symbol: 66 | label: Require Special Character 67 | span: right 68 | type: checkbox 69 | comment: Require at least one non-alphanumeric character 70 | tab: Registration 71 | 72 | # Notifications tab 73 | 74 | _template_hint: 75 | type: hint 76 | comment: You can customize mail templates by selecting Mail → Mail Templates from the settings menu. 77 | tab: Notifications 78 | 79 | notify_user: 80 | label: Notify User 81 | type: checkbox 82 | tab: Notifications 83 | comment: Send a welcome message to the user when they confirm their email address. 84 | 85 | user_message_template: 86 | label: User Message Template 87 | commentAbove: Select the mail template to send to the user. 88 | type: dropdown 89 | optionsMethod: System\Models\MailTemplate::listAllTemplates 90 | valueTrans: false 91 | tab: Notifications 92 | cssClass: field-indent 93 | trigger: 94 | action: enable 95 | field: notify_user 96 | condition: checked 97 | 98 | notify_system: 99 | label: Notify Administrators 100 | type: checkbox 101 | tab: Notifications 102 | comment: Notify administrators when a new user confirms their email. 103 | 104 | system_message_template: 105 | label: System Message Template 106 | commentAbove: Select the mail template to send to the admins. 107 | type: dropdown 108 | optionsMethod: System\Models\MailTemplate::listAllTemplates 109 | valueTrans: false 110 | tab: Notifications 111 | cssClass: field-indent 112 | trigger: 113 | action: enable 114 | field: notify_system 115 | condition: checked 116 | 117 | admin_group: 118 | label: Notify Admin Group 119 | commentAbove: Specify the administrator group that should receive a notification when a new user joins. 120 | type: relation 121 | tab: Notifications 122 | span: full 123 | cssClass: field-indent 124 | trigger: 125 | action: enable 126 | field: notify_system 127 | condition: checked 128 | -------------------------------------------------------------------------------- /models/user/HasAuthenticatable.php: -------------------------------------------------------------------------------- 1 | getKeyName(); 20 | } 21 | 22 | /** 23 | * getAuthIdentifier for the user. 24 | */ 25 | public function getAuthIdentifier() 26 | { 27 | return $this->{$this->getAuthIdentifierName()}; 28 | } 29 | 30 | /** 31 | * getAuthPasswordName of the password attribute for the user. 32 | */ 33 | public function getAuthPasswordName() 34 | { 35 | return 'password'; 36 | } 37 | 38 | /** 39 | * getAuthIdentifierForBroadcasting 40 | */ 41 | public function getAuthIdentifierForBroadcasting() 42 | { 43 | return $this->getAuthIdentifier(); 44 | } 45 | 46 | /** 47 | * getAuthPassword for the user. 48 | * @return string 49 | */ 50 | public function getAuthPassword() 51 | { 52 | return $this->password; 53 | } 54 | 55 | /** 56 | * getRememberToken value for the "remember me" session. 57 | * @return string|null 58 | */ 59 | public function getRememberToken() 60 | { 61 | if (!empty($this->getRememberTokenName())) { 62 | return (string) $this->{$this->getRememberTokenName()}; 63 | } 64 | } 65 | 66 | /** 67 | * setRememberToken value for the "remember me" session. 68 | * @param string $value 69 | */ 70 | public function setRememberToken($value) 71 | { 72 | if (!empty($this->getRememberTokenName())) { 73 | $this->{$this->getRememberTokenName()} = $value; 74 | } 75 | } 76 | 77 | /** 78 | * getRememberTokenName gets the column name for the "remember me" token. 79 | * @return string 80 | */ 81 | public function getRememberTokenName() 82 | { 83 | return $this->rememberTokenName; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /models/user/HasModelAttributes.php: -------------------------------------------------------------------------------- 1 | {$attribute}; 28 | } 29 | 30 | /** 31 | * getFullNameAttribute 32 | */ 33 | public function getFullNameAttribute() 34 | { 35 | return "{$this->first_name} {$this->last_name}"; 36 | } 37 | 38 | /** 39 | * getAvatarUrl 40 | */ 41 | public function getAvatarUrlAttribute() 42 | { 43 | return $this->relationLoaded('avatar') 44 | ? $this->getAvatarThumb() 45 | : null; 46 | } 47 | 48 | /** 49 | * getIsBannedAttribute 50 | */ 51 | public function getIsBannedAttribute() 52 | { 53 | return $this->banned_at !== null; 54 | } 55 | 56 | /** 57 | * getIsActivatedAttribute 58 | */ 59 | public function getIsActivatedAttribute() 60 | { 61 | return $this->activated_at !== null; 62 | } 63 | 64 | /** 65 | * getIsTwoFactorEnabledAttribute 66 | */ 67 | public function getIsTwoFactorEnabledAttribute() 68 | { 69 | return $this->two_factor_confirmed_at !== null; 70 | } 71 | 72 | /** 73 | * setIsTwoFactorEnabledAttribute 74 | */ 75 | public function setIsTwoFactorEnabledAttribute($value) 76 | { 77 | if ($value && !$this->two_factor_confirmed_at) { 78 | $this->two_factor_confirmed_at = $this->freshTimestamp(); 79 | } 80 | 81 | if (!$value) { 82 | $this->two_factor_confirmed_at = null; 83 | } 84 | } 85 | 86 | /** 87 | * setPasswordAttribute protects the password from being reset to null 88 | */ 89 | public function setPasswordAttribute($value) 90 | { 91 | if ($this->exists && empty($value)) { 92 | unset($this->attributes['password']); 93 | } 94 | else { 95 | $this->attributes['password'] = $value; 96 | } 97 | } 98 | 99 | /** 100 | * @deprecated use `login` attribute 101 | * @see getLoginAttribute 102 | */ 103 | public function getLogin() 104 | { 105 | return $this->login; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /models/user/HasModelScopes.php: -------------------------------------------------------------------------------- 1 | where('is_guest', false); 17 | } 18 | 19 | /** 20 | * applyStatusCode 21 | */ 22 | public function scopeApplyStatusCode($query, $value) 23 | { 24 | if ($value instanceof \Backend\Classes\FilterScope) { 25 | $value = $value->value; 26 | } 27 | 28 | if ($value === 'deleted') { 29 | return $query->onlyTrashed(); 30 | } 31 | 32 | if ($value === 'active') { 33 | return $query->withoutTrashed()->whereNotNull('activated_at'); 34 | } 35 | 36 | if ($value === 'inactive') { 37 | return $query->withoutTrashed()->whereNull('activated_at'); 38 | } 39 | 40 | return $query->withoutTrashed(); 41 | } 42 | 43 | /** 44 | * scopeIsActivated 45 | */ 46 | public function scopeIsActivated($query) 47 | { 48 | return $query->whereNotNull('activated_at'); 49 | } 50 | 51 | /** 52 | * scopeApplyPrimaryGroup 53 | */ 54 | public function scopeApplyPrimaryGroup($query, $filter) 55 | { 56 | if ($filter instanceof \Backend\Classes\FilterScope) { 57 | $filter = $filter->value; 58 | } 59 | 60 | return $query->whereIn('primary_group_id', (array) $filter); 61 | } 62 | 63 | /** 64 | * scopeApplyGroups 65 | */ 66 | public function scopeApplyGroups($query, $filter) 67 | { 68 | return $query->whereHas('groups', function($group) use ($filter) { 69 | $group->whereIn('id', $filter); 70 | }); 71 | } 72 | 73 | /** 74 | * @deprecated 75 | */ 76 | public function scopeFilterByGroup($query, $filter) 77 | { 78 | return $this->scopeApplyGroups($query, $filter); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /models/user/HasPasswordReset.php: -------------------------------------------------------------------------------- 1 | passwordResetUrl = $url; 24 | } 25 | 26 | /** 27 | * getEmailForPasswordReset 28 | * @return string 29 | */ 30 | public function getEmailForPasswordReset() 31 | { 32 | return $this->email; 33 | } 34 | 35 | /** 36 | * sendPasswordResetNotification 37 | * @param string $token 38 | */ 39 | public function sendPasswordResetNotification($token) 40 | { 41 | $url = $this->passwordResetUrl ?: Cms::entryUrl('resetPassword'); 42 | $url .= str_contains($url, '?') ? '&' : '?'; 43 | $url .= http_build_query([ 44 | 'reset' => $token, 45 | 'email' => $this->getEmailForPasswordReset() 46 | ]); 47 | 48 | $data = [ 49 | 'url' => $url, 50 | 'token' => $token, 51 | 'count' => Config::get('auth.passwords.users.expire') 52 | ]; 53 | 54 | $data += $this->getNotificationVars(); 55 | 56 | Mail::send('user:recover_password', $data, function($message) { 57 | $message->to($this->email, $this->full_name); 58 | }); 59 | } 60 | 61 | /** 62 | * sendConfirmRegistrationNotification welcomes a new user an invites them to set a password 63 | */ 64 | public function sendConfirmRegistrationNotification() 65 | { 66 | $token = Password::createToken($this); 67 | 68 | $url = $this->passwordResetUrl ?: Cms::entryUrl('resetPassword'); 69 | $url .= str_contains($url, '?') ? '&' : '?'; 70 | $url .= http_build_query([ 71 | 'reset' => $token, 72 | 'email' => $this->getEmailForPasswordReset(), 73 | 'new' => true 74 | ]); 75 | 76 | $data = [ 77 | 'url' => $url, 78 | 'token' => $token, 79 | 'count' => Config::get('auth.passwords.users.expire') 80 | ]; 81 | 82 | $data += $this->getNotificationVars(); 83 | 84 | Mail::send('user:invite_email', $data, function($message) { 85 | $message->to($this->email, $this->full_name); 86 | }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /models/user/HasPersistCode.php: -------------------------------------------------------------------------------- 1 | persist_code) { 18 | return $this->persist_code; 19 | } 20 | 21 | $timestamps = $this->timestamps; 22 | 23 | $this->timestamps = false; 24 | 25 | $this->setPersistCode($code = $this->generatePersistCode()); 26 | 27 | $this->save(['force' => true]); 28 | 29 | $this->timestamps = $timestamps; 30 | 31 | return $code; 32 | } 33 | 34 | /** 35 | * setPersistCode sets a persistence code 36 | */ 37 | public function setPersistCode($code) 38 | { 39 | $this->persist_code = $code; 40 | } 41 | 42 | /** 43 | * generateRecoveryCode 44 | */ 45 | public function generatePersistCode(): string 46 | { 47 | return Str::random(42); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /models/user/HasTwoFactor.php: -------------------------------------------------------------------------------- 1 | generateSecretKey(); 26 | 27 | $recoveryCodes = Collection::times(8, function() { 28 | return $this->generateRecoveryCode(); 29 | })->all(); 30 | 31 | $this->forceFill([ 32 | 'two_factor_secret' => $secretKey, 33 | 'two_factor_recovery_codes' => json_encode($recoveryCodes), 34 | ]); 35 | 36 | $this->save(); 37 | } 38 | 39 | /** 40 | * generateNewRecoveryCodes 41 | */ 42 | public function generateNewRecoveryCodes() 43 | { 44 | $recoveryCodes = Collection::times(8, function() { 45 | return $this->generateRecoveryCode(); 46 | })->all(); 47 | 48 | $this->forceFill([ 49 | 'two_factor_recovery_codes' => json_encode($recoveryCodes), 50 | ]); 51 | 52 | $this->save(); 53 | } 54 | 55 | /** 56 | * disableTwoFactorAuthentication 57 | */ 58 | public function disableTwoFactorAuthentication() 59 | { 60 | if ($this->two_factor_secret !== null || 61 | $this->two_factor_recovery_codes !== null || 62 | $this->two_factor_confirmed_at !== null 63 | ) { 64 | $this->forceFill([ 65 | 'two_factor_secret' => null, 66 | 'two_factor_recovery_codes' => null, 67 | 'two_factor_confirmed_at' => null 68 | ]); 69 | 70 | $this->save(); 71 | } 72 | } 73 | 74 | /** 75 | * @return bool hasEnabledTwoFactorAuthentication determines if two-factor authentication has been enabled. 76 | */ 77 | public function hasEnabledTwoFactorAuthentication() 78 | { 79 | return $this->two_factor_secret !== null && $this->two_factor_confirmed_at !== null; 80 | } 81 | 82 | /** 83 | * recoveryCodes gets the user two factor authentication recovery codes. 84 | */ 85 | public function recoveryCodes(): array 86 | { 87 | return json_decode($this->two_factor_recovery_codes, true); 88 | } 89 | 90 | /** 91 | * replaceRecoveryCode with a new one in the user's stored codes. 92 | */ 93 | public function replaceRecoveryCode(string $code) 94 | { 95 | $this->forceFill([ 96 | 'two_factor_recovery_codes' => str_replace( 97 | $code, 98 | $this->generateRecoveryCode(), 99 | $this->two_factor_recovery_codes 100 | ), 101 | ]); 102 | 103 | $this->save(); 104 | } 105 | 106 | /** 107 | * twoFactorQrCodeSvg gets the QR code SVG of the user's two factor authentication QR code URL. 108 | */ 109 | public function twoFactorQrCodeSvg(): string 110 | { 111 | $url = $this->twoFactorQrCodeUrl(); 112 | if (!$url) { 113 | return ''; 114 | } 115 | 116 | $svg = (new Writer( 117 | new ImageRenderer( 118 | new RendererStyle(192, 0, null, null, Fill::uniformColor(new Rgb(255, 255, 255), new Rgb(45, 55, 72))), 119 | new SvgImageBackEnd 120 | ) 121 | ))->writeString($url); 122 | 123 | return trim(substr($svg, strpos($svg, "\n") + 1)); 124 | } 125 | 126 | /** 127 | * twoFactorQrCodeUrl gets the two factor authentication QR code URL. 128 | */ 129 | public function twoFactorQrCodeUrl(): string 130 | { 131 | if (!$this->two_factor_secret) { 132 | return ''; 133 | } 134 | 135 | return TwoFactorManager::instance()->qrCodeUrl( 136 | Config::Get('app.name'), 137 | $this->{UserHelper::username()}, 138 | $this->two_factor_secret 139 | ); 140 | } 141 | 142 | /** 143 | * generateRecoveryCode 144 | */ 145 | public function generateRecoveryCode(): string 146 | { 147 | return Str::random(10) . '-' . Str::random(10); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /models/user/UserExport.php: -------------------------------------------------------------------------------- 1 | type)); 27 | 28 | $path = plugins_path("rainlab/user/models/userlog/_detail_{$typeName}.php"); 29 | 30 | if ($event = Event::fire('rainlab.user.extendLogDetailViewPath', [$this, $this->type], true)) { 31 | $path = $event; 32 | } 33 | 34 | return file_exists($path) ? View::file($path, ['record' => $this]) : "{$this->type} event"; 35 | } 36 | 37 | /** 38 | * getActorNameAttribute returns the `actor_admin_name` attribute 39 | */ 40 | public function getActorAdminNameAttribute() 41 | { 42 | if ($this->created_user_id) { 43 | return $this->created_user_id === $this->getUserFootprintAuth()->id() 44 | ? __("You") 45 | : ($this->created_user?->full_name ?: __("Admin")); 46 | } 47 | 48 | return __("System"); 49 | } 50 | 51 | /** 52 | * getActorNameAttribute returns the `actor_user_name` attribute 53 | */ 54 | public function getActorUserNameAttribute() 55 | { 56 | if ($this->user_full_name) { 57 | return $this->user_full_name; 58 | } 59 | 60 | if ($this->user_id) { 61 | return $this->user?->full_name ?: __("User"); 62 | } 63 | 64 | return __("User"); 65 | } 66 | 67 | /** 68 | * getUserBackendLinkageAttribute returns the `user_backend_linkage` attribute 69 | */ 70 | public function getUserBackendLinkageAttribute() 71 | { 72 | if (App::runningInBackend() && $this->user) { 73 | return [ 74 | Backend::url("user/users/preview/{$this->user->id}"), 75 | $this->user->full_name 76 | ]; 77 | } 78 | 79 | return ''; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /models/userlog/_column_detail.php: -------------------------------------------------------------------------------- 1 | detail ?> 2 | -------------------------------------------------------------------------------- /models/userlog/_detail_new_user.php: -------------------------------------------------------------------------------- 1 | is_system): ?> 2 | e($record->actor_admin_name)]) ?> 3 | 4 | e($record->actor_user_name)]) ?> 5 | 6 | -------------------------------------------------------------------------------- /models/userlog/_detail_self_delete.php: -------------------------------------------------------------------------------- 1 | e($record->actor_user_name)]) ?> 2 | -------------------------------------------------------------------------------- /models/userlog/_detail_self_login.php: -------------------------------------------------------------------------------- 1 | is_two_factor): ?> 2 | e($record->actor_user_name)]) ?> 3 | 4 | e($record->actor_user_name)]) ?> 5 | 6 | -------------------------------------------------------------------------------- /models/userlog/_detail_self_verify.php: -------------------------------------------------------------------------------- 1 | e($record->actor_user_name), 3 | 'user_email' => e($record->user_email), 4 | ]) ?> 5 | -------------------------------------------------------------------------------- /models/userlog/_detail_set_email.php: -------------------------------------------------------------------------------- 1 | e($record->old_value), 4 | 'new_value' => e($record->new_value) 5 | ]; 6 | ?> 7 | is_system): ?> 8 | e($record->actor_admin_name), 10 | ]) ?> 11 | 12 | e($record->actor_user_name) 14 | ]) ?> 15 | 16 | -------------------------------------------------------------------------------- /models/userlog/_detail_set_password.php: -------------------------------------------------------------------------------- 1 | is_system): ?> 2 | e($record->actor_admin_name), 4 | ]) ?> 5 | 6 | e($record->actor_user_name) 8 | ]) ?> 9 | 10 | -------------------------------------------------------------------------------- /models/userlog/_detail_set_two_factor.php: -------------------------------------------------------------------------------- 1 | is_two_factor_enabled): ?> 2 | e($record->actor_user_name)]) ?> 3 | 4 | e($record->actor_user_name)]) ?> 5 | 6 | -------------------------------------------------------------------------------- /models/userlog/columns-global.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # List Column Definitions 3 | # =================================== 4 | 5 | columns: 6 | detail: 7 | label: Detail 8 | type: partial 9 | searchable: false 10 | sortable: false 11 | 12 | user_full_name: 13 | label: User 14 | relation: user 15 | valueFrom: fullname 16 | type: linkage 17 | displayFrom: user_backend_linkage 18 | 19 | created_at: 20 | label: Created 21 | type: timetense 22 | sortable: true 23 | searchable: false 24 | -------------------------------------------------------------------------------- /models/userlog/columns.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # List Column Definitions 3 | # =================================== 4 | 5 | columns: 6 | detail: 7 | label: Detail 8 | type: partial 9 | searchable: false 10 | sortable: false 11 | 12 | created_at: 13 | label: Created 14 | type: timetense 15 | sortable: true 16 | searchable: false 17 | -------------------------------------------------------------------------------- /models/userlog/fields.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # Form Field Definitions 3 | # =================================== 4 | 5 | fields: 6 | id: 7 | label: ID 8 | disabled: true 9 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /tests/AuthManagerTest.php: -------------------------------------------------------------------------------- 1 | 'Some', 17 | 'last_name' => 'User', 18 | 'email' => 'some@website.tld', 19 | 'password' => 'ChangeMe888', 20 | 'password_confirmation' => 'ChangeMe888', 21 | ]); 22 | 23 | $this->assertEquals(1, User::count()); 24 | $this->assertInstanceOf(User::class, $user); 25 | 26 | $this->assertFalse($user->is_activated); 27 | $this->assertEquals('Some User', $user->full_name); 28 | $this->assertEquals('some@website.tld', $user->email); 29 | } 30 | 31 | /** 32 | * testRegisterUserWithAutoActivation 33 | */ 34 | public function testRegisterUserWithAutoActivation() 35 | { 36 | // Stop activation events from other plugins 37 | Event::forget('rainlab.user.activate'); 38 | 39 | $user = User::create([ 40 | 'first_name' => 'Some', 41 | 'last_name' => 'User', 42 | 'email' => 'some@website.tld', 43 | 'password' => 'ChangeMe888', 44 | 'password_confirmation' => 'ChangeMe888', 45 | ]); 46 | 47 | $user->markEmailAsVerified(); 48 | Auth::loginQuietly($user); 49 | 50 | $this->assertTrue($user->is_activated); 51 | $this->assertTrue($user->hasVerifiedEmail()); 52 | $this->assertNotNull(Auth::user()); 53 | } 54 | 55 | /** 56 | * testRegisterGuest 57 | */ 58 | public function testRegisterGuest() 59 | { 60 | $guest = User::create([ 61 | 'first_name' => 'Some', 62 | 'last_name' => 'User', 63 | 'email' => 'person@acme.tld', 64 | 'is_guest' => true 65 | ]); 66 | 67 | $this->assertEquals(1, User::count()); 68 | $this->assertInstanceOf(User::class, $guest); 69 | 70 | $this->assertTrue($guest->is_guest); 71 | $this->assertEquals('person@acme.tld', $guest->email); 72 | 73 | $secondGuest = User::create([ 74 | 'first_name' => 'Some', 75 | 'last_name' => 'User', 76 | 'email' => 'person@acme.tld', 77 | 'password' => 'ChangeMe888', 78 | 'password_confirmation' => 'ChangeMe888', 79 | 'is_guest' => true 80 | ]); 81 | 82 | $this->assertEquals(2, User::count()); 83 | $this->assertInstanceOf(User::class, $secondGuest); 84 | $this->assertTrue($guest->is_guest); 85 | $this->assertEquals('person@acme.tld', $guest->email); 86 | 87 | $firstGuest = User::where('email', 'person@acme.tld')->first(); 88 | $firstGuest->convertToRegistered(false); 89 | 90 | $this->assertEquals('person@acme.tld', $firstGuest->email); 91 | $this->assertFalse($firstGuest->is_guest); 92 | } 93 | 94 | /** 95 | * testLoginAndCheckAuth 96 | */ 97 | public function testLoginAndCheckAuth() 98 | { 99 | $this->assertFalse(Auth::check()); 100 | 101 | $user = User::create([ 102 | 'first_name' => 'Some', 103 | 'last_name' => 'User', 104 | 'email' => 'some@website.tld', 105 | 'password' => 'ChangeMe888', 106 | 'password_confirmation' => 'ChangeMe888', 107 | ]); 108 | 109 | $user->markEmailAsVerified(); 110 | 111 | Auth::login($user); 112 | 113 | $this->assertTrue(Auth::check()); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /traits/ConfirmsPassword.php: -------------------------------------------------------------------------------- 1 | isUserPasswordValid(post('confirmable_password'))) { 23 | throw new ValidationException([ 24 | 'confirmable_password' => __('This password does not match our records.'), 25 | ]); 26 | } 27 | 28 | Session::put(['auth.password_confirmed_at' => time()]); 29 | 30 | $this->dispatchBrowserEvent('app:password-confirmed'); 31 | 32 | return ['passwordConfirmed' => true]; 33 | } 34 | 35 | /** 36 | * isUserPasswordValid 37 | */ 38 | protected function isUserPasswordValid(string $password): bool 39 | { 40 | $user = Auth::user(); 41 | $username = UserHelper::username(); 42 | 43 | if (!$user || !$password) { 44 | return false; 45 | } 46 | 47 | return Auth::validate([ 48 | $username => $user->{$username}, 49 | 'password' => $password 50 | ]); 51 | } 52 | 53 | /** 54 | * checkConfirmedPassword checks if the user password has been confirmed 55 | */ 56 | protected function checkConfirmedPassword() 57 | { 58 | if ($this->passwordIsConfirmed()) { 59 | return; 60 | } 61 | 62 | $this->dispatchBrowserEvent('app:password-confirming'); 63 | 64 | return ['confirmingPassword' => true]; 65 | } 66 | 67 | /** 68 | * passwordIsConfirmed determine if the user's password has been recently confirmed. 69 | */ 70 | protected function passwordIsConfirmed(?int $maxSecondsSinceConfirmation = null): bool 71 | { 72 | $maxSecondsSinceConfirmation = $maxSecondsSinceConfirmation ?: Config::get('auth.password_timeout', 900); 73 | 74 | return (time() - Session::get('auth.password_confirmed_at', 0)) < $maxSecondsSinceConfirmation; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /updates/000001_create_users.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 12 | $table->boolean('is_guest')->default(false); 13 | $table->boolean('is_mail_blocked')->default(false); 14 | $table->string('first_name')->nullable(); 15 | $table->string('last_name')->nullable(); 16 | $table->string('username')->nullable()->index(); 17 | $table->string('email'); 18 | $table->mediumText('notes')->nullable(); 19 | $table->string('password'); 20 | $table->string('activation_code')->nullable()->index(); 21 | $table->string('persist_code')->nullable(); 22 | $table->string('remember_token')->nullable(); 23 | $table->text('two_factor_secret')->nullable(); 24 | $table->text('two_factor_recovery_codes')->nullable(); 25 | $table->bigInteger('primary_group_id')->nullable()->unsigned(); 26 | $table->string('created_ip_address')->nullable(); 27 | $table->string('last_ip_address')->nullable(); 28 | $table->text('banned_reason')->nullable(); 29 | $table->timestamp('banned_at')->nullable(); 30 | $table->timestamp('activated_at')->nullable(); 31 | $table->timestamp('two_factor_confirmed_at')->nullable(); 32 | $table->timestamp('last_seen')->nullable(); 33 | $table->timestamp('deleted_at')->nullable(); 34 | $table->timestamps(); 35 | }); 36 | } 37 | 38 | public function down() 39 | { 40 | Schema::dropIfExists('users'); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /updates/000002_create_password_resets.php: -------------------------------------------------------------------------------- 1 | string('email')->primary(); 12 | $table->string('token'); 13 | $table->timestamp('created_at')->nullable(); 14 | }); 15 | } 16 | 17 | public function down() 18 | { 19 | Schema::dropIfExists('user_password_resets'); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /updates/000003_create_user_groups.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 12 | $table->string('name'); 13 | $table->string('code')->nullable()->index(); 14 | $table->text('description')->nullable(); 15 | $table->timestamps(); 16 | }); 17 | 18 | Schema::create('users_groups', function(Blueprint $table) { 19 | $table->bigInteger('user_id')->unsigned(); 20 | $table->bigInteger('user_group_id')->unsigned(); 21 | $table->primary(['user_id', 'user_group_id'], 'rainlab_user_group'); 22 | }); 23 | } 24 | 25 | public function down() 26 | { 27 | Schema::dropIfExists('user_groups'); 28 | Schema::dropIfExists('users_groups'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /updates/000004_create_user_preferences.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 12 | $table->bigInteger('user_id')->unsigned()->nullable()->index(); 13 | $table->string('item')->index()->nullable(); 14 | $table->mediumText('value')->nullable(); 15 | $table->timestamps(); 16 | }); 17 | } 18 | 19 | public function down() 20 | { 21 | Schema::dropIfExists('rainlab_user_preferences'); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /updates/000005_create_user_logs.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 19 | $table->bigInteger('user_id')->unsigned()->nullable()->index(); 20 | $table->string('type')->nullable(); 21 | $table->mediumText('data')->nullable(); 22 | $table->mediumText('comment')->nullable(); 23 | $table->boolean('is_comment')->default(false)->index(); 24 | $table->boolean('is_system')->default(false); 25 | $table->bigInteger('updated_user_id')->unsigned()->nullable(); 26 | $table->bigInteger('created_user_id')->unsigned()->nullable(); 27 | $table->timestamps(); 28 | }); 29 | } 30 | 31 | /** 32 | * down reverses the migration 33 | */ 34 | public function down() 35 | { 36 | Schema::dropIfExists('rainlab_user_user_logs'); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /updates/migrate_v3_0_0.php: -------------------------------------------------------------------------------- 1 | setUp(__DIR__.'/000002_create_password_resets.php'); 13 | $updater->setUp(__DIR__.'/000004_create_user_preferences.php'); 14 | $updater->setUp(__DIR__.'/000005_create_user_logs.php'); 15 | } 16 | 17 | if (!Schema::hasColumn('users', 'first_name')) { 18 | Schema::table('users', function(Blueprint $table) { 19 | $table->boolean('is_mail_blocked')->default(false); 20 | $table->string('first_name')->nullable(); 21 | $table->string('last_name')->nullable(); 22 | $table->mediumText('notes')->nullable(); 23 | $table->bigInteger('primary_group_id')->nullable()->unsigned(); 24 | $table->string('remember_token')->nullable(); 25 | $table->text('banned_reason')->nullable(); 26 | $table->timestamp('banned_at')->nullable(); 27 | $table->text('two_factor_secret')->nullable(); 28 | $table->text('two_factor_recovery_codes')->nullable(); 29 | $table->timestamp('two_factor_confirmed_at')->nullable(); 30 | }); 31 | 32 | Db::update("update users set first_name=name, last_name=surname"); 33 | } 34 | 35 | if (Schema::hasTable('rainlab_user_mail_blockers')) { 36 | $emails = Db::table('rainlab_user_mail_blockers')->where('template', '*')->pluck('email'); 37 | foreach ($emails->chunk(100) as $chunks) { 38 | Db::table('users')->whereIn('email', $chunks)->update(['is_mail_blocked' => true]); 39 | } 40 | } 41 | } 42 | 43 | public function down() 44 | { 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /updates/migrate_v3_1_0.php: -------------------------------------------------------------------------------- 1 | string('activation_code')->nullable()->index(); 13 | }); 14 | } 15 | } 16 | 17 | public function down() 18 | { 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /updates/seed_tables.php: -------------------------------------------------------------------------------- 1 | 'Guest', 18 | 'code' => 'guest', 19 | 'description' => 'Default group for guest users.' 20 | ]); 21 | 22 | UserGroup::create([ 23 | 'name' => 'Registered', 24 | 'code' => 'registered', 25 | 'description' => 'Default group for registered users.' 26 | ]); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /updates/version.yaml: -------------------------------------------------------------------------------- 1 | v1.0.1: First version of User 2 | v1.0.2: 3 | - Set up database tables 4 | - 000001_create_users.php 5 | - 000002_create_password_resets.php 6 | - 000003_create_user_groups.php 7 | - 000004_create_user_preferences.php 8 | - 000005_create_user_logs.php 9 | v1.0.3: 10 | - Seed all database tables 11 | - seed_tables.php 12 | v1.1.0: Profile fields and Locations have been removed. 13 | v1.2.0: Users can now deactivate their own accounts. 14 | v1.3.0: Introduced guest user accounts. 15 | v1.4.0: The Notifications tab in User Settings has been removed. 16 | v1.5.0: Required password length is now a minimum of 8 characters. Previous passwords will not be affected until the next password change. 17 | v1.6.0: Apply persistence settings on activation and registration. Fixes last seen touched when impersonating. Fixes user suspension not clearing. 18 | v1.7.0: Add password policy 19 | v1.7.1: Fixes compatibility with legacy sites 20 | v2.0.0: Compatibility with October v3 only 21 | v2.1.0: Adds bearer token (JWT) support to session component 22 | v3.0.0: 23 | - Major Upgrade to User Plugin 24 | - migrate_v3_0_0.php 25 | v3.0.4: Fixes bug in JWT authentication check 26 | v3.0.5: Adds permission for viewing user activity log 27 | v3.0.7: Fixes missing notification variables in mail templates 28 | v3.1.0: 29 | - New email verification logic 30 | - migrate_v3_1_0.php 31 | v3.1.1: Fixes compatibility with Mall plugin 32 | v3.1.3: Translation updates 33 | v3.2.0: Fixes events with 2FA, requires October v3.7 or above 34 | v3.2.1: Fixes support with Tailor integration 35 | -------------------------------------------------------------------------------- /views/mail/invite_email.htm: -------------------------------------------------------------------------------- 1 | subject = "An Account Has Been Created For You" 2 | description = "Invite a new user to the website" 3 | == 4 | Hello {{ first_name }} 5 | 6 | You are receiving this email because we received a request to create a new user account for you. If you did not perform this request, you can safely ignore this email. 7 | 8 | {% partial 'button' url=url body %} 9 | Confirm Account 10 | {% endpartial %} 11 | 12 | This confirmation link will expire in {{ count }} minutes. You may attempt this again using our password recovery service. 13 | 14 | {% partial 'subcopy' body %} 15 | If you're having trouble clicking the button, copy the URL below into your browser. 16 | 17 | {{ url|raw }} 18 | {% endpartial %} 19 | -------------------------------------------------------------------------------- /views/mail/new_user_internal.htm: -------------------------------------------------------------------------------- 1 | subject = "A new user has signed up" 2 | description = "Notify admins of a new sign up" 3 | layout = "system" 4 | == 5 | A new user has just signed up. Here are there details: 6 | 7 | - ID: `{{ id }}` 8 | - Name: `{{ full_name }}` 9 | - Email: `{{ email }}` 10 | -------------------------------------------------------------------------------- /views/mail/recover_password.htm: -------------------------------------------------------------------------------- 1 | subject = "Reset Password Notification" 2 | description = "User has requested to recover their password" 3 | == 4 | # Hello {{ first_name }} 5 | 6 | You are receiving this email because we received a password recovery request for your account. If you did not perform this request, you can safely ignore this email. 7 | 8 | {% partial 'button' url=url body %} 9 | Reset Password 10 | {% endpartial %} 11 | 12 | This password reset link will expire in {{ count }} minutes. 13 | 14 | {% partial 'subcopy' body %} 15 | If you're having trouble clicking the button, copy the URL below into your browser. 16 | 17 | {{ url|raw }} 18 | {% endpartial %} 19 | -------------------------------------------------------------------------------- /views/mail/verify_email.htm: -------------------------------------------------------------------------------- 1 | subject = "Confirm your email address" 2 | description = "Verification for a user email address" 3 | == 4 | # Hello {{ first_name }} 5 | 6 | Please click the button below to verify your email address. If you did not create this account, no further action is required. 7 | 8 | {% partial 'button' url=url body %} 9 | Confirm Email Address 10 | {% endpartial %} 11 | 12 | {% partial 'subcopy' body %} 13 | If you're having trouble clicking the button, copy the URL below into your browser. 14 | 15 | {{ url|raw }} 16 | {% endpartial %} 17 | -------------------------------------------------------------------------------- /views/mail/welcome_email.htm: -------------------------------------------------------------------------------- 1 | subject = "Your account has been confirmed!" 2 | == 3 | Hello {{ name }}, 4 | 5 | This is a message to inform you that your account has been activated successfully. 6 | 7 | == 8 |

Hello {{ name }},

9 | 10 |

This is a message to inform you that your account has been activated successfully.

11 | --------------------------------------------------------------------------------