├── .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 | -------------------------------------------------------------------------------- /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 |
= __("Use this checkbox to generate new random password for the user and send a registration notification email.") ?>
28 |7 | = __("This user has not confirmed their email address.") ?> 8 | " 11 | data-stripe-load-indicator 12 | >= __("Activate this user manually") ?>. 13 |
14 |7 | = __("This user has been banned by an administrator and will be unable to sign in.") ?> 8 | " 11 | data-stripe-load-indicator 12 | >= __("Unban this user") ?>. 13 |
14 |7 | = __("This user is stored for reference purposes only and needs to register before signing in.") ?> 8 |
9 |7 | = __("This user account has been deactivated can no longer be used.") ?> 8 | = __("This account cannot be deleted since it contains internal references.") ?> 9 | 10 |
11 |= e($formModel->full_name) ?>
18 | 19 |= __("Anonymous") ?>
20 | 21 |22 | = __("Email") ?>: = Html::mailto($formModel->email) ?> 23 |
24 |= $formModel->is_guest ? __("Guest") : __("Registered") ?>
29 |30 | = __("Created") ?>: = $formModel->created_at->toFormattedDateString() ?> 31 |
32 |= $formModel->last_seen->diffForHumans() ?>
38 |39 | = $formModel->isOnline() ? __("Online now") : __("Currently offline") ?> 40 |
41 |= $formModel->deleted_at->toFormattedDateString() ?>
48 | 54 |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 | = $record->detail ?> 2 | -------------------------------------------------------------------------------- /models/userlog/_detail_new_user.php: -------------------------------------------------------------------------------- 1 | is_system): ?> 2 | = __(":name created this user", ['name' => e($record->actor_admin_name)]) ?> 3 | 4 | = __(":name has registered as a new user", ['name' => e($record->actor_user_name)]) ?> 5 | 6 | -------------------------------------------------------------------------------- /models/userlog/_detail_self_delete.php: -------------------------------------------------------------------------------- 1 | = __(":name deleted their account", ['name' => e($record->actor_user_name)]) ?> 2 | -------------------------------------------------------------------------------- /models/userlog/_detail_self_login.php: -------------------------------------------------------------------------------- 1 | is_two_factor): ?> 2 | = __(":name authenticated using 2FA successfully", ['name' => e($record->actor_user_name)]) ?> 3 | 4 | = __(":name authenticated successfully", ['name' => e($record->actor_user_name)]) ?> 5 | 6 | -------------------------------------------------------------------------------- /models/userlog/_detail_self_verify.php: -------------------------------------------------------------------------------- 1 | = __(":name verified their email address :user_email", [ 2 | 'name' => 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 | = __(":name changed this user's email from :old_value to :new_value", $replacements + [ 9 | 'name' => e($record->actor_admin_name), 10 | ]) ?> 11 | 12 | = __(":name changed their email from :old_value to :new_value", $replacements + [ 13 | 'name' => e($record->actor_user_name) 14 | ]) ?> 15 | 16 | -------------------------------------------------------------------------------- /models/userlog/_detail_set_password.php: -------------------------------------------------------------------------------- 1 | is_system): ?> 2 | = __(":name changed this user's password", [ 3 | 'name' => e($record->actor_admin_name), 4 | ]) ?> 5 | 6 | = __(":name changed their password", [ 7 | 'name' => e($record->actor_user_name) 8 | ]) ?> 9 | 10 | -------------------------------------------------------------------------------- /models/userlog/_detail_set_two_factor.php: -------------------------------------------------------------------------------- 1 | is_two_factor_enabled): ?> 2 | = __(":name enabled two-factor authentication", ['name' => e($record->actor_user_name)]) ?> 3 | 4 | = __(":name disabled two-factor authentication", ['name' => 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 |Hello {{ name }},
9 | 10 |This is a message to inform you that your account has been activated successfully.
11 | --------------------------------------------------------------------------------