From 92b7dd3335a6572debeacfb5faa82c63a5e67888 Mon Sep 17 00:00:00 2001 From: Marvin Borner Date: Fri, 8 Jun 2018 20:03:25 +0200 Subject: Some minor fixes --- main/app/sprinkles/account/src/Account.php | 42 +- .../account/src/Authenticate/AuthGuard.php | 110 +- .../account/src/Authenticate/Authenticator.php | 814 +++---- .../Exception/AccountDisabledException.php | 44 +- .../Exception/AccountInvalidException.php | 44 +- .../Exception/AccountNotVerifiedException.php | 44 +- .../Exception/AuthCompromisedException.php | 42 +- .../Exception/AuthExpiredException.php | 44 +- .../Exception/InvalidCredentialsException.php | 44 +- .../sprinkles/account/src/Authenticate/Hasher.php | 210 +- .../src/Authorize/AccessConditionExpression.php | 276 +-- .../src/Authorize/AuthorizationException.php | 48 +- .../account/src/Authorize/AuthorizationManager.php | 306 +-- .../src/Authorize/ParserNodeFunctionEvaluator.php | 378 +-- .../account/src/Bakery/CreateAdminUser.php | 640 ++--- .../account/src/Controller/AccountController.php | 2542 ++++++++++---------- .../Exception/SpammyRequestException.php | 42 +- .../Database/Migrations/v400/ActivitiesTable.php | 104 +- .../src/Database/Migrations/v400/GroupsTable.php | 162 +- .../Migrations/v400/PasswordResetsTable.php | 112 +- .../Migrations/v400/PermissionRolesTable.php | 108 +- .../Database/Migrations/v400/PermissionsTable.php | 520 ++-- .../Database/Migrations/v400/PersistencesTable.php | 112 +- .../Database/Migrations/v400/RoleUsersTable.php | 108 +- .../src/Database/Migrations/v400/RolesTable.php | 154 +- .../src/Database/Migrations/v400/UsersTable.php | 136 +- .../Migrations/v400/VerificationsTable.php | 112 +- .../account/src/Database/Models/Activity.php | 166 +- .../account/src/Database/Models/Group.php | 136 +- .../account/src/Database/Models/PasswordReset.php | 148 +- .../account/src/Database/Models/Permission.php | 234 +- .../sprinkles/account/src/Database/Models/Role.php | 202 +- .../sprinkles/account/src/Database/Models/User.php | 938 ++++---- .../account/src/Database/Models/Verification.php | 136 +- .../Handler/AuthCompromisedExceptionHandler.php | 68 +- .../Error/Handler/AuthExpiredExceptionHandler.php | 100 +- .../Error/Handler/ForbiddenExceptionHandler.php | 62 +- .../app/sprinkles/account/src/Facades/Password.php | 56 +- .../src/Log/UserActivityDatabaseHandler.php | 66 +- .../account/src/Log/UserActivityProcessor.php | 88 +- .../src/Repository/PasswordResetRepository.php | 68 +- .../account/src/Repository/TokenRepository.php | 446 ++-- .../src/Repository/VerificationRepository.php | 62 +- .../src/ServicesProvider/ServicesProvider.php | 888 +++---- .../account/src/Twig/AccountExtension.php | 124 +- .../account/src/Util/HashFailedException.php | 44 +- main/app/sprinkles/account/src/Util/Util.php | 78 +- 47 files changed, 5704 insertions(+), 5704 deletions(-) (limited to 'main/app/sprinkles/account/src') diff --git a/main/app/sprinkles/account/src/Account.php b/main/app/sprinkles/account/src/Account.php index 9f43166..1faccf4 100644 --- a/main/app/sprinkles/account/src/Account.php +++ b/main/app/sprinkles/account/src/Account.php @@ -1,21 +1,21 @@ -authenticator = $authenticator; - } - - /** - * Invoke the AuthGuard middleware, throwing an exception if there is no authenticated user in the session. - * - * @param \Psr\Http\Message\ServerRequestInterface $request PSR7 request - * @param \Psr\Http\Message\ResponseInterface $response PSR7 response - * @param callable $next Next middleware - * - * @return \Psr\Http\Message\ResponseInterface - */ - public function __invoke($request, $response, $next) { - if (!$this->authenticator->check()) { - throw new AuthExpiredException(); - } else { - return $next($request, $response); - } - - return $response; - } -} +authenticator = $authenticator; + } + + /** + * Invoke the AuthGuard middleware, throwing an exception if there is no authenticated user in the session. + * + * @param \Psr\Http\Message\ServerRequestInterface $request PSR7 request + * @param \Psr\Http\Message\ResponseInterface $response PSR7 response + * @param callable $next Next middleware + * + * @return \Psr\Http\Message\ResponseInterface + */ + public function __invoke($request, $response, $next) { + if (!$this->authenticator->check()) { + throw new AuthExpiredException(); + } else { + return $next($request, $response); + } + + return $response; + } +} diff --git a/main/app/sprinkles/account/src/Authenticate/Authenticator.php b/main/app/sprinkles/account/src/Authenticate/Authenticator.php index 735a688..a4586e4 100644 --- a/main/app/sprinkles/account/src/Authenticate/Authenticator.php +++ b/main/app/sprinkles/account/src/Authenticate/Authenticator.php @@ -1,407 +1,407 @@ -classMapper = $classMapper; - $this->session = $session; - $this->config = $config; - $this->cache = $cache; - - // Initialize RememberMe storage - $this->rememberMeStorage = new RememberMePDO($this->config['remember_me.table']); - - // Get the actual PDO instance from Eloquent - $pdo = Capsule::connection()->getPdo(); - - $this->rememberMeStorage->setConnection($pdo); - - // Set up RememberMe - $this->rememberMe = new RememberMe($this->rememberMeStorage); - // Set cookie name - $cookieName = $this->config['session.name'] . '-' . $this->config['remember_me.cookie.name']; - $this->rememberMe->getCookie()->setName($cookieName); - - // Change cookie path - $this->rememberMe->getCookie()->setPath($this->config['remember_me.session.path']); - - // Set expire time, if specified - if ($this->config->has('remember_me.expire_time') && ($this->config->has('remember_me.expire_time') != NULL)) { - $this->rememberMe->getCookie()->setExpireTime($this->config['remember_me.expire_time']); - } - - $this->user = NULL; - - $this->viaRemember = FALSE; - } - - /** - * Attempts to authenticate a user based on a supplied identity and password. - * - * If successful, the user's id is stored in session. - */ - public function attempt($identityColumn, $identityValue, $password, $rememberMe = FALSE) { - // Try to load the user, using the specified conditions - $user = $this->classMapper->staticMethod('user', 'where', $identityColumn, $identityValue)->first(); - - if (!$user) { - throw new InvalidCredentialsException(); - } - - // Check that the user has a password set (so, rule out newly created accounts without a password) - if (!$user->password) { - throw new InvalidCredentialsException(); - } - - // Check that the user's account is enabled - if ($user->flag_enabled == 0) { - throw new AccountDisabledException(); - } - - // Check that the user's account is verified (if verification is required) - if ($this->config['site.registration.require_email_verification'] && $user->flag_verified == 0) { - throw new AccountNotVerifiedException(); - } - - // Here is my password. May I please assume the identify of this user now? - if (Password::verify($password, $user->password)) { - $this->login($user, $rememberMe); - return $user; - } else { - // We know the password is at fault here (as opposed to the identity), but lets not give away the combination in case of someone bruteforcing - throw new InvalidCredentialsException(); - } - } - - /** - * Determine if the current user is authenticated. - * - * @return bool - */ - public function check() { - return !is_null($this->user()); - } - - /** - * Determine if the current user is a guest (unauthenticated). - * - * @return bool - */ - public function guest() { - return !$this->check(); - } - - /** - * Process an account login request. - * - * This method logs in the specified user, allowing the client to assume the user's identity for the duration of the session. - * @param User $user The user to log in. - * @param bool $rememberMe Set to true to make this a "persistent session", i.e. one that will re-login even after the session expires. - * @odo Figure out a way to update the currentUser service to reflect the logged-in user *immediately* in the service provider. - * As it stands, the currentUser service will still reflect a "guest user" for the remainder of the request. - */ - public function login($user, $rememberMe = FALSE) { - $oldId = session_id(); - $this->session->regenerateId(TRUE); - - // Since regenerateId deletes the old session, we'll do the same in cache - $this->flushSessionCache($oldId); - - // If the user wants to be remembered, create Rememberme cookie - if ($rememberMe) { - $this->rememberMe->createCookie($user->id); - } else { - $this->rememberMe->clearCookie(); - } - - // Assume identity - $key = $this->config['session.keys.current_user_id']; - $this->session[$key] = $user->id; - - // Set auth mode - $this->viaRemember = FALSE; - - // User login actions - $user->onLogin(); - } - - /** - * Processes an account logout request. - * - * Logs the currently authenticated user out, destroying the PHP session and clearing the persistent session. - * This can optionally remove persistent sessions across all browsers/devices, since there can be a "RememberMe" cookie - * and corresponding database entries in multiple browsers/devices. See http://jaspan.com/improved_persistent_login_cookie_best_practice. - * - * @param bool $complete If set to true, will ensure that the user is logged out from *all* browsers on all devices. - */ - public function logout($complete = FALSE) { - $currentUserId = $this->session->get($this->config['session.keys.current_user_id']); - - // This removes all of the user's persistent logins from the database - if ($complete) { - $this->storage->cleanAllTriplets($currentUserId); - } - - // Clear the rememberMe cookie - $this->rememberMe->clearCookie(); - - // User logout actions - if ($currentUserId) { - $currentUser = $this->classMapper->staticMethod('user', 'find', $currentUserId); - if ($currentUser) { - $currentUser->onLogout(); - } - } - - $this->user = NULL; - $this->loggedOut = TRUE; - - $oldId = session_id(); - - // Completely destroy the session - $this->session->destroy(); - - // Since regenerateId deletes the old session, we'll do the same in cache - $this->flushSessionCache($oldId); - - // Restart the session service - $this->session->start(); - } - - /** - * Try to get the currently authenticated user, returning a guest user if none was found. - * - * Tries to re-establish a session for "remember-me" users who have been logged out due to an expired session. - * @return User|null - * @throws AuthExpiredException - * @throws AuthCompromisedException - * @throws AccountInvalidException - * @throws AccountDisabledException - */ - public function user() { - $user = NULL; - - if (!$this->loggedOut) { - - // Return any cached user - if (!is_null($this->user)) { - return $this->user; - } - - // If this throws a PDOException we catch it and return null than allowing the exception to propagate. - // This is because the error handler relies on Twig, which relies on a Twig Extension, which relies on the global current_user variable. - // So, we really don't want this method to throw any database exceptions. - try { - // Now, check to see if we have a user in session - $user = $this->loginSessionUser(); - - // If no user was found in the session, try to login via RememberMe cookie - if (!$user) { - $user = $this->loginRememberedUser(); - } - } catch (\PDOException $e) { - $user = NULL; - } - } - - return $this->user = $user; - } - - /** - * Determine whether the current user was authenticated using a remember me cookie. - * - * This function is useful when users are performing sensitive operations, and you may want to force them to re-authenticate. - * @return bool - */ - public function viaRemember() { - return $this->viaRemember; - } - - /** - * Attempt to log in the client from their rememberMe token (in their cookie). - * - * @return User|bool If successful, the User object of the remembered user. Otherwise, return false. - * @throws AuthCompromisedException The client attempted to log in with an invalid rememberMe token. - */ - protected function loginRememberedUser() { - /** @var \Birke\Rememberme\LoginResult $loginResult */ - $loginResult = $this->rememberMe->login(); - - if ($loginResult->isSuccess()) { - // Update in session - $this->session[$this->config['session.keys.current_user_id']] = $loginResult->getCredential(); - // There is a chance that an attacker has stolen the login token, - // so we store the fact that the user was logged in via RememberMe (instead of login form) - $this->viaRemember = TRUE; - } else { - // If $rememberMe->login() was not successful, check if the token was invalid as well. This means the cookie was stolen. - if ($loginResult->hasPossibleManipulation()) { - throw new AuthCompromisedException(); - } - } - - return $this->validateUserAccount($loginResult->getCredential()); - } - - /** - * Attempt to log in the client from the session. - * - * @return User|null If successful, the User object of the user in session. Otherwise, return null. - * @throws AuthExpiredException The client attempted to use an expired rememberMe token. - */ - protected function loginSessionUser() { - $userId = $this->session->get($this->config['session.keys.current_user_id']); - - // If a user_id was found in the session, check any rememberMe cookie that was submitted. - // If they submitted an expired rememberMe cookie, then we need to log them out. - if ($userId) { - if (!$this->validateRememberMeCookie()) { - $this->logout(); - throw new AuthExpiredException(); - } - } - - return $this->validateUserAccount($userId); - } - - /** - * Determine if the cookie contains a valid rememberMe token. - * - * @return bool - */ - protected function validateRememberMeCookie() { - $cookieValue = $this->rememberMe->getCookie()->getValue(); - if (!$cookieValue) { - return TRUE; - } - $triplet = RememberMeTriplet::fromString($cookieValue); - if (!$triplet->isValid()) { - return FALSE; - } - - return TRUE; - } - - /** - * Tries to load the specified user by id from the database. - * - * Checks that the account is valid and enabled, throwing an exception if not. - * @param int $userId - * @return User|null - * @throws AccountInvalidException - * @throws AccountDisabledException - */ - protected function validateUserAccount($userId) { - if ($userId) { - $user = $this->classMapper->staticMethod('user', 'find', $userId); - - // If the user doesn't exist any more, throw an exception. - if (!$user) { - throw new AccountInvalidException(); - } - - // If the user has been disabled since their last request, throw an exception. - if (!$user->flag_enabled) { - throw new AccountDisabledException(); - } - - return $user; - } else { - return NULL; - } - } - - /** - * Flush the cache associated with a session id - * - * @param string $id The session id - * @return bool - */ - public function flushSessionCache($id) { - return $this->cache->tags('_s' . $id)->flush(); - } -} +classMapper = $classMapper; + $this->session = $session; + $this->config = $config; + $this->cache = $cache; + + // Initialize RememberMe storage + $this->rememberMeStorage = new RememberMePDO($this->config['remember_me.table']); + + // Get the actual PDO instance from Eloquent + $pdo = Capsule::connection()->getPdo(); + + $this->rememberMeStorage->setConnection($pdo); + + // Set up RememberMe + $this->rememberMe = new RememberMe($this->rememberMeStorage); + // Set cookie name + $cookieName = $this->config['session.name'] . '-' . $this->config['remember_me.cookie.name']; + $this->rememberMe->getCookie()->setName($cookieName); + + // Change cookie path + $this->rememberMe->getCookie()->setPath($this->config['remember_me.session.path']); + + // Set expire time, if specified + if ($this->config->has('remember_me.expire_time') && ($this->config->has('remember_me.expire_time') != NULL)) { + $this->rememberMe->getCookie()->setExpireTime($this->config['remember_me.expire_time']); + } + + $this->user = NULL; + + $this->viaRemember = FALSE; + } + + /** + * Attempts to authenticate a user based on a supplied identity and password. + * + * If successful, the user's id is stored in session. + */ + public function attempt($identityColumn, $identityValue, $password, $rememberMe = FALSE) { + // Try to load the user, using the specified conditions + $user = $this->classMapper->staticMethod('user', 'where', $identityColumn, $identityValue)->first(); + + if (!$user) { + throw new InvalidCredentialsException(); + } + + // Check that the user has a password set (so, rule out newly created accounts without a password) + if (!$user->password) { + throw new InvalidCredentialsException(); + } + + // Check that the user's account is enabled + if ($user->flag_enabled == 0) { + throw new AccountDisabledException(); + } + + // Check that the user's account is verified (if verification is required) + if ($this->config['site.registration.require_email_verification'] && $user->flag_verified == 0) { + throw new AccountNotVerifiedException(); + } + + // Here is my password. May I please assume the identify of this user now? + if (Password::verify($password, $user->password)) { + $this->login($user, $rememberMe); + return $user; + } else { + // We know the password is at fault here (as opposed to the identity), but lets not give away the combination in case of someone bruteforcing + throw new InvalidCredentialsException(); + } + } + + /** + * Determine if the current user is authenticated. + * + * @return bool + */ + public function check() { + return !is_null($this->user()); + } + + /** + * Determine if the current user is a guest (unauthenticated). + * + * @return bool + */ + public function guest() { + return !$this->check(); + } + + /** + * Process an account login request. + * + * This method logs in the specified user, allowing the client to assume the user's identity for the duration of the session. + * @param User $user The user to log in. + * @param bool $rememberMe Set to true to make this a "persistent session", i.e. one that will re-login even after the session expires. + * @odo Figure out a way to update the currentUser service to reflect the logged-in user *immediately* in the service provider. + * As it stands, the currentUser service will still reflect a "guest user" for the remainder of the request. + */ + public function login($user, $rememberMe = FALSE) { + $oldId = session_id(); + $this->session->regenerateId(TRUE); + + // Since regenerateId deletes the old session, we'll do the same in cache + $this->flushSessionCache($oldId); + + // If the user wants to be remembered, create Rememberme cookie + if ($rememberMe) { + $this->rememberMe->createCookie($user->id); + } else { + $this->rememberMe->clearCookie(); + } + + // Assume identity + $key = $this->config['session.keys.current_user_id']; + $this->session[$key] = $user->id; + + // Set auth mode + $this->viaRemember = FALSE; + + // User login actions + $user->onLogin(); + } + + /** + * Processes an account logout request. + * + * Logs the currently authenticated user out, destroying the PHP session and clearing the persistent session. + * This can optionally remove persistent sessions across all browsers/devices, since there can be a "RememberMe" cookie + * and corresponding database entries in multiple browsers/devices. See http://jaspan.com/improved_persistent_login_cookie_best_practice. + * + * @param bool $complete If set to true, will ensure that the user is logged out from *all* browsers on all devices. + */ + public function logout($complete = FALSE) { + $currentUserId = $this->session->get($this->config['session.keys.current_user_id']); + + // This removes all of the user's persistent logins from the database + if ($complete) { + $this->storage->cleanAllTriplets($currentUserId); + } + + // Clear the rememberMe cookie + $this->rememberMe->clearCookie(); + + // User logout actions + if ($currentUserId) { + $currentUser = $this->classMapper->staticMethod('user', 'find', $currentUserId); + if ($currentUser) { + $currentUser->onLogout(); + } + } + + $this->user = NULL; + $this->loggedOut = TRUE; + + $oldId = session_id(); + + // Completely destroy the session + $this->session->destroy(); + + // Since regenerateId deletes the old session, we'll do the same in cache + $this->flushSessionCache($oldId); + + // Restart the session service + $this->session->start(); + } + + /** + * Try to get the currently authenticated user, returning a guest user if none was found. + * + * Tries to re-establish a session for "remember-me" users who have been logged out due to an expired session. + * @return User|null + * @throws AuthExpiredException + * @throws AuthCompromisedException + * @throws AccountInvalidException + * @throws AccountDisabledException + */ + public function user() { + $user = NULL; + + if (!$this->loggedOut) { + + // Return any cached user + if (!is_null($this->user)) { + return $this->user; + } + + // If this throws a PDOException we catch it and return null than allowing the exception to propagate. + // This is because the error handler relies on Twig, which relies on a Twig Extension, which relies on the global current_user variable. + // So, we really don't want this method to throw any database exceptions. + try { + // Now, check to see if we have a user in session + $user = $this->loginSessionUser(); + + // If no user was found in the session, try to login via RememberMe cookie + if (!$user) { + $user = $this->loginRememberedUser(); + } + } catch (\PDOException $e) { + $user = NULL; + } + } + + return $this->user = $user; + } + + /** + * Determine whether the current user was authenticated using a remember me cookie. + * + * This function is useful when users are performing sensitive operations, and you may want to force them to re-authenticate. + * @return bool + */ + public function viaRemember() { + return $this->viaRemember; + } + + /** + * Attempt to log in the client from their rememberMe token (in their cookie). + * + * @return User|bool If successful, the User object of the remembered user. Otherwise, return false. + * @throws AuthCompromisedException The client attempted to log in with an invalid rememberMe token. + */ + protected function loginRememberedUser() { + /** @var \Birke\Rememberme\LoginResult $loginResult */ + $loginResult = $this->rememberMe->login(); + + if ($loginResult->isSuccess()) { + // Update in session + $this->session[$this->config['session.keys.current_user_id']] = $loginResult->getCredential(); + // There is a chance that an attacker has stolen the login token, + // so we store the fact that the user was logged in via RememberMe (instead of login form) + $this->viaRemember = TRUE; + } else { + // If $rememberMe->login() was not successful, check if the token was invalid as well. This means the cookie was stolen. + if ($loginResult->hasPossibleManipulation()) { + throw new AuthCompromisedException(); + } + } + + return $this->validateUserAccount($loginResult->getCredential()); + } + + /** + * Attempt to log in the client from the session. + * + * @return User|null If successful, the User object of the user in session. Otherwise, return null. + * @throws AuthExpiredException The client attempted to use an expired rememberMe token. + */ + protected function loginSessionUser() { + $userId = $this->session->get($this->config['session.keys.current_user_id']); + + // If a user_id was found in the session, check any rememberMe cookie that was submitted. + // If they submitted an expired rememberMe cookie, then we need to log them out. + if ($userId) { + if (!$this->validateRememberMeCookie()) { + $this->logout(); + throw new AuthExpiredException(); + } + } + + return $this->validateUserAccount($userId); + } + + /** + * Determine if the cookie contains a valid rememberMe token. + * + * @return bool + */ + protected function validateRememberMeCookie() { + $cookieValue = $this->rememberMe->getCookie()->getValue(); + if (!$cookieValue) { + return TRUE; + } + $triplet = RememberMeTriplet::fromString($cookieValue); + if (!$triplet->isValid()) { + return FALSE; + } + + return TRUE; + } + + /** + * Tries to load the specified user by id from the database. + * + * Checks that the account is valid and enabled, throwing an exception if not. + * @param int $userId + * @return User|null + * @throws AccountInvalidException + * @throws AccountDisabledException + */ + protected function validateUserAccount($userId) { + if ($userId) { + $user = $this->classMapper->staticMethod('user', 'find', $userId); + + // If the user doesn't exist any more, throw an exception. + if (!$user) { + throw new AccountInvalidException(); + } + + // If the user has been disabled since their last request, throw an exception. + if (!$user->flag_enabled) { + throw new AccountDisabledException(); + } + + return $user; + } else { + return NULL; + } + } + + /** + * Flush the cache associated with a session id + * + * @param string $id The session id + * @return bool + */ + public function flushSessionCache($id) { + return $this->cache->tags('_s' . $id)->flush(); + } +} diff --git a/main/app/sprinkles/account/src/Authenticate/Exception/AccountDisabledException.php b/main/app/sprinkles/account/src/Authenticate/Exception/AccountDisabledException.php index 3ad4c59..314fcc3 100644 --- a/main/app/sprinkles/account/src/Authenticate/Exception/AccountDisabledException.php +++ b/main/app/sprinkles/account/src/Authenticate/Exception/AccountDisabledException.php @@ -1,22 +1,22 @@ - $this->cost($options), - ]); - - if (!$hash) { - throw new HashFailedException(); - } - - return $hash; - } - - /** - * Verify a plaintext password against the user's hashed password. - * - * @param string $password The plaintext password to verify. - * @param string $hash The hash to compare against. - * @param array $options - * @return boolean True if the password matches, false otherwise. - */ - public function verify($password, $hash, array $options = []) { - $hashType = $this->getHashType($hash); - - if ($hashType == 'sha1') { - // Legacy UserCake passwords - $salt = substr($hash, 0, 25); // Extract the salt from the hash - $inputHash = $salt . sha1($salt . $password); - - return (hash_equals($inputHash, $hash) === TRUE); - - } else if ($hashType == 'legacy') { - // Homegrown implementation (assuming that current install has been using a cost parameter of 12) - // Used for manual implementation of bcrypt. - // Note that this legacy hashing put the salt at the _end_ for some reason. - $salt = substr($hash, 60); - $inputHash = crypt($password, '$2y$12$' . $salt); - $correctHash = substr($hash, 0, 60); - - return (hash_equals($inputHash, $correctHash) === TRUE); - } - - // Modern implementation - return password_verify($password, $hash); - } - - /** - * Extract the cost value from the options array. - * - * @param array $options - * @return int - */ - protected function cost(array $options = []) { - return isset($options['rounds']) ? $options['rounds'] : $this->defaultRounds; - } -} + $this->cost($options), + ]); + + if (!$hash) { + throw new HashFailedException(); + } + + return $hash; + } + + /** + * Verify a plaintext password against the user's hashed password. + * + * @param string $password The plaintext password to verify. + * @param string $hash The hash to compare against. + * @param array $options + * @return boolean True if the password matches, false otherwise. + */ + public function verify($password, $hash, array $options = []) { + $hashType = $this->getHashType($hash); + + if ($hashType == 'sha1') { + // Legacy UserCake passwords + $salt = substr($hash, 0, 25); // Extract the salt from the hash + $inputHash = $salt . sha1($salt . $password); + + return (hash_equals($inputHash, $hash) === TRUE); + + } else if ($hashType == 'legacy') { + // Homegrown implementation (assuming that current install has been using a cost parameter of 12) + // Used for manual implementation of bcrypt. + // Note that this legacy hashing put the salt at the _end_ for some reason. + $salt = substr($hash, 60); + $inputHash = crypt($password, '$2y$12$' . $salt); + $correctHash = substr($hash, 0, 60); + + return (hash_equals($inputHash, $correctHash) === TRUE); + } + + // Modern implementation + return password_verify($password, $hash); + } + + /** + * Extract the cost value from the options array. + * + * @param array $options + * @return int + */ + protected function cost(array $options = []) { + return isset($options['rounds']) ? $options['rounds'] : $this->defaultRounds; + } +} diff --git a/main/app/sprinkles/account/src/Authorize/AccessConditionExpression.php b/main/app/sprinkles/account/src/Authorize/AccessConditionExpression.php index e36f4f4..8a8225e 100644 --- a/main/app/sprinkles/account/src/Authorize/AccessConditionExpression.php +++ b/main/app/sprinkles/account/src/Authorize/AccessConditionExpression.php @@ -1,138 +1,138 @@ -nodeVisitor = $nodeVisitor; - $this->user = $user; - $this->parser = new Parser(new EmulativeLexer); - $this->traverser = new NodeTraverser; - $this->traverser->addVisitor($nodeVisitor); - $this->prettyPrinter = new StandardPrettyPrinter; - $this->logger = $logger; - $this->debug = $debug; - } - - /** - * Evaluates a condition expression, based on the given parameters. - * - * The special parameter `self` is an array of the current user's data. - * This get included automatically, and so does not need to be passed in. - * @param string $condition a boolean expression composed of calls to AccessCondition functions. - * @param array[mixed] $params the parameters to be used when evaluating the expression. - * @return bool true if the condition is passed for the given parameters, otherwise returns false. - */ - public function evaluateCondition($condition, $params) { - // Set the reserved `self` parameters. - // This replaces any values of `self` specified in the arguments, thus preventing them from being overridden in malicious user input. - // (For example, from an unfiltered request body). - $params['self'] = $this->user->export(); - - $this->nodeVisitor->setParams($params); - - $code = "debug) { - $this->logger->debug("Evaluating access condition '$condition' with parameters:", $params); - } - - // Traverse the parse tree, and execute any callbacks found using the supplied parameters. - // Replace the function node with the return value of the callback. - try { - // parse - $stmts = $this->parser->parse($code); - - // traverse - $stmts = $this->traverser->traverse($stmts); - - // Evaluate boolean statement. It is safe to use eval() here, because our expression has been reduced entirely to a boolean expression. - $expr = $this->prettyPrinter->prettyPrintExpr($stmts[0]); - $expr_eval = "return " . $expr . ";\n"; - $result = eval($expr_eval); - - if ($this->debug) { - $this->logger->debug("Expression '$expr' evaluates to " . ($result == TRUE ? "true" : "false")); - } - - return $result; - } catch (PhpParserException $e) { - if ($this->debug) { - $this->logger->debug("Error parsing access condition '$condition':" . $e->getMessage()); - } - return FALSE; // Access fails if the access condition can't be parsed. - } catch (AuthorizationException $e) { - if ($this->debug) { - $this->logger->debug("Error parsing access condition '$condition':" . $e->getMessage()); - } - return FALSE; - } - } -} +nodeVisitor = $nodeVisitor; + $this->user = $user; + $this->parser = new Parser(new EmulativeLexer); + $this->traverser = new NodeTraverser; + $this->traverser->addVisitor($nodeVisitor); + $this->prettyPrinter = new StandardPrettyPrinter; + $this->logger = $logger; + $this->debug = $debug; + } + + /** + * Evaluates a condition expression, based on the given parameters. + * + * The special parameter `self` is an array of the current user's data. + * This get included automatically, and so does not need to be passed in. + * @param string $condition a boolean expression composed of calls to AccessCondition functions. + * @param array[mixed] $params the parameters to be used when evaluating the expression. + * @return bool true if the condition is passed for the given parameters, otherwise returns false. + */ + public function evaluateCondition($condition, $params) { + // Set the reserved `self` parameters. + // This replaces any values of `self` specified in the arguments, thus preventing them from being overridden in malicious user input. + // (For example, from an unfiltered request body). + $params['self'] = $this->user->export(); + + $this->nodeVisitor->setParams($params); + + $code = "debug) { + $this->logger->debug("Evaluating access condition '$condition' with parameters:", $params); + } + + // Traverse the parse tree, and execute any callbacks found using the supplied parameters. + // Replace the function node with the return value of the callback. + try { + // parse + $stmts = $this->parser->parse($code); + + // traverse + $stmts = $this->traverser->traverse($stmts); + + // Evaluate boolean statement. It is safe to use eval() here, because our expression has been reduced entirely to a boolean expression. + $expr = $this->prettyPrinter->prettyPrintExpr($stmts[0]); + $expr_eval = "return " . $expr . ";\n"; + $result = eval($expr_eval); + + if ($this->debug) { + $this->logger->debug("Expression '$expr' evaluates to " . ($result == TRUE ? "true" : "false")); + } + + return $result; + } catch (PhpParserException $e) { + if ($this->debug) { + $this->logger->debug("Error parsing access condition '$condition':" . $e->getMessage()); + } + return FALSE; // Access fails if the access condition can't be parsed. + } catch (AuthorizationException $e) { + if ($this->debug) { + $this->logger->debug("Error parsing access condition '$condition':" . $e->getMessage()); + } + return FALSE; + } + } +} diff --git a/main/app/sprinkles/account/src/Authorize/AuthorizationException.php b/main/app/sprinkles/account/src/Authorize/AuthorizationException.php index 33f3d35..f93e847 100644 --- a/main/app/sprinkles/account/src/Authorize/AuthorizationException.php +++ b/main/app/sprinkles/account/src/Authorize/AuthorizationException.php @@ -1,24 +1,24 @@ -ci = $ci; - $this->callbacks = $callbacks; - } - - /** - * Register an authorization callback, which can then be used in permission conditions. - * - * To add additional callbacks, simply extend the `authorizer` service in your Sprinkle's service provider. - * @param string $name - * @param callable $callback - */ - public function addCallback($name, $callback) { - $this->callbacks[$name] = $callback; - return $this; - } - - /** - * Get all authorization callbacks. - * - * @return callable[] - */ - public function getCallbacks() { - return $this->callbacks; - } - - /** - * Checks whether or not a user has access on a particular permission slug. - * - * Determine if this user has access to the given $slug under the given $params. - * - * @param UserFrosting\Sprinkle\Account\Database\Models\User $user - * @param string $slug The permission slug to check for access. - * @param array $params [optional] An array of field names => values, specifying any additional data to provide the authorization module - * when determining whether or not this user has access. - * @return boolean True if the user has access, false otherwise. - */ - public function checkAccess(User $user, $slug, array $params = []) { - $debug = $this->ci->config['debug.auth']; - - if ($debug) { - $trace = array_slice(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3), 1); - $this->ci->authLogger->debug("Authorization check requested at: ", $trace); - $this->ci->authLogger->debug("Checking authorization for user {$user->id} ('{$user->user_name}') on permission '$slug'..."); - } - - if ($this->ci->authenticator->guest()) { - if ($debug) { - $this->ci->authLogger->debug("User is not logged in. Access denied."); - } - return FALSE; - } - - // The master (root) account has access to everything. - // Need to use loose comparison for now, because some DBs return `id` as a string. - - if ($user->id == $this->ci->config['reserved_user_ids.master']) { - if ($debug) { - $this->ci->authLogger->debug("User is the master (root) user. Access granted."); - } - return TRUE; - } - - // Find all permissions that apply to this user (via roles), and check if any evaluate to true. - $permissions = $user->getCachedPermissions(); - - if (empty($permissions) || !isset($permissions[$slug])) { - if ($debug) { - $this->ci->authLogger->debug("No matching permissions found. Access denied."); - } - return FALSE; - } - - $permissions = $permissions[$slug]; - - if ($debug) { - $this->ci->authLogger->debug("Found matching permissions: \n" . print_r($this->getPermissionsArrayDebugInfo($permissions), TRUE)); - } - - $nodeVisitor = new ParserNodeFunctionEvaluator($this->callbacks, $this->ci->authLogger, $debug); - $ace = new AccessConditionExpression($nodeVisitor, $user, $this->ci->authLogger, $debug); - - foreach ($permissions as $permission) { - $pass = $ace->evaluateCondition($permission->conditions, $params); - if ($pass) { - if ($debug) { - $this->ci->authLogger->debug("User passed conditions '{$permission->conditions}' . Access granted."); - } - return TRUE; - } - } - - if ($debug) { - $this->ci->authLogger->debug("User failed to pass any of the matched permissions. Access denied."); - } - - return FALSE; - } - - /** - * Remove extraneous information from the permission to reduce verbosity. - * - * @param array - * @return array - */ - protected function getPermissionsArrayDebugInfo($permissions) { - $permissionsInfo = []; - foreach ($permissions as $permission) { - $permissionData = array_only($permission->toArray(), ['id', 'slug', 'name', 'conditions', 'description']); - // Remove this until we can find an efficient way to only load these once during debugging - //$permissionData['roles_via'] = $permission->roles_via->pluck('id')->all(); - $permissionsInfo[] = $permissionData; - } - - return $permissionsInfo; - } -} +ci = $ci; + $this->callbacks = $callbacks; + } + + /** + * Register an authorization callback, which can then be used in permission conditions. + * + * To add additional callbacks, simply extend the `authorizer` service in your Sprinkle's service provider. + * @param string $name + * @param callable $callback + */ + public function addCallback($name, $callback) { + $this->callbacks[$name] = $callback; + return $this; + } + + /** + * Get all authorization callbacks. + * + * @return callable[] + */ + public function getCallbacks() { + return $this->callbacks; + } + + /** + * Checks whether or not a user has access on a particular permission slug. + * + * Determine if this user has access to the given $slug under the given $params. + * + * @param UserFrosting\Sprinkle\Account\Database\Models\User $user + * @param string $slug The permission slug to check for access. + * @param array $params [optional] An array of field names => values, specifying any additional data to provide the authorization module + * when determining whether or not this user has access. + * @return boolean True if the user has access, false otherwise. + */ + public function checkAccess(User $user, $slug, array $params = []) { + $debug = $this->ci->config['debug.auth']; + + if ($debug) { + $trace = array_slice(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3), 1); + $this->ci->authLogger->debug("Authorization check requested at: ", $trace); + $this->ci->authLogger->debug("Checking authorization for user {$user->id} ('{$user->user_name}') on permission '$slug'..."); + } + + if ($this->ci->authenticator->guest()) { + if ($debug) { + $this->ci->authLogger->debug("User is not logged in. Access denied."); + } + return FALSE; + } + + // The master (root) account has access to everything. + // Need to use loose comparison for now, because some DBs return `id` as a string. + + if ($user->id == $this->ci->config['reserved_user_ids.master']) { + if ($debug) { + $this->ci->authLogger->debug("User is the master (root) user. Access granted."); + } + return TRUE; + } + + // Find all permissions that apply to this user (via roles), and check if any evaluate to true. + $permissions = $user->getCachedPermissions(); + + if (empty($permissions) || !isset($permissions[$slug])) { + if ($debug) { + $this->ci->authLogger->debug("No matching permissions found. Access denied."); + } + return FALSE; + } + + $permissions = $permissions[$slug]; + + if ($debug) { + $this->ci->authLogger->debug("Found matching permissions: \n" . print_r($this->getPermissionsArrayDebugInfo($permissions), TRUE)); + } + + $nodeVisitor = new ParserNodeFunctionEvaluator($this->callbacks, $this->ci->authLogger, $debug); + $ace = new AccessConditionExpression($nodeVisitor, $user, $this->ci->authLogger, $debug); + + foreach ($permissions as $permission) { + $pass = $ace->evaluateCondition($permission->conditions, $params); + if ($pass) { + if ($debug) { + $this->ci->authLogger->debug("User passed conditions '{$permission->conditions}' . Access granted."); + } + return TRUE; + } + } + + if ($debug) { + $this->ci->authLogger->debug("User failed to pass any of the matched permissions. Access denied."); + } + + return FALSE; + } + + /** + * Remove extraneous information from the permission to reduce verbosity. + * + * @param array + * @return array + */ + protected function getPermissionsArrayDebugInfo($permissions) { + $permissionsInfo = []; + foreach ($permissions as $permission) { + $permissionData = array_only($permission->toArray(), ['id', 'slug', 'name', 'conditions', 'description']); + // Remove this until we can find an efficient way to only load these once during debugging + //$permissionData['roles_via'] = $permission->roles_via->pluck('id')->all(); + $permissionsInfo[] = $permissionData; + } + + return $permissionsInfo; + } +} diff --git a/main/app/sprinkles/account/src/Authorize/ParserNodeFunctionEvaluator.php b/main/app/sprinkles/account/src/Authorize/ParserNodeFunctionEvaluator.php index e0db07d..af26d9a 100644 --- a/main/app/sprinkles/account/src/Authorize/ParserNodeFunctionEvaluator.php +++ b/main/app/sprinkles/account/src/Authorize/ParserNodeFunctionEvaluator.php @@ -1,189 +1,189 @@ -callbacks = $callbacks; - $this->prettyPrinter = new StandardPrettyPrinter; - $this->logger = $logger; - $this->debug = $debug; - $this->params = []; - } - - public function leaveNode(Node $node) { - // Look for function calls - if ($node instanceof \PhpParser\Node\Expr\FuncCall) { - $eval = new \PhpParser\Node\Scalar\LNumber; - - // Get the method name - $callbackName = $node->name->toString(); - // Get the method arguments - $argNodes = $node->args; - - $args = []; - $argsInfo = []; - foreach ($argNodes as $arg) { - $argString = $this->prettyPrinter->prettyPrintExpr($arg->value); - - // Debugger info - $currentArgInfo = [ - 'expression' => $argString - ]; - // Resolve parameter placeholders ('variable' names (either single-word or array-dot identifiers)) - if (($arg->value instanceof \PhpParser\Node\Expr\BinaryOp\Concat) || ($arg->value instanceof \PhpParser\Node\Expr\ConstFetch)) { - $value = $this->resolveParamPath($argString); - $currentArgInfo['type'] = "parameter"; - $currentArgInfo['resolved_value'] = $value; - // Resolve arrays - } else if ($arg->value instanceof \PhpParser\Node\Expr\Array_) { - $value = $this->resolveArray($arg); - $currentArgInfo['type'] = "array"; - $currentArgInfo['resolved_value'] = print_r($value, TRUE); - // Resolve strings - } else if ($arg->value instanceof \PhpParser\Node\Scalar\String_) { - $value = $arg->value->value; - $currentArgInfo['type'] = "string"; - $currentArgInfo['resolved_value'] = $value; - // Resolve numbers - } else if ($arg->value instanceof \PhpParser\Node\Scalar\DNumber) { - $value = $arg->value->value; - $currentArgInfo['type'] = "float"; - $currentArgInfo['resolved_value'] = $value; - } else if ($arg->value instanceof \PhpParser\Node\Scalar\LNumber) { - $value = $arg->value->value; - $currentArgInfo['type'] = "integer"; - $currentArgInfo['resolved_value'] = $value; - // Anything else is simply interpreted as its literal string value - } else { - $value = $argString; - $currentArgInfo['type'] = "unknown"; - $currentArgInfo['resolved_value'] = $value; - } - - $args[] = $value; - $argsInfo[] = $currentArgInfo; - } - - if ($this->debug) { - if (count($args)) { - $this->logger->debug("Evaluating callback '$callbackName' on: ", $argsInfo); - } else { - $this->logger->debug("Evaluating callback '$callbackName'..."); - } - } - - // Call the specified access condition callback with the specified arguments. - if (isset($this->callbacks[$callbackName]) && is_callable($this->callbacks[$callbackName])) { - $result = call_user_func_array($this->callbacks[$callbackName], $args); - } else { - throw new AuthorizationException("Authorization failed: Access condition method '$callbackName' does not exist."); - } - - if ($this->debug) { - $this->logger->debug("Result: " . ($result ? "1" : "0")); - } - - return new \PhpParser\Node\Scalar\LNumber($result ? "1" : "0"); - } - } - - public function setParams($params) { - $this->params = $params; - } - - /** - * Resolve an array expression in a condition expression into an actual array. - * - * @param string $arg the array, represented as a string. - * @return array[mixed] the array, as a plain ol' PHP array. - */ - private function resolveArray($arg) { - $arr = []; - $items = (array)$arg->value->items; - foreach ($items as $item) { - if ($item->key) { - $arr[$item->key] = $item->value->value; - } else { - $arr[] = $item->value->value; - } - } - return $arr; - } - - /** - * Resolve a parameter path (e.g. "user.id", "post", etc) into its value. - * - * @param string $path the name of the parameter to resolve, based on the parameters set in this object. - * @throws Exception the path could not be resolved. Path is malformed or key does not exist. - * @return mixed the value of the specified parameter. - */ - private function resolveParamPath($path) { - $pathTokens = explode(".", $path); - $value = $this->params; - foreach ($pathTokens as $token) { - $token = trim($token); - if (is_array($value) && isset($value[$token])) { - $value = $value[$token]; - continue; - } else if (is_object($value) && isset($value->$token)) { - $value = $value->$token; - continue; - } else { - throw new AuthorizationException("Cannot resolve the path \"$path\". Error at token \"$token\"."); - } - } - return $value; - } -} +callbacks = $callbacks; + $this->prettyPrinter = new StandardPrettyPrinter; + $this->logger = $logger; + $this->debug = $debug; + $this->params = []; + } + + public function leaveNode(Node $node) { + // Look for function calls + if ($node instanceof \PhpParser\Node\Expr\FuncCall) { + $eval = new \PhpParser\Node\Scalar\LNumber; + + // Get the method name + $callbackName = $node->name->toString(); + // Get the method arguments + $argNodes = $node->args; + + $args = []; + $argsInfo = []; + foreach ($argNodes as $arg) { + $argString = $this->prettyPrinter->prettyPrintExpr($arg->value); + + // Debugger info + $currentArgInfo = [ + 'expression' => $argString + ]; + // Resolve parameter placeholders ('variable' names (either single-word or array-dot identifiers)) + if (($arg->value instanceof \PhpParser\Node\Expr\BinaryOp\Concat) || ($arg->value instanceof \PhpParser\Node\Expr\ConstFetch)) { + $value = $this->resolveParamPath($argString); + $currentArgInfo['type'] = "parameter"; + $currentArgInfo['resolved_value'] = $value; + // Resolve arrays + } else if ($arg->value instanceof \PhpParser\Node\Expr\Array_) { + $value = $this->resolveArray($arg); + $currentArgInfo['type'] = "array"; + $currentArgInfo['resolved_value'] = print_r($value, TRUE); + // Resolve strings + } else if ($arg->value instanceof \PhpParser\Node\Scalar\String_) { + $value = $arg->value->value; + $currentArgInfo['type'] = "string"; + $currentArgInfo['resolved_value'] = $value; + // Resolve numbers + } else if ($arg->value instanceof \PhpParser\Node\Scalar\DNumber) { + $value = $arg->value->value; + $currentArgInfo['type'] = "float"; + $currentArgInfo['resolved_value'] = $value; + } else if ($arg->value instanceof \PhpParser\Node\Scalar\LNumber) { + $value = $arg->value->value; + $currentArgInfo['type'] = "integer"; + $currentArgInfo['resolved_value'] = $value; + // Anything else is simply interpreted as its literal string value + } else { + $value = $argString; + $currentArgInfo['type'] = "unknown"; + $currentArgInfo['resolved_value'] = $value; + } + + $args[] = $value; + $argsInfo[] = $currentArgInfo; + } + + if ($this->debug) { + if (count($args)) { + $this->logger->debug("Evaluating callback '$callbackName' on: ", $argsInfo); + } else { + $this->logger->debug("Evaluating callback '$callbackName'..."); + } + } + + // Call the specified access condition callback with the specified arguments. + if (isset($this->callbacks[$callbackName]) && is_callable($this->callbacks[$callbackName])) { + $result = call_user_func_array($this->callbacks[$callbackName], $args); + } else { + throw new AuthorizationException("Authorization failed: Access condition method '$callbackName' does not exist."); + } + + if ($this->debug) { + $this->logger->debug("Result: " . ($result ? "1" : "0")); + } + + return new \PhpParser\Node\Scalar\LNumber($result ? "1" : "0"); + } + } + + public function setParams($params) { + $this->params = $params; + } + + /** + * Resolve an array expression in a condition expression into an actual array. + * + * @param string $arg the array, represented as a string. + * @return array[mixed] the array, as a plain ol' PHP array. + */ + private function resolveArray($arg) { + $arr = []; + $items = (array)$arg->value->items; + foreach ($items as $item) { + if ($item->key) { + $arr[$item->key] = $item->value->value; + } else { + $arr[] = $item->value->value; + } + } + return $arr; + } + + /** + * Resolve a parameter path (e.g. "user.id", "post", etc) into its value. + * + * @param string $path the name of the parameter to resolve, based on the parameters set in this object. + * @throws Exception the path could not be resolved. Path is malformed or key does not exist. + * @return mixed the value of the specified parameter. + */ + private function resolveParamPath($path) { + $pathTokens = explode(".", $path); + $value = $this->params; + foreach ($pathTokens as $token) { + $token = trim($token); + if (is_array($value) && isset($value[$token])) { + $value = $value[$token]; + continue; + } else if (is_object($value) && isset($value->$token)) { + $value = $value->$token; + continue; + } else { + throw new AuthorizationException("Cannot resolve the path \"$path\". Error at token \"$token\"."); + } + } + return $value; + } +} diff --git a/main/app/sprinkles/account/src/Bakery/CreateAdminUser.php b/main/app/sprinkles/account/src/Bakery/CreateAdminUser.php index f928a2c..178c2b3 100644 --- a/main/app/sprinkles/account/src/Bakery/CreateAdminUser.php +++ b/main/app/sprinkles/account/src/Bakery/CreateAdminUser.php @@ -1,321 +1,321 @@ -setName("create-admin") - ->setDescription("Create the initial admin (root) user account"); - } - - /** - * {@inheritDoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) { - $this->io->title("Root account setup"); - - // Need the database - try { - $this->io->writeln("Testing database connection", OutputInterface::VERBOSITY_VERBOSE); - $this->testDB(); - $this->io->writeln("Ok", OutputInterface::VERBOSITY_VERBOSE); - } catch (\Exception $e) { - $this->io->error($e->getMessage()); - exit(1); - } - - // Need migration table - if (!Capsule::schema()->hasColumn('migrations', 'id')) { - $this->io->error("Migrations doesn't appear to have been run! Make sure the database is properly migrated by using the `php bakery migrate` command."); - exit(1); - } - - // Make sure the required mirgations have been run - foreach ($this->dependencies as $migration) { - if (!Migrations::where('migration', $migration)->exists()) { - $this->io->error("Migration `$migration` doesn't appear to have been run! Make sure all migrations are up to date by using the `php bakery migrate` command."); - exit(1); - } - } - - // Make sure that there are no users currently in the user table - // We setup the root account here so it can be done independent of the version check - if (User::count() > 0) { - - $this->io->note("Table 'users' is not empty. Skipping root account setup. To set up the root account again, please truncate or drop the table and try again."); - - } else { - - $this->io->writeln("Please answer the following questions to create the root account:\n"); - - // Get the account details - $userName = $this->askUsername(); - $email = $this->askEmail(); - $firstName = $this->askFirstName(); - $lastName = $this->askLastName(); - $password = $this->askPassword(); - - // Ok, now we've got the info and we can create the new user. - $this->io->write("\nSaving the root user details..."); - $rootUser = new User([ - "user_name" => $userName, - "email" => $email, - "first_name" => $firstName, - "last_name" => $lastName, - "password" => Password::hash($password) - ]); - - $rootUser->save(); - - $defaultRoles = [ - 'user' => Role::where('slug', 'user')->first(), - 'group-admin' => Role::where('slug', 'group-admin')->first(), - 'site-admin' => Role::where('slug', 'site-admin')->first() - ]; - - foreach ($defaultRoles as $slug => $role) { - if ($role) { - $rootUser->roles()->attach($role->id); - } - } - - $this->io->success("Root user creation successful!"); - } - } - - /** - * Ask for the username - * - * @access protected - * @return void - */ - protected function askUsername() { - while (!isset($userName) || !$this->validateUsername($userName)) { - $userName = $this->io->ask("Choose a root username (1-50 characters, no leading or trailing whitespace)"); - } - return $userName; - } - - /** - * Validate the username. - * - * @access protected - * @param mixed $userName - * @return void - */ - protected function validateUsername($userName) { - // Validate length - if (strlen($userName) < 1 || strlen($userName) > 50) { - $this->io->error("Username must be between 1-50 characters"); - return FALSE; - } - - // Validate format - $options = [ - 'options' => [ - 'regexp' => "/^\S((.*\S)|)$/" - ] - ]; - $validate = filter_var($userName, FILTER_VALIDATE_REGEXP, $options); - if (!$validate) { - $this->io->error("Username can't have any leading or trailing whitespace"); - return FALSE; - } - - return TRUE; - } - - /** - * Ask for the email - * - * @access protected - * @return void - */ - protected function askEmail() { - while (!isset($email) || !$this->validateEmail($email)) { - $email = $this->io->ask("Enter a valid email address (1-254 characters, must be compatible with FILTER_VALIDATE_EMAIL)"); - } - return $email; - } - - /** - * Validate the email. - * - * @access protected - * @param mixed $email - * @return void - */ - protected function validateEmail($email) { - // Validate length - if (strlen($email) < 1 || strlen($email) > 254) { - $this->io->error("Email must be between 1-254 characters"); - return FALSE; - } - - // Validate format - if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { - $this->io->error("Email must be compatible with FILTER_VALIDATE_EMAIL"); - return FALSE; - } - - return TRUE; - } - - /** - * Ask for the first name - * - * @access protected - * @return void - */ - protected function askFirstName() { - while (!isset($firstName) || !$this->validateFirstName($firstName)) { - $firstName = $this->io->ask("Enter the user first name (1-20 characters)"); - } - return $firstName; - } - - /** - * validateFirstName function. - * - * @access protected - * @param mixed $name - * @return void - */ - protected function validateFirstName($firstName) { - // Validate length - if (strlen($firstName) < 1 || strlen($firstName) > 20) { - $this->io->error("First name must be between 1-20 characters"); - return FALSE; - } - - return TRUE; - } - - /** - * Ask for the last name - * - * @access protected - * @return void - */ - protected function askLastName() { - while (!isset($lastName) || !$this->validateLastName($lastName)) { - $lastName = $this->io->ask("Enter the user last name (1-30 characters)"); - } - return $lastName; - } - - /** - * validateLastName function. - * - * @access protected - * @param mixed $lastName - * @return void - */ - protected function validateLastName($lastName) { - // Validate length - if (strlen($lastName) < 1 || strlen($lastName) > 30) { - $this->io->error("Last name must be between 1-30 characters"); - return FALSE; - } - - return TRUE; - } - - /** - * Ask for the password - * - * @access protected - * @return void - */ - protected function askPassword() { - while (!isset($password) || !$this->validatePassword($password) || !$this->confirmPassword($password)) { - $password = $this->io->askHidden("Enter password (12-255 characters)"); - } - return $password; - } - - /** - * validatePassword function. - * - * @access protected - * @param mixed $password - * @return void - */ - protected function validatePassword($password) { - if (strlen($password) < 12 || strlen($password) > 255) { - $this->io->error("Password must be between 12-255 characters"); - return FALSE; - } - - return TRUE; - } - - /** - * confirmPassword function. - * - * @access protected - * @param mixed $passwordToConfirm - * @return void - */ - protected function confirmPassword($passwordToConfirm) { - while (!isset($password)) { - $password = $this->io->askHidden("Please re-enter the chosen password"); - } - return $this->validatePasswordConfirmation($password, $passwordToConfirm); - } - - /** - * validatePasswordConfirmation function. - * - * @access protected - * @param mixed $password - * @param mixed $passwordToConfirm - * @return void - */ - protected function validatePasswordConfirmation($password, $passwordToConfirm) { - if ($password != $passwordToConfirm) { - $this->io->error("Passwords do not match, please try again."); - return FALSE; - } - - return TRUE; - } +setName("create-admin") + ->setDescription("Create the initial admin (root) user account"); + } + + /** + * {@inheritDoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) { + $this->io->title("Root account setup"); + + // Need the database + try { + $this->io->writeln("Testing database connection", OutputInterface::VERBOSITY_VERBOSE); + $this->testDB(); + $this->io->writeln("Ok", OutputInterface::VERBOSITY_VERBOSE); + } catch (\Exception $e) { + $this->io->error($e->getMessage()); + exit(1); + } + + // Need migration table + if (!Capsule::schema()->hasColumn('migrations', 'id')) { + $this->io->error("Migrations doesn't appear to have been run! Make sure the database is properly migrated by using the `php bakery migrate` command."); + exit(1); + } + + // Make sure the required mirgations have been run + foreach ($this->dependencies as $migration) { + if (!Migrations::where('migration', $migration)->exists()) { + $this->io->error("Migration `$migration` doesn't appear to have been run! Make sure all migrations are up to date by using the `php bakery migrate` command."); + exit(1); + } + } + + // Make sure that there are no users currently in the user table + // We setup the root account here so it can be done independent of the version check + if (User::count() > 0) { + + $this->io->note("Table 'users' is not empty. Skipping root account setup. To set up the root account again, please truncate or drop the table and try again."); + + } else { + + $this->io->writeln("Please answer the following questions to create the root account:\n"); + + // Get the account details + $userName = $this->askUsername(); + $email = $this->askEmail(); + $firstName = $this->askFirstName(); + $lastName = $this->askLastName(); + $password = $this->askPassword(); + + // Ok, now we've got the info and we can create the new user. + $this->io->write("\nSaving the root user details..."); + $rootUser = new User([ + "user_name" => $userName, + "email" => $email, + "first_name" => $firstName, + "last_name" => $lastName, + "password" => Password::hash($password) + ]); + + $rootUser->save(); + + $defaultRoles = [ + 'user' => Role::where('slug', 'user')->first(), + 'group-admin' => Role::where('slug', 'group-admin')->first(), + 'site-admin' => Role::where('slug', 'site-admin')->first() + ]; + + foreach ($defaultRoles as $slug => $role) { + if ($role) { + $rootUser->roles()->attach($role->id); + } + } + + $this->io->success("Root user creation successful!"); + } + } + + /** + * Ask for the username + * + * @access protected + * @return void + */ + protected function askUsername() { + while (!isset($userName) || !$this->validateUsername($userName)) { + $userName = $this->io->ask("Choose a root username (1-50 characters, no leading or trailing whitespace)"); + } + return $userName; + } + + /** + * Validate the username. + * + * @access protected + * @param mixed $userName + * @return void + */ + protected function validateUsername($userName) { + // Validate length + if (strlen($userName) < 1 || strlen($userName) > 50) { + $this->io->error("Username must be between 1-50 characters"); + return FALSE; + } + + // Validate format + $options = [ + 'options' => [ + 'regexp' => "/^\S((.*\S)|)$/" + ] + ]; + $validate = filter_var($userName, FILTER_VALIDATE_REGEXP, $options); + if (!$validate) { + $this->io->error("Username can't have any leading or trailing whitespace"); + return FALSE; + } + + return TRUE; + } + + /** + * Ask for the email + * + * @access protected + * @return void + */ + protected function askEmail() { + while (!isset($email) || !$this->validateEmail($email)) { + $email = $this->io->ask("Enter a valid email address (1-254 characters, must be compatible with FILTER_VALIDATE_EMAIL)"); + } + return $email; + } + + /** + * Validate the email. + * + * @access protected + * @param mixed $email + * @return void + */ + protected function validateEmail($email) { + // Validate length + if (strlen($email) < 1 || strlen($email) > 254) { + $this->io->error("Email must be between 1-254 characters"); + return FALSE; + } + + // Validate format + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + $this->io->error("Email must be compatible with FILTER_VALIDATE_EMAIL"); + return FALSE; + } + + return TRUE; + } + + /** + * Ask for the first name + * + * @access protected + * @return void + */ + protected function askFirstName() { + while (!isset($firstName) || !$this->validateFirstName($firstName)) { + $firstName = $this->io->ask("Enter the user first name (1-20 characters)"); + } + return $firstName; + } + + /** + * validateFirstName function. + * + * @access protected + * @param mixed $name + * @return void + */ + protected function validateFirstName($firstName) { + // Validate length + if (strlen($firstName) < 1 || strlen($firstName) > 20) { + $this->io->error("First name must be between 1-20 characters"); + return FALSE; + } + + return TRUE; + } + + /** + * Ask for the last name + * + * @access protected + * @return void + */ + protected function askLastName() { + while (!isset($lastName) || !$this->validateLastName($lastName)) { + $lastName = $this->io->ask("Enter the user last name (1-30 characters)"); + } + return $lastName; + } + + /** + * validateLastName function. + * + * @access protected + * @param mixed $lastName + * @return void + */ + protected function validateLastName($lastName) { + // Validate length + if (strlen($lastName) < 1 || strlen($lastName) > 30) { + $this->io->error("Last name must be between 1-30 characters"); + return FALSE; + } + + return TRUE; + } + + /** + * Ask for the password + * + * @access protected + * @return void + */ + protected function askPassword() { + while (!isset($password) || !$this->validatePassword($password) || !$this->confirmPassword($password)) { + $password = $this->io->askHidden("Enter password (12-255 characters)"); + } + return $password; + } + + /** + * validatePassword function. + * + * @access protected + * @param mixed $password + * @return void + */ + protected function validatePassword($password) { + if (strlen($password) < 12 || strlen($password) > 255) { + $this->io->error("Password must be between 12-255 characters"); + return FALSE; + } + + return TRUE; + } + + /** + * confirmPassword function. + * + * @access protected + * @param mixed $passwordToConfirm + * @return void + */ + protected function confirmPassword($passwordToConfirm) { + while (!isset($password)) { + $password = $this->io->askHidden("Please re-enter the chosen password"); + } + return $this->validatePasswordConfirmation($password, $passwordToConfirm); + } + + /** + * validatePasswordConfirmation function. + * + * @access protected + * @param mixed $password + * @param mixed $passwordToConfirm + * @return void + */ + protected function validatePasswordConfirmation($password, $passwordToConfirm) { + if ($password != $passwordToConfirm) { + $this->io->error("Passwords do not match, please try again."); + return FALSE; + } + + return TRUE; + } } \ No newline at end of file diff --git a/main/app/sprinkles/account/src/Controller/AccountController.php b/main/app/sprinkles/account/src/Controller/AccountController.php index 7373923..c4201a7 100644 --- a/main/app/sprinkles/account/src/Controller/AccountController.php +++ b/main/app/sprinkles/account/src/Controller/AccountController.php @@ -1,1271 +1,1271 @@ -ci->alerts; - - // GET parameters - $params = $request->getQueryParams(); - - // Load request schema - $schema = new RequestSchema('schema://requests/check-username.yaml'); - - // Whitelist and set parameter defaults - $transformer = new RequestDataTransformer($schema); - $data = $transformer->transform($params); - - // Validate, and halt on validation errors. - $validator = new ServerSideValidator($schema, $this->ci->translator); - if (!$validator->validate($data)) { - // O: encapsulate the communication of error messages from ServerSideValidator to the BadRequestException - $e = new BadRequestException('Missing or malformed request data!'); - foreach ($validator->errors() as $idx => $field) { - foreach ($field as $eidx => $error) { - $e->addUserMessage($error); - } - } - throw $e; - } - - /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */ - $throttler = $this->ci->throttler; - $delay = $throttler->getDelay('check_username_request'); - - // Throttle requests - if ($delay > 0) { - return $response->withStatus(429); - } - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - /** @var \UserFrosting\I18n\MessageTranslator $translator */ - $translator = $this->ci->translator; - - // Log throttleable event - $throttler->logEvent('check_username_request'); - - if ($classMapper->staticMethod('user', 'findUnique', $data['user_name'], 'user_name')) { - $message = $translator->translate('USERNAME.NOT_AVAILABLE', $data); - return $response->write($message)->withStatus(200); - } else { - return $response->write('true')->withStatus(200); - } - } - - /** - * Processes a request to cancel a password reset request. - * - * This is provided so that users can cancel a password reset request, if they made it in error or if it was not initiated by themselves. - * Processes the request from the password reset link, checking that: - * 1. The provided token is associated with an existing user account, who has a pending password reset request. - * Request type: GET - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function denyResetPassword(Request $request, Response $response, $args) { - // GET parameters - $params = $request->getQueryParams(); - - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - $loginPage = $this->ci->router->pathFor('login'); - - // Load validation rules - $schema = new RequestSchema('schema://requests/deny-password.yaml'); - - // Whitelist and set parameter defaults - $transformer = new RequestDataTransformer($schema); - $data = $transformer->transform($params); - - // Validate, and halt on validation errors. Since this is a GET request, we need to redirect on failure - $validator = new ServerSideValidator($schema, $this->ci->translator); - if (!$validator->validate($data)) { - $ms->addValidationErrors($validator); - // 400 code + redirect is perfectly fine, according to user Dilaz in #laravel - return $response->withRedirect($loginPage, 400); - } - - $passwordReset = $this->ci->repoPasswordReset->cancel($data['token']); - - if (!$passwordReset) { - $ms->addMessageTranslated('danger', 'PASSWORD.FORGET.INVALID'); - return $response->withRedirect($loginPage, 400); - } - - $ms->addMessageTranslated('success', 'PASSWORD.FORGET.REQUEST_CANNED'); - return $response->withRedirect($loginPage); - } - - /** - * Processes a request to email a forgotten password reset link to the user. - * - * Processes the request from the form on the "forgot password" page, checking that: - * 1. The rate limit for this type of request is being observed. - * 2. The provided email address belongs to a registered account; - * 3. The submitted data is valid. - * Note that we have removed the requirement that a password reset request not already be in progress. - * This is because we need to allow users to re-request a reset, even if they lose the first reset email. - * This route is "public access". - * Request type: POST - * @odo require additional user information - * @odo prevent password reset requests for root account? - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function forgotPassword(Request $request, Response $response, $args) { - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - // Get POST parameters - $params = $request->getParsedBody(); - - // Load the request schema - $schema = new RequestSchema('schema://requests/forgot-password.yaml'); - - // Whitelist and set parameter defaults - $transformer = new RequestDataTransformer($schema); - $data = $transformer->transform($params); - - // Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit. - $validator = new ServerSideValidator($schema, $this->ci->translator); - if (!$validator->validate($data)) { - $ms->addValidationErrors($validator); - return $response->withStatus(400); - } - - // Throttle requests - - /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */ - $throttler = $this->ci->throttler; - - $throttleData = [ - 'email' => $data['email'] - ]; - $delay = $throttler->getDelay('password_reset_request', $throttleData); - - if ($delay > 0) { - $ms->addMessageTranslated('danger', 'RATE_LIMIT_EXCEEDED', ['delay' => $delay]); - return $response->withStatus(429); - } - - // All checks passed! log events/activities, update user, and send email - // Begin transaction - DB will be rolled back if an exception occurs - Capsule::transaction(function () use ($classMapper, $data, $throttler, $throttleData, $config) { - - // Log throttleable event - $throttler->logEvent('password_reset_request', $throttleData); - - // Load the user, by email address - $user = $classMapper->staticMethod('user', 'where', 'email', $data['email'])->first(); - - // Check that the email exists. - // If there is no user with that email address, we should still pretend like we succeeded, to prevent account enumeration - if ($user) { - // Try to generate a new password reset request. - // Use timeout for "reset password" - $passwordReset = $this->ci->repoPasswordReset->create($user, $config['password_reset.timeouts.reset']); - - // Create and send email - $message = new TwigMailMessage($this->ci->view, 'mail/password-reset.html.twig'); - $message->from($config['address_book.admin']) - ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name)) - ->addParams([ - 'user' => $user, - 'token' => $passwordReset->getToken(), - 'request_date' => Carbon::now()->format('Y-m-d H:i:s') - ]); - - $this->ci->mailer->send($message); - } - }); - - $ms->addMessageTranslated('success', 'PASSWORD.FORGET.REQUEST_SENT', ['email' => $data['email']]); - return $response->withStatus(200); - } - - /** - * Returns a modal containing account terms of service. - * - * This does NOT render a complete page. Instead, it renders the HTML for the form, which can be embedded in other pages. - * Request type: GET - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function getModalAccountTos(Request $request, Response $response, $args) { - return $this->ci->view->render($response, 'modals/tos.html.twig'); - } - - /** - * Generate a random captcha, store it to the session, and return the captcha image. - * - * Request type: GET - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function imageCaptcha(Request $request, Response $response, $args) { - $captcha = new Captcha($this->ci->session, $this->ci->config['session.keys.captcha']); - $captcha->generateRandomCode(); - - return $response->withStatus(200) - ->withHeader('Content-Type', 'image/png;base64') - ->write($captcha->getImage()); - } - - /** - * Processes an account login request. - * - * Processes the request from the form on the login page, checking that: - * 1. The user is not already logged in. - * 2. The rate limit for this type of request is being observed. - * 3. Email login is enabled, if an email address was used. - * 4. The user account exists. - * 5. The user account is enabled and verified. - * 6. The user entered a valid username/email and password. - * This route, by definition, is "public access". - * Request type: POST - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function login(Request $request, Response $response, $args) { - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - - /** @var \UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ - $authenticator = $this->ci->authenticator; - - // Return 200 success if user is already logged in - if ($authenticator->check()) { - $ms->addMessageTranslated('warning', 'LOGIN.ALREADY_COMPLETE'); - return $response->withStatus(200); - } - - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - // Get POST parameters - $params = $request->getParsedBody(); - - // Load the request schema - $schema = new RequestSchema('schema://requests/login.yaml'); - - // Whitelist and set parameter defaults - $transformer = new RequestDataTransformer($schema); - $data = $transformer->transform($params); - - // Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit. - $validator = new ServerSideValidator($schema, $this->ci->translator); - if (!$validator->validate($data)) { - $ms->addValidationErrors($validator); - return $response->withStatus(400); - } - - // Determine whether we are trying to log in with an email address or a username - $isEmail = filter_var($data['user_name'], FILTER_VALIDATE_EMAIL); - - // Throttle requests - - /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */ - $throttler = $this->ci->throttler; - - $userIdentifier = $data['user_name']; - - $throttleData = [ - 'user_identifier' => $userIdentifier - ]; - - $delay = $throttler->getDelay('sign_in_attempt', $throttleData); - if ($delay > 0) { - $ms->addMessageTranslated('danger', 'RATE_LIMIT_EXCEEDED', [ - 'delay' => $delay - ]); - return $response->withStatus(429); - } - - // Log throttleable event - $throttler->logEvent('sign_in_attempt', $throttleData); - - // If credential is an email address, but email login is not enabled, raise an error. - // Note that we do this after logging throttle event, so this error counts towards throttling limit. - if ($isEmail && !$config['site.login.enable_email']) { - $ms->addMessageTranslated('danger', 'USER_OR_PASS_INVALID'); - return $response->withStatus(403); - } - - // Try to authenticate the user. Authenticator will throw an exception on failure. - /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ - $authenticator = $this->ci->authenticator; - - $currentUser = $authenticator->attempt(($isEmail ? 'email' : 'user_name'), $userIdentifier, $data['password'], $data['rememberme']); - - $ms->addMessageTranslated('success', 'WELCOME', $currentUser->export()); - - // Set redirect, if relevant - $redirectOnLogin = $this->ci->get('redirect.onLogin'); - - return $redirectOnLogin($request, $response, $args); - } - - /** - * Log the user out completely, including destroying any "remember me" token. - * - * Request type: GET - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function logout(Request $request, Response $response, $args) { - // Destroy the session - $this->ci->authenticator->logout(); - - // Return to home page - $config = $this->ci->config; - return $response->withStatus(302)->withHeader('Location', $config['site.uri.public']); - } - - /** - * Render the "forgot password" page. - * - * This creates a simple form to allow users who forgot their password to have a time-limited password reset link emailed to them. - * By default, this is a "public page" (does not require authentication). - * Request type: GET - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function pageForgotPassword(Request $request, Response $response, $args) { - // Load validation rules - $schema = new RequestSchema('schema://requests/forgot-password.yaml'); - $validator = new JqueryValidationAdapter($schema, $this->ci->translator); - - return $this->ci->view->render($response, 'pages/forgot-password.html.twig', [ - 'page' => [ - 'validators' => [ - 'forgot_password' => $validator->rules('json', FALSE) - ] - ] - ]); - } - - - /** - * Render the account registration page for UserFrosting. - * - * This allows new (non-authenticated) users to create a new account for themselves on your website (if enabled). - * By definition, this is a "public page" (does not require authentication). - * Request type: GET - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function pageRegister(Request $request, Response $response, $args) { - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - if (!$config['site.registration.enabled']) { - throw new NotFoundException($request, $response); - } - - /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ - $authenticator = $this->ci->authenticator; - - // Redirect if user is already logged in - if ($authenticator->check()) { - $redirect = $this->ci->get('redirect.onAlreadyLoggedIn'); - - return $redirect($request, $response, $args); - } - - // Load validation rules - $schema = new RequestSchema('schema://requests/register.yaml'); - $validatorRegister = new JqueryValidationAdapter($schema, $this->ci->translator); - - return $this->ci->view->render($response, 'pages/register.html.twig', [ - 'page' => [ - 'validators' => [ - 'register' => $validatorRegister->rules('json', FALSE) - ] - ] - ]); - } - - /** - * Render the "resend verification email" page. - * - * This is a form that allows users who lost their account verification link to have the link resent to their email address. - * By default, this is a "public page" (does not require authentication). - * Request type: GET - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function pageResendVerification(Request $request, Response $response, $args) { - // Load validation rules - $schema = new RequestSchema('schema://requests/resend-verification.yaml'); - $validator = new JqueryValidationAdapter($schema, $this->ci->translator); - - return $this->ci->view->render($response, 'pages/resend-verification.html.twig', [ - 'page' => [ - 'validators' => [ - 'resend_verification' => $validator->rules('json', FALSE) - ] - ] - ]); - } - - /** - * Reset password page. - * - * Renders the new password page for password reset requests. - * Request type: GET - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function pageResetPassword(Request $request, Response $response, $args) { - // Insert the user's secret token from the link into the password reset form - $params = $request->getQueryParams(); - - // Load validation rules - note this uses the same schema as "set password" - $schema = new RequestSchema('schema://requests/set-password.yaml'); - $validator = new JqueryValidationAdapter($schema, $this->ci->translator); - - return $this->ci->view->render($response, 'pages/reset-password.html.twig', [ - 'page' => [ - 'validators' => [ - 'set_password' => $validator->rules('json', FALSE) - ] - ], - 'token' => isset($params['token']) ? $params['token'] : '', - ]); - } - - /** - * Render the "set password" page. - * - * Renders the page where new users who have had accounts created for them by another user, can set their password. - * By default, this is a "public page" (does not require authentication). - * Request type: GET - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function pageSetPassword(Request $request, Response $response, $args) { - // Insert the user's secret token from the link into the password set form - $params = $request->getQueryParams(); - - // Load validation rules - $schema = new RequestSchema('schema://requests/set-password.yaml'); - $validator = new JqueryValidationAdapter($schema, $this->ci->translator); - - return $this->ci->view->render($response, 'pages/set-password.html.twig', [ - 'page' => [ - 'validators' => [ - 'set_password' => $validator->rules('json', FALSE) - ] - ], - 'token' => isset($params['token']) ? $params['token'] : '', - ]); - } - - /** - * Account settings page. - * - * Provides a form for users to modify various properties of their account, such as name, email, locale, etc. - * Any fields that the user does not have permission to modify will be automatically disabled. - * This page requires authentication. - * Request type: GET - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function pageSettings(Request $request, Response $response, $args) { - /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ - $authorizer = $this->ci->authorizer; - - /** @var \UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - // Access-controlled page - if (!$authorizer->checkAccess($currentUser, 'uri_account_settings')) { - throw new ForbiddenException(); - } - - // Load validation rules - $schema = new RequestSchema('schema://requests/account-settings.yaml'); - $validatorAccountSettings = new JqueryValidationAdapter($schema, $this->ci->translator); - - $schema = new RequestSchema('schema://requests/profile-settings.yaml'); - $validatorProfileSettings = new JqueryValidationAdapter($schema, $this->ci->translator); - - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - // Get a list of all locales - $locales = $config->getDefined('site.locales.available'); - - return $this->ci->view->render($response, 'pages/account-settings.html.twig', [ - 'locales' => $locales, - 'page' => [ - 'validators' => [ - 'account_settings' => $validatorAccountSettings->rules('json', FALSE), - 'profile_settings' => $validatorProfileSettings->rules('json', FALSE) - ], - 'visibility' => ($authorizer->checkAccess($currentUser, 'update_account_settings') ? '' : 'disabled') - ] - ]); - } - - /** - * Render the account sign-in page for UserFrosting. - * - * This allows existing users to sign in. - * By definition, this is a "public page" (does not require authentication). - * Request type: GET - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function pageSignIn(Request $request, Response $response, $args) { - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ - $authenticator = $this->ci->authenticator; - - // Redirect if user is already logged in - if ($authenticator->check()) { - $redirect = $this->ci->get('redirect.onAlreadyLoggedIn'); - - return $redirect($request, $response, $args); - } - - // Load validation rules - $schema = new RequestSchema('schema://requests/login.yaml'); - $validatorLogin = new JqueryValidationAdapter($schema, $this->ci->translator); - - return $this->ci->view->render($response, 'pages/sign-in.html.twig', [ - 'page' => [ - 'validators' => [ - 'login' => $validatorLogin->rules('json', FALSE) - ] - ] - ]); - } - - /** - * Processes a request to update a user's profile information. - * - * Processes the request from the user profile settings form, checking that: - * 1. They have the necessary permissions to update the posted field(s); - * 2. The submitted data is valid. - * This route requires authentication. - * Request type: POST - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function profile(Request $request, Response $response, $args) { - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - - /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ - $authorizer = $this->ci->authorizer; - - /** @var \UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - // Access control for entire resource - check that the current user has permission to modify themselves - // See recipe "per-field access control" for dynamic fine-grained control over which properties a user can modify. - if (!$authorizer->checkAccess($currentUser, 'update_account_settings')) { - $ms->addMessageTranslated('danger', 'ACCOUNT.ACCESS_DENIED'); - return $response->withStatus(403); - } - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - // POST parameters - $params = $request->getParsedBody(); - - // Load the request schema - $schema = new RequestSchema('schema://requests/profile-settings.yaml'); - - // Whitelist and set parameter defaults - $transformer = new RequestDataTransformer($schema); - $data = $transformer->transform($params); - - $error = FALSE; - - // Validate, and halt on validation errors. - $validator = new ServerSideValidator($schema, $this->ci->translator); - if (!$validator->validate($data)) { - $ms->addValidationErrors($validator); - $error = TRUE; - } - - // Check that locale is valid - $locales = $config->getDefined('site.locales.available'); - if (!array_key_exists($data['locale'], $locales)) { - $ms->addMessageTranslated('danger', 'LOCALE.INVALID', $data); - $error = TRUE; - } - - if ($error) { - return $response->withStatus(400); - } - - // Looks good, let's update with new values! - // Note that only fields listed in `profile-settings.yaml` will be permitted in $data, so this prevents the user from updating all columns in the DB - $currentUser->fill($data); - - $currentUser->save(); - - // Create activity record - $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated their profile settings.", [ - 'type' => 'update_profile_settings' - ]); - - $ms->addMessageTranslated('success', 'PROFILE.UPDATED'); - return $response->withStatus(200); - } - - /** - * Processes an new account registration request. - * - * This is throttled to prevent account enumeration, since it needs to divulge when a username/email has been used. - * Processes the request from the form on the registration page, checking that: - * 1. The honeypot was not modified; - * 2. The master account has already been created (during installation); - * 3. Account registration is enabled; - * 4. The user is not already logged in; - * 5. Valid information was entered; - * 6. The captcha, if enabled, is correct; - * 7. The username and email are not already taken. - * Automatically sends an activation link upon success, if account activation is enabled. - * This route is "public access". - * Request type: POST - * Returns the User Object for the user record that was created. - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function register(Request $request, Response $response, $args) { - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - // Get POST parameters: user_name, first_name, last_name, email, password, passwordc, captcha, spiderbro, csrf_token - $params = $request->getParsedBody(); - - // Check the honeypot. 'spiderbro' is not a real field, it is hidden on the main page and must be submitted with its default value for this to be processed. - if (!isset($params['spiderbro']) || $params['spiderbro'] != 'http://') { - throw new SpammyRequestException('Possible spam received:' . print_r($params, TRUE)); - } - - // Security measure: do not allow registering new users until the master account has been created. - if (!$classMapper->staticMethod('user', 'find', $config['reserved_user_ids.master'])) { - $ms->addMessageTranslated('danger', 'ACCOUNT.MASTER_NOT_EXISTS'); - return $response->withStatus(403); - } - - // Check if registration is currently enabled - if (!$config['site.registration.enabled']) { - $ms->addMessageTranslated('danger', 'REGISTRATION.DISABLED'); - return $response->withStatus(403); - } - - /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ - $authenticator = $this->ci->authenticator; - - // Prevent the user from registering if he/she is already logged in - if ($authenticator->check()) { - $ms->addMessageTranslated('danger', 'REGISTRATION.LOGOUT'); - return $response->withStatus(403); - } - - // Load the request schema - $schema = new RequestSchema('schema://requests/register.yaml'); - - // Whitelist and set parameter defaults - $transformer = new RequestDataTransformer($schema); - $data = $transformer->transform($params); - - $error = FALSE; - - // Validate request data - $validator = new ServerSideValidator($schema, $this->ci->translator); - if (!$validator->validate($data)) { - $ms->addValidationErrors($validator); - $error = TRUE; - } - - /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */ - $throttler = $this->ci->throttler; - $delay = $throttler->getDelay('registration_attempt'); - - // Throttle requests - if ($delay > 0) { - return $response->withStatus(429); - } - - // Check if username or email already exists - if ($classMapper->staticMethod('user', 'findUnique', $data['user_name'], 'user_name')) { - $ms->addMessageTranslated('danger', 'USERNAME.IN_USE', $data); - $error = TRUE; - } - - if ($classMapper->staticMethod('user', 'findUnique', $data['email'], 'email')) { - $ms->addMessageTranslated('danger', 'EMAIL.IN_USE', $data); - $error = TRUE; - } - - // Check captcha, if required - if ($config['site.registration.captcha']) { - $captcha = new Captcha($this->ci->session, $this->ci->config['session.keys.captcha']); - if (!$data['captcha'] || !$captcha->verifyCode($data['captcha'])) { - $ms->addMessageTranslated('danger', 'CAPTCHA.FAIL'); - $error = TRUE; - } - } - - if ($error) { - return $response->withStatus(400); - } - - // Remove captcha, password confirmation from object data after validation - unset($data['captcha']); - unset($data['passwordc']); - - if ($config['site.registration.require_email_verification']) { - $data['flag_verified'] = FALSE; - } else { - $data['flag_verified'] = TRUE; - } - - // Load default group - $groupSlug = $config['site.registration.user_defaults.group']; - $defaultGroup = $classMapper->staticMethod('group', 'where', 'slug', $groupSlug)->first(); - - if (!$defaultGroup) { - $e = new HttpException("Account registration is not working because the default group '$groupSlug' does not exist."); - $e->addUserMessage('REGISTRATION.BROKEN'); - throw $e; - } - - // Set default group - $data['group_id'] = $defaultGroup->id; - - // Set default locale - $data['locale'] = $config['site.registration.user_defaults.locale']; - - // Hash password - $data['password'] = Password::hash($data['password']); - - // All checks passed! log events/activities, create user, and send verification email (if required) - // Begin transaction - DB will be rolled back if an exception occurs - Capsule::transaction(function () use ($classMapper, $data, $ms, $config, $throttler) { - // Log throttleable event - $throttler->logEvent('registration_attempt'); - - // Create the user - $user = $classMapper->createInstance('user', $data); - - // Store new user to database - $user->save(); - - // Create activity record - $this->ci->userActivityLogger->info("User {$user->user_name} registered for a new account.", [ - 'type' => 'sign_up', - 'user_id' => $user->id - ]); - - // Load default roles - $defaultRoleSlugs = $classMapper->staticMethod('role', 'getDefaultSlugs'); - $defaultRoles = $classMapper->staticMethod('role', 'whereIn', 'slug', $defaultRoleSlugs)->get(); - $defaultRoleIds = $defaultRoles->pluck('id')->all(); - - // Attach default roles - $user->roles()->attach($defaultRoleIds); - - // Verification email - if ($config['site.registration.require_email_verification']) { - // Try to generate a new verification request - $verification = $this->ci->repoVerification->create($user, $config['verification.timeout']); - - // Create and send verification email - $message = new TwigMailMessage($this->ci->view, 'mail/verify-account.html.twig'); - - $message->from($config['address_book.admin']) - ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name)) - ->addParams([ - 'user' => $user, - 'token' => $verification->getToken() - ]); - - $this->ci->mailer->send($message); - - $ms->addMessageTranslated('success', 'REGISTRATION.COMPLETE_TYPE2', $user->toArray()); - } else { - // No verification required - $ms->addMessageTranslated('success', 'REGISTRATION.COMPLETE_TYPE1'); - } - }); - - return $response->withStatus(200); - } - - /** - * Processes a request to resend the verification email for a new user account. - * - * Processes the request from the resend verification email form, checking that: - * 1. The rate limit on this type of request is observed; - * 2. The provided email is associated with an existing user account; - * 3. The user account is not already verified; - * 4. The submitted data is valid. - * This route is "public access". - * Request type: POST - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function resendVerification(Request $request, Response $response, $args) { - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - // Get POST parameters - $params = $request->getParsedBody(); - - // Load the request schema - $schema = new RequestSchema('schema://requests/resend-verification.yaml'); - - // Whitelist and set parameter defaults - $transformer = new RequestDataTransformer($schema); - $data = $transformer->transform($params); - - // Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit. - $validator = new ServerSideValidator($schema, $this->ci->translator); - if (!$validator->validate($data)) { - $ms->addValidationErrors($validator); - return $response->withStatus(400); - } - - // Throttle requests - - /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */ - $throttler = $this->ci->throttler; - - $throttleData = [ - 'email' => $data['email'] - ]; - $delay = $throttler->getDelay('verification_request', $throttleData); - - if ($delay > 0) { - $ms->addMessageTranslated('danger', 'RATE_LIMIT_EXCEEDED', ['delay' => $delay]); - return $response->withStatus(429); - } - - // All checks passed! log events/activities, create user, and send verification email (if required) - // Begin transaction - DB will be rolled back if an exception occurs - Capsule::transaction(function () use ($classMapper, $data, $throttler, $throttleData, $config) { - // Log throttleable event - $throttler->logEvent('verification_request', $throttleData); - - // Load the user, by email address - $user = $classMapper->staticMethod('user', 'where', 'email', $data['email'])->first(); - - // Check that the user exists and is not already verified. - // If there is no user with that email address, or the user exists and is already verified, - // we pretend like we succeeded to prevent account enumeration - if ($user && $user->flag_verified != '1') { - // We're good to go - record user activity and send the email - $verification = $this->ci->repoVerification->create($user, $config['verification.timeout']); - - // Create and send verification email - $message = new TwigMailMessage($this->ci->view, 'mail/resend-verification.html.twig'); - - $message->from($config['address_book.admin']) - ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name)) - ->addParams([ - 'user' => $user, - 'token' => $verification->getToken() - ]); - - $this->ci->mailer->send($message); - } - }); - - $ms->addMessageTranslated('success', 'ACCOUNT.VERIFICATION.NEW_LINK_SENT', ['email' => $data['email']]); - return $response->withStatus(200); - } - - /** - * Processes a request to set the password for a new or current user. - * - * Processes the request from the password create/reset form, which should have the secret token embedded in it, checking that: - * 1. The provided secret token is associated with an existing user account; - * 2. The user has a password set/reset request in progress; - * 3. The token has not expired; - * 4. The submitted data (new password) is valid. - * This route is "public access". - * Request type: POST - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function setPassword(Request $request, Response $response, $args) { - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - // Get POST parameters - $params = $request->getParsedBody(); - - // Load the request schema - $schema = new RequestSchema('schema://requests/set-password.yaml'); - - // Whitelist and set parameter defaults - $transformer = new RequestDataTransformer($schema); - $data = $transformer->transform($params); - - // Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit. - $validator = new ServerSideValidator($schema, $this->ci->translator); - if (!$validator->validate($data)) { - $ms->addValidationErrors($validator); - return $response->withStatus(400); - } - - $forgotPasswordPage = $this->ci->router->pathFor('forgot-password'); - - // Ok, try to complete the request with the specified token and new password - $passwordReset = $this->ci->repoPasswordReset->complete($data['token'], [ - 'password' => $data['password'] - ]); - - if (!$passwordReset) { - $ms->addMessageTranslated('danger', 'PASSWORD.FORGET.INVALID', ['url' => $forgotPasswordPage]); - return $response->withStatus(400); - } - - $ms->addMessageTranslated('success', 'PASSWORD.UPDATED'); - - /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ - $authenticator = $this->ci->authenticator; - - // Log out any existing user, and create a new session - if ($authenticator->check()) { - $authenticator->logout(); - } - - // Auto-login the user (without "remember me") - $user = $passwordReset->user; - $authenticator->login($user); - - $ms->addMessageTranslated('success', 'WELCOME', $user->export()); - return $response->withStatus(200); - } - - /** - * Processes a request to update a user's account information. - * - * Processes the request from the user account settings form, checking that: - * 1. The user correctly input their current password; - * 2. They have the necessary permissions to update the posted field(s); - * 3. The submitted data is valid. - * This route requires authentication. - * Request type: POST - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function settings(Request $request, Response $response, $args) { - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - - /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ - $authorizer = $this->ci->authorizer; - - /** @var \UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ - $currentUser = $this->ci->currentUser; - - // Access control for entire resource - check that the current user has permission to modify themselves - // See recipe "per-field access control" for dynamic fine-grained control over which properties a user can modify. - if (!$authorizer->checkAccess($currentUser, 'update_account_settings')) { - $ms->addMessageTranslated('danger', 'ACCOUNT.ACCESS_DENIED'); - return $response->withStatus(403); - } - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - // POST parameters - $params = $request->getParsedBody(); - - // Load the request schema - $schema = new RequestSchema('schema://requests/account-settings.yaml'); - - // Whitelist and set parameter defaults - $transformer = new RequestDataTransformer($schema); - $data = $transformer->transform($params); - - $error = FALSE; - - // Validate, and halt on validation errors. - $validator = new ServerSideValidator($schema, $this->ci->translator); - if (!$validator->validate($data)) { - $ms->addValidationErrors($validator); - $error = TRUE; - } - - // Confirm current password - if (!isset($data['passwordcheck']) || !Password::verify($data['passwordcheck'], $currentUser->password)) { - $ms->addMessageTranslated('danger', 'PASSWORD.INVALID'); - $error = TRUE; - } - - // Remove password check, password confirmation from object data after validation - unset($data['passwordcheck']); - unset($data['passwordc']); - - // If new email was submitted, check that the email address is not in use - if (isset($data['email']) && $data['email'] != $currentUser->email && $classMapper->staticMethod('user', 'findUnique', $data['email'], 'email')) { - $ms->addMessageTranslated('danger', 'EMAIL.IN_USE', $data); - $error = TRUE; - } - - if ($error) { - return $response->withStatus(400); - } - - // Hash new password, if specified - if (isset($data['password']) && !empty($data['password'])) { - $data['password'] = Password::hash($data['password']); - } else { - // Do not pass to model if no password is specified - unset($data['password']); - } - - // Looks good, let's update with new values! - // Note that only fields listed in `account-settings.yaml` will be permitted in $data, so this prevents the user from updating all columns in the DB - $currentUser->fill($data); - - $currentUser->save(); - - // Create activity record - $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated their account settings.", [ - 'type' => 'update_account_settings' - ]); - - $ms->addMessageTranslated('success', 'ACCOUNT.SETTINGS.UPDATED'); - return $response->withStatus(200); - } - - /** - * Suggest an available username for a specified first/last name. - * - * This route is "public access". - * Request type: GET - * @odo Can this route be abused for account enumeration? If so we should throttle it as well. - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function suggestUsername(Request $request, Response $response, $args) { - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - $suggestion = AccountUtil::randomUniqueUsername($classMapper, 50, 10); - - // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. - // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). - return $response->withJson([ - 'user_name' => $suggestion - ], 200, JSON_PRETTY_PRINT); - } - - /** - * Processes an new email verification request. - * - * Processes the request from the email verification link that was emailed to the user, checking that: - * 1. The token provided matches a user in the database; - * 2. The user account is not already verified; - * This route is "public access". - * Request type: GET - * - * @param Request $request - * @param Response $response - * @param array $args - * @return void - */ - public function verify(Request $request, Response $response, $args) { - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - $loginPage = $this->ci->router->pathFor('login'); - - // GET parameters - $params = $request->getQueryParams(); - - // Load request schema - $schema = new RequestSchema('schema://requests/account-verify.yaml'); - - // Whitelist and set parameter defaults - $transformer = new RequestDataTransformer($schema); - $data = $transformer->transform($params); - - // Validate, and halt on validation errors. This is a GET request, so we redirect on validation error. - $validator = new ServerSideValidator($schema, $this->ci->translator); - if (!$validator->validate($data)) { - $ms->addValidationErrors($validator); - // 400 code + redirect is perfectly fine, according to user Dilaz in #laravel - return $response->withRedirect($loginPage, 400); - } - - $verification = $this->ci->repoVerification->complete($data['token']); - - if (!$verification) { - $ms->addMessageTranslated('danger', 'ACCOUNT.VERIFICATION.TOKEN_NOT_FOUND'); - return $response->withRedirect($loginPage, 400); - } - - $ms->addMessageTranslated('success', 'ACCOUNT.VERIFICATION.COMPLETE'); - - // Forward to login page - return $response->withRedirect($loginPage); - } -} +ci->alerts; + + // GET parameters + $params = $request->getQueryParams(); + + // Load request schema + $schema = new RequestSchema('schema://requests/check-username.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and halt on validation errors. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + // O: encapsulate the communication of error messages from ServerSideValidator to the BadRequestException + $e = new BadRequestException('Missing or malformed request data!'); + foreach ($validator->errors() as $idx => $field) { + foreach ($field as $eidx => $error) { + $e->addUserMessage($error); + } + } + throw $e; + } + + /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */ + $throttler = $this->ci->throttler; + $delay = $throttler->getDelay('check_username_request'); + + // Throttle requests + if ($delay > 0) { + return $response->withStatus(429); + } + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\I18n\MessageTranslator $translator */ + $translator = $this->ci->translator; + + // Log throttleable event + $throttler->logEvent('check_username_request'); + + if ($classMapper->staticMethod('user', 'findUnique', $data['user_name'], 'user_name')) { + $message = $translator->translate('USERNAME.NOT_AVAILABLE', $data); + return $response->write($message)->withStatus(200); + } else { + return $response->write('true')->withStatus(200); + } + } + + /** + * Processes a request to cancel a password reset request. + * + * This is provided so that users can cancel a password reset request, if they made it in error or if it was not initiated by themselves. + * Processes the request from the password reset link, checking that: + * 1. The provided token is associated with an existing user account, who has a pending password reset request. + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function denyResetPassword(Request $request, Response $response, $args) { + // GET parameters + $params = $request->getQueryParams(); + + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + $loginPage = $this->ci->router->pathFor('login'); + + // Load validation rules + $schema = new RequestSchema('schema://requests/deny-password.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and halt on validation errors. Since this is a GET request, we need to redirect on failure + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + // 400 code + redirect is perfectly fine, according to user Dilaz in #laravel + return $response->withRedirect($loginPage, 400); + } + + $passwordReset = $this->ci->repoPasswordReset->cancel($data['token']); + + if (!$passwordReset) { + $ms->addMessageTranslated('danger', 'PASSWORD.FORGET.INVALID'); + return $response->withRedirect($loginPage, 400); + } + + $ms->addMessageTranslated('success', 'PASSWORD.FORGET.REQUEST_CANNED'); + return $response->withRedirect($loginPage); + } + + /** + * Processes a request to email a forgotten password reset link to the user. + * + * Processes the request from the form on the "forgot password" page, checking that: + * 1. The rate limit for this type of request is being observed. + * 2. The provided email address belongs to a registered account; + * 3. The submitted data is valid. + * Note that we have removed the requirement that a password reset request not already be in progress. + * This is because we need to allow users to re-request a reset, even if they lose the first reset email. + * This route is "public access". + * Request type: POST + * @odo require additional user information + * @odo prevent password reset requests for root account? + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function forgotPassword(Request $request, Response $response, $args) { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // Get POST parameters + $params = $request->getParsedBody(); + + // Load the request schema + $schema = new RequestSchema('schema://requests/forgot-password.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + return $response->withStatus(400); + } + + // Throttle requests + + /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */ + $throttler = $this->ci->throttler; + + $throttleData = [ + 'email' => $data['email'] + ]; + $delay = $throttler->getDelay('password_reset_request', $throttleData); + + if ($delay > 0) { + $ms->addMessageTranslated('danger', 'RATE_LIMIT_EXCEEDED', ['delay' => $delay]); + return $response->withStatus(429); + } + + // All checks passed! log events/activities, update user, and send email + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction(function () use ($classMapper, $data, $throttler, $throttleData, $config) { + + // Log throttleable event + $throttler->logEvent('password_reset_request', $throttleData); + + // Load the user, by email address + $user = $classMapper->staticMethod('user', 'where', 'email', $data['email'])->first(); + + // Check that the email exists. + // If there is no user with that email address, we should still pretend like we succeeded, to prevent account enumeration + if ($user) { + // Try to generate a new password reset request. + // Use timeout for "reset password" + $passwordReset = $this->ci->repoPasswordReset->create($user, $config['password_reset.timeouts.reset']); + + // Create and send email + $message = new TwigMailMessage($this->ci->view, 'mail/password-reset.html.twig'); + $message->from($config['address_book.admin']) + ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name)) + ->addParams([ + 'user' => $user, + 'token' => $passwordReset->getToken(), + 'request_date' => Carbon::now()->format('Y-m-d H:i:s') + ]); + + $this->ci->mailer->send($message); + } + }); + + $ms->addMessageTranslated('success', 'PASSWORD.FORGET.REQUEST_SENT', ['email' => $data['email']]); + return $response->withStatus(200); + } + + /** + * Returns a modal containing account terms of service. + * + * This does NOT render a complete page. Instead, it renders the HTML for the form, which can be embedded in other pages. + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function getModalAccountTos(Request $request, Response $response, $args) { + return $this->ci->view->render($response, 'modals/tos.html.twig'); + } + + /** + * Generate a random captcha, store it to the session, and return the captcha image. + * + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function imageCaptcha(Request $request, Response $response, $args) { + $captcha = new Captcha($this->ci->session, $this->ci->config['session.keys.captcha']); + $captcha->generateRandomCode(); + + return $response->withStatus(200) + ->withHeader('Content-Type', 'image/png;base64') + ->write($captcha->getImage()); + } + + /** + * Processes an account login request. + * + * Processes the request from the form on the login page, checking that: + * 1. The user is not already logged in. + * 2. The rate limit for this type of request is being observed. + * 3. Email login is enabled, if an email address was used. + * 4. The user account exists. + * 5. The user account is enabled and verified. + * 6. The user entered a valid username/email and password. + * This route, by definition, is "public access". + * Request type: POST + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function login(Request $request, Response $response, $args) { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ + $authenticator = $this->ci->authenticator; + + // Return 200 success if user is already logged in + if ($authenticator->check()) { + $ms->addMessageTranslated('warning', 'LOGIN.ALREADY_COMPLETE'); + return $response->withStatus(200); + } + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // Get POST parameters + $params = $request->getParsedBody(); + + // Load the request schema + $schema = new RequestSchema('schema://requests/login.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + return $response->withStatus(400); + } + + // Determine whether we are trying to log in with an email address or a username + $isEmail = filter_var($data['user_name'], FILTER_VALIDATE_EMAIL); + + // Throttle requests + + /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */ + $throttler = $this->ci->throttler; + + $userIdentifier = $data['user_name']; + + $throttleData = [ + 'user_identifier' => $userIdentifier + ]; + + $delay = $throttler->getDelay('sign_in_attempt', $throttleData); + if ($delay > 0) { + $ms->addMessageTranslated('danger', 'RATE_LIMIT_EXCEEDED', [ + 'delay' => $delay + ]); + return $response->withStatus(429); + } + + // Log throttleable event + $throttler->logEvent('sign_in_attempt', $throttleData); + + // If credential is an email address, but email login is not enabled, raise an error. + // Note that we do this after logging throttle event, so this error counts towards throttling limit. + if ($isEmail && !$config['site.login.enable_email']) { + $ms->addMessageTranslated('danger', 'USER_OR_PASS_INVALID'); + return $response->withStatus(403); + } + + // Try to authenticate the user. Authenticator will throw an exception on failure. + /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ + $authenticator = $this->ci->authenticator; + + $currentUser = $authenticator->attempt(($isEmail ? 'email' : 'user_name'), $userIdentifier, $data['password'], $data['rememberme']); + + $ms->addMessageTranslated('success', 'WELCOME', $currentUser->export()); + + // Set redirect, if relevant + $redirectOnLogin = $this->ci->get('redirect.onLogin'); + + return $redirectOnLogin($request, $response, $args); + } + + /** + * Log the user out completely, including destroying any "remember me" token. + * + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function logout(Request $request, Response $response, $args) { + // Destroy the session + $this->ci->authenticator->logout(); + + // Return to home page + $config = $this->ci->config; + return $response->withStatus(302)->withHeader('Location', $config['site.uri.public']); + } + + /** + * Render the "forgot password" page. + * + * This creates a simple form to allow users who forgot their password to have a time-limited password reset link emailed to them. + * By default, this is a "public page" (does not require authentication). + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function pageForgotPassword(Request $request, Response $response, $args) { + // Load validation rules + $schema = new RequestSchema('schema://requests/forgot-password.yaml'); + $validator = new JqueryValidationAdapter($schema, $this->ci->translator); + + return $this->ci->view->render($response, 'pages/forgot-password.html.twig', [ + 'page' => [ + 'validators' => [ + 'forgot_password' => $validator->rules('json', FALSE) + ] + ] + ]); + } + + + /** + * Render the account registration page for UserFrosting. + * + * This allows new (non-authenticated) users to create a new account for themselves on your website (if enabled). + * By definition, this is a "public page" (does not require authentication). + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function pageRegister(Request $request, Response $response, $args) { + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + if (!$config['site.registration.enabled']) { + throw new NotFoundException($request, $response); + } + + /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ + $authenticator = $this->ci->authenticator; + + // Redirect if user is already logged in + if ($authenticator->check()) { + $redirect = $this->ci->get('redirect.onAlreadyLoggedIn'); + + return $redirect($request, $response, $args); + } + + // Load validation rules + $schema = new RequestSchema('schema://requests/register.yaml'); + $validatorRegister = new JqueryValidationAdapter($schema, $this->ci->translator); + + return $this->ci->view->render($response, 'pages/register.html.twig', [ + 'page' => [ + 'validators' => [ + 'register' => $validatorRegister->rules('json', FALSE) + ] + ] + ]); + } + + /** + * Render the "resend verification email" page. + * + * This is a form that allows users who lost their account verification link to have the link resent to their email address. + * By default, this is a "public page" (does not require authentication). + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function pageResendVerification(Request $request, Response $response, $args) { + // Load validation rules + $schema = new RequestSchema('schema://requests/resend-verification.yaml'); + $validator = new JqueryValidationAdapter($schema, $this->ci->translator); + + return $this->ci->view->render($response, 'pages/resend-verification.html.twig', [ + 'page' => [ + 'validators' => [ + 'resend_verification' => $validator->rules('json', FALSE) + ] + ] + ]); + } + + /** + * Reset password page. + * + * Renders the new password page for password reset requests. + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function pageResetPassword(Request $request, Response $response, $args) { + // Insert the user's secret token from the link into the password reset form + $params = $request->getQueryParams(); + + // Load validation rules - note this uses the same schema as "set password" + $schema = new RequestSchema('schema://requests/set-password.yaml'); + $validator = new JqueryValidationAdapter($schema, $this->ci->translator); + + return $this->ci->view->render($response, 'pages/reset-password.html.twig', [ + 'page' => [ + 'validators' => [ + 'set_password' => $validator->rules('json', FALSE) + ] + ], + 'token' => isset($params['token']) ? $params['token'] : '', + ]); + } + + /** + * Render the "set password" page. + * + * Renders the page where new users who have had accounts created for them by another user, can set their password. + * By default, this is a "public page" (does not require authentication). + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function pageSetPassword(Request $request, Response $response, $args) { + // Insert the user's secret token from the link into the password set form + $params = $request->getQueryParams(); + + // Load validation rules + $schema = new RequestSchema('schema://requests/set-password.yaml'); + $validator = new JqueryValidationAdapter($schema, $this->ci->translator); + + return $this->ci->view->render($response, 'pages/set-password.html.twig', [ + 'page' => [ + 'validators' => [ + 'set_password' => $validator->rules('json', FALSE) + ] + ], + 'token' => isset($params['token']) ? $params['token'] : '', + ]); + } + + /** + * Account settings page. + * + * Provides a form for users to modify various properties of their account, such as name, email, locale, etc. + * Any fields that the user does not have permission to modify will be automatically disabled. + * This page requires authentication. + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function pageSettings(Request $request, Response $response, $args) { + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_account_settings')) { + throw new ForbiddenException(); + } + + // Load validation rules + $schema = new RequestSchema('schema://requests/account-settings.yaml'); + $validatorAccountSettings = new JqueryValidationAdapter($schema, $this->ci->translator); + + $schema = new RequestSchema('schema://requests/profile-settings.yaml'); + $validatorProfileSettings = new JqueryValidationAdapter($schema, $this->ci->translator); + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // Get a list of all locales + $locales = $config->getDefined('site.locales.available'); + + return $this->ci->view->render($response, 'pages/account-settings.html.twig', [ + 'locales' => $locales, + 'page' => [ + 'validators' => [ + 'account_settings' => $validatorAccountSettings->rules('json', FALSE), + 'profile_settings' => $validatorProfileSettings->rules('json', FALSE) + ], + 'visibility' => ($authorizer->checkAccess($currentUser, 'update_account_settings') ? '' : 'disabled') + ] + ]); + } + + /** + * Render the account sign-in page for UserFrosting. + * + * This allows existing users to sign in. + * By definition, this is a "public page" (does not require authentication). + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function pageSignIn(Request $request, Response $response, $args) { + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ + $authenticator = $this->ci->authenticator; + + // Redirect if user is already logged in + if ($authenticator->check()) { + $redirect = $this->ci->get('redirect.onAlreadyLoggedIn'); + + return $redirect($request, $response, $args); + } + + // Load validation rules + $schema = new RequestSchema('schema://requests/login.yaml'); + $validatorLogin = new JqueryValidationAdapter($schema, $this->ci->translator); + + return $this->ci->view->render($response, 'pages/sign-in.html.twig', [ + 'page' => [ + 'validators' => [ + 'login' => $validatorLogin->rules('json', FALSE) + ] + ] + ]); + } + + /** + * Processes a request to update a user's profile information. + * + * Processes the request from the user profile settings form, checking that: + * 1. They have the necessary permissions to update the posted field(s); + * 2. The submitted data is valid. + * This route requires authentication. + * Request type: POST + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function profile(Request $request, Response $response, $args) { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access control for entire resource - check that the current user has permission to modify themselves + // See recipe "per-field access control" for dynamic fine-grained control over which properties a user can modify. + if (!$authorizer->checkAccess($currentUser, 'update_account_settings')) { + $ms->addMessageTranslated('danger', 'ACCOUNT.ACCESS_DENIED'); + return $response->withStatus(403); + } + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // POST parameters + $params = $request->getParsedBody(); + + // Load the request schema + $schema = new RequestSchema('schema://requests/profile-settings.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + $error = FALSE; + + // Validate, and halt on validation errors. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + $error = TRUE; + } + + // Check that locale is valid + $locales = $config->getDefined('site.locales.available'); + if (!array_key_exists($data['locale'], $locales)) { + $ms->addMessageTranslated('danger', 'LOCALE.INVALID', $data); + $error = TRUE; + } + + if ($error) { + return $response->withStatus(400); + } + + // Looks good, let's update with new values! + // Note that only fields listed in `profile-settings.yaml` will be permitted in $data, so this prevents the user from updating all columns in the DB + $currentUser->fill($data); + + $currentUser->save(); + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated their profile settings.", [ + 'type' => 'update_profile_settings' + ]); + + $ms->addMessageTranslated('success', 'PROFILE.UPDATED'); + return $response->withStatus(200); + } + + /** + * Processes an new account registration request. + * + * This is throttled to prevent account enumeration, since it needs to divulge when a username/email has been used. + * Processes the request from the form on the registration page, checking that: + * 1. The honeypot was not modified; + * 2. The master account has already been created (during installation); + * 3. Account registration is enabled; + * 4. The user is not already logged in; + * 5. Valid information was entered; + * 6. The captcha, if enabled, is correct; + * 7. The username and email are not already taken. + * Automatically sends an activation link upon success, if account activation is enabled. + * This route is "public access". + * Request type: POST + * Returns the User Object for the user record that was created. + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function register(Request $request, Response $response, $args) { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // Get POST parameters: user_name, first_name, last_name, email, password, passwordc, captcha, spiderbro, csrf_token + $params = $request->getParsedBody(); + + // Check the honeypot. 'spiderbro' is not a real field, it is hidden on the main page and must be submitted with its default value for this to be processed. + if (!isset($params['spiderbro']) || $params['spiderbro'] != 'http://') { + throw new SpammyRequestException('Possible spam received:' . print_r($params, TRUE)); + } + + // Security measure: do not allow registering new users until the master account has been created. + if (!$classMapper->staticMethod('user', 'find', $config['reserved_user_ids.master'])) { + $ms->addMessageTranslated('danger', 'ACCOUNT.MASTER_NOT_EXISTS'); + return $response->withStatus(403); + } + + // Check if registration is currently enabled + if (!$config['site.registration.enabled']) { + $ms->addMessageTranslated('danger', 'REGISTRATION.DISABLED'); + return $response->withStatus(403); + } + + /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ + $authenticator = $this->ci->authenticator; + + // Prevent the user from registering if he/she is already logged in + if ($authenticator->check()) { + $ms->addMessageTranslated('danger', 'REGISTRATION.LOGOUT'); + return $response->withStatus(403); + } + + // Load the request schema + $schema = new RequestSchema('schema://requests/register.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + $error = FALSE; + + // Validate request data + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + $error = TRUE; + } + + /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */ + $throttler = $this->ci->throttler; + $delay = $throttler->getDelay('registration_attempt'); + + // Throttle requests + if ($delay > 0) { + return $response->withStatus(429); + } + + // Check if username or email already exists + if ($classMapper->staticMethod('user', 'findUnique', $data['user_name'], 'user_name')) { + $ms->addMessageTranslated('danger', 'USERNAME.IN_USE', $data); + $error = TRUE; + } + + if ($classMapper->staticMethod('user', 'findUnique', $data['email'], 'email')) { + $ms->addMessageTranslated('danger', 'EMAIL.IN_USE', $data); + $error = TRUE; + } + + // Check captcha, if required + if ($config['site.registration.captcha']) { + $captcha = new Captcha($this->ci->session, $this->ci->config['session.keys.captcha']); + if (!$data['captcha'] || !$captcha->verifyCode($data['captcha'])) { + $ms->addMessageTranslated('danger', 'CAPTCHA.FAIL'); + $error = TRUE; + } + } + + if ($error) { + return $response->withStatus(400); + } + + // Remove captcha, password confirmation from object data after validation + unset($data['captcha']); + unset($data['passwordc']); + + if ($config['site.registration.require_email_verification']) { + $data['flag_verified'] = FALSE; + } else { + $data['flag_verified'] = TRUE; + } + + // Load default group + $groupSlug = $config['site.registration.user_defaults.group']; + $defaultGroup = $classMapper->staticMethod('group', 'where', 'slug', $groupSlug)->first(); + + if (!$defaultGroup) { + $e = new HttpException("Account registration is not working because the default group '$groupSlug' does not exist."); + $e->addUserMessage('REGISTRATION.BROKEN'); + throw $e; + } + + // Set default group + $data['group_id'] = $defaultGroup->id; + + // Set default locale + $data['locale'] = $config['site.registration.user_defaults.locale']; + + // Hash password + $data['password'] = Password::hash($data['password']); + + // All checks passed! log events/activities, create user, and send verification email (if required) + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction(function () use ($classMapper, $data, $ms, $config, $throttler) { + // Log throttleable event + $throttler->logEvent('registration_attempt'); + + // Create the user + $user = $classMapper->createInstance('user', $data); + + // Store new user to database + $user->save(); + + // Create activity record + $this->ci->userActivityLogger->info("User {$user->user_name} registered for a new account.", [ + 'type' => 'sign_up', + 'user_id' => $user->id + ]); + + // Load default roles + $defaultRoleSlugs = $classMapper->staticMethod('role', 'getDefaultSlugs'); + $defaultRoles = $classMapper->staticMethod('role', 'whereIn', 'slug', $defaultRoleSlugs)->get(); + $defaultRoleIds = $defaultRoles->pluck('id')->all(); + + // Attach default roles + $user->roles()->attach($defaultRoleIds); + + // Verification email + if ($config['site.registration.require_email_verification']) { + // Try to generate a new verification request + $verification = $this->ci->repoVerification->create($user, $config['verification.timeout']); + + // Create and send verification email + $message = new TwigMailMessage($this->ci->view, 'mail/verify-account.html.twig'); + + $message->from($config['address_book.admin']) + ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name)) + ->addParams([ + 'user' => $user, + 'token' => $verification->getToken() + ]); + + $this->ci->mailer->send($message); + + $ms->addMessageTranslated('success', 'REGISTRATION.COMPLETE_TYPE2', $user->toArray()); + } else { + // No verification required + $ms->addMessageTranslated('success', 'REGISTRATION.COMPLETE_TYPE1'); + } + }); + + return $response->withStatus(200); + } + + /** + * Processes a request to resend the verification email for a new user account. + * + * Processes the request from the resend verification email form, checking that: + * 1. The rate limit on this type of request is observed; + * 2. The provided email is associated with an existing user account; + * 3. The user account is not already verified; + * 4. The submitted data is valid. + * This route is "public access". + * Request type: POST + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function resendVerification(Request $request, Response $response, $args) { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // Get POST parameters + $params = $request->getParsedBody(); + + // Load the request schema + $schema = new RequestSchema('schema://requests/resend-verification.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + return $response->withStatus(400); + } + + // Throttle requests + + /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */ + $throttler = $this->ci->throttler; + + $throttleData = [ + 'email' => $data['email'] + ]; + $delay = $throttler->getDelay('verification_request', $throttleData); + + if ($delay > 0) { + $ms->addMessageTranslated('danger', 'RATE_LIMIT_EXCEEDED', ['delay' => $delay]); + return $response->withStatus(429); + } + + // All checks passed! log events/activities, create user, and send verification email (if required) + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction(function () use ($classMapper, $data, $throttler, $throttleData, $config) { + // Log throttleable event + $throttler->logEvent('verification_request', $throttleData); + + // Load the user, by email address + $user = $classMapper->staticMethod('user', 'where', 'email', $data['email'])->first(); + + // Check that the user exists and is not already verified. + // If there is no user with that email address, or the user exists and is already verified, + // we pretend like we succeeded to prevent account enumeration + if ($user && $user->flag_verified != '1') { + // We're good to go - record user activity and send the email + $verification = $this->ci->repoVerification->create($user, $config['verification.timeout']); + + // Create and send verification email + $message = new TwigMailMessage($this->ci->view, 'mail/resend-verification.html.twig'); + + $message->from($config['address_book.admin']) + ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name)) + ->addParams([ + 'user' => $user, + 'token' => $verification->getToken() + ]); + + $this->ci->mailer->send($message); + } + }); + + $ms->addMessageTranslated('success', 'ACCOUNT.VERIFICATION.NEW_LINK_SENT', ['email' => $data['email']]); + return $response->withStatus(200); + } + + /** + * Processes a request to set the password for a new or current user. + * + * Processes the request from the password create/reset form, which should have the secret token embedded in it, checking that: + * 1. The provided secret token is associated with an existing user account; + * 2. The user has a password set/reset request in progress; + * 3. The token has not expired; + * 4. The submitted data (new password) is valid. + * This route is "public access". + * Request type: POST + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function setPassword(Request $request, Response $response, $args) { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // Get POST parameters + $params = $request->getParsedBody(); + + // Load the request schema + $schema = new RequestSchema('schema://requests/set-password.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + return $response->withStatus(400); + } + + $forgotPasswordPage = $this->ci->router->pathFor('forgot-password'); + + // Ok, try to complete the request with the specified token and new password + $passwordReset = $this->ci->repoPasswordReset->complete($data['token'], [ + 'password' => $data['password'] + ]); + + if (!$passwordReset) { + $ms->addMessageTranslated('danger', 'PASSWORD.FORGET.INVALID', ['url' => $forgotPasswordPage]); + return $response->withStatus(400); + } + + $ms->addMessageTranslated('success', 'PASSWORD.UPDATED'); + + /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ + $authenticator = $this->ci->authenticator; + + // Log out any existing user, and create a new session + if ($authenticator->check()) { + $authenticator->logout(); + } + + // Auto-login the user (without "remember me") + $user = $passwordReset->user; + $authenticator->login($user); + + $ms->addMessageTranslated('success', 'WELCOME', $user->export()); + return $response->withStatus(200); + } + + /** + * Processes a request to update a user's account information. + * + * Processes the request from the user account settings form, checking that: + * 1. The user correctly input their current password; + * 2. They have the necessary permissions to update the posted field(s); + * 3. The submitted data is valid. + * This route requires authentication. + * Request type: POST + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function settings(Request $request, Response $response, $args) { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access control for entire resource - check that the current user has permission to modify themselves + // See recipe "per-field access control" for dynamic fine-grained control over which properties a user can modify. + if (!$authorizer->checkAccess($currentUser, 'update_account_settings')) { + $ms->addMessageTranslated('danger', 'ACCOUNT.ACCESS_DENIED'); + return $response->withStatus(403); + } + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // POST parameters + $params = $request->getParsedBody(); + + // Load the request schema + $schema = new RequestSchema('schema://requests/account-settings.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + $error = FALSE; + + // Validate, and halt on validation errors. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + $error = TRUE; + } + + // Confirm current password + if (!isset($data['passwordcheck']) || !Password::verify($data['passwordcheck'], $currentUser->password)) { + $ms->addMessageTranslated('danger', 'PASSWORD.INVALID'); + $error = TRUE; + } + + // Remove password check, password confirmation from object data after validation + unset($data['passwordcheck']); + unset($data['passwordc']); + + // If new email was submitted, check that the email address is not in use + if (isset($data['email']) && $data['email'] != $currentUser->email && $classMapper->staticMethod('user', 'findUnique', $data['email'], 'email')) { + $ms->addMessageTranslated('danger', 'EMAIL.IN_USE', $data); + $error = TRUE; + } + + if ($error) { + return $response->withStatus(400); + } + + // Hash new password, if specified + if (isset($data['password']) && !empty($data['password'])) { + $data['password'] = Password::hash($data['password']); + } else { + // Do not pass to model if no password is specified + unset($data['password']); + } + + // Looks good, let's update with new values! + // Note that only fields listed in `account-settings.yaml` will be permitted in $data, so this prevents the user from updating all columns in the DB + $currentUser->fill($data); + + $currentUser->save(); + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated their account settings.", [ + 'type' => 'update_account_settings' + ]); + + $ms->addMessageTranslated('success', 'ACCOUNT.SETTINGS.UPDATED'); + return $response->withStatus(200); + } + + /** + * Suggest an available username for a specified first/last name. + * + * This route is "public access". + * Request type: GET + * @odo Can this route be abused for account enumeration? If so we should throttle it as well. + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function suggestUsername(Request $request, Response $response, $args) { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + $suggestion = AccountUtil::randomUniqueUsername($classMapper, 50, 10); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $response->withJson([ + 'user_name' => $suggestion + ], 200, JSON_PRETTY_PRINT); + } + + /** + * Processes an new email verification request. + * + * Processes the request from the email verification link that was emailed to the user, checking that: + * 1. The token provided matches a user in the database; + * 2. The user account is not already verified; + * This route is "public access". + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function verify(Request $request, Response $response, $args) { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + $loginPage = $this->ci->router->pathFor('login'); + + // GET parameters + $params = $request->getQueryParams(); + + // Load request schema + $schema = new RequestSchema('schema://requests/account-verify.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and halt on validation errors. This is a GET request, so we redirect on validation error. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + // 400 code + redirect is perfectly fine, according to user Dilaz in #laravel + return $response->withRedirect($loginPage, 400); + } + + $verification = $this->ci->repoVerification->complete($data['token']); + + if (!$verification) { + $ms->addMessageTranslated('danger', 'ACCOUNT.VERIFICATION.TOKEN_NOT_FOUND'); + return $response->withRedirect($loginPage, 400); + } + + $ms->addMessageTranslated('success', 'ACCOUNT.VERIFICATION.COMPLETE'); + + // Forward to login page + return $response->withRedirect($loginPage); + } +} diff --git a/main/app/sprinkles/account/src/Controller/Exception/SpammyRequestException.php b/main/app/sprinkles/account/src/Controller/Exception/SpammyRequestException.php index d66a16c..1f0cf4a 100644 --- a/main/app/sprinkles/account/src/Controller/Exception/SpammyRequestException.php +++ b/main/app/sprinkles/account/src/Controller/Exception/SpammyRequestException.php @@ -1,21 +1,21 @@ -schema->hasTable('activities')) { - $this->schema->create('activities', function (Blueprint $table) { - $table->increments('id'); - $table->string('ip_address', 45)->nullable(); - $table->integer('user_id')->unsigned(); - $table->string('type', 255)->comment('An identifier used to track the type of activity.'); - $table->timestamp('occurred_at')->nullable(); - $table->text('description')->nullable(); - - $table->engine = 'InnoDB'; - $table->collation = 'utf8_unicode_ci'; - $table->charset = 'utf8'; - //$table->foreign('user_id')->references('id')->on('users'); - $table->index('user_id'); - }); - } - } - - /** - * {@inheritDoc} - */ - public function down() { - $this->schema->drop('activities'); - } +schema->hasTable('activities')) { + $this->schema->create('activities', function (Blueprint $table) { + $table->increments('id'); + $table->string('ip_address', 45)->nullable(); + $table->integer('user_id')->unsigned(); + $table->string('type', 255)->comment('An identifier used to track the type of activity.'); + $table->timestamp('occurred_at')->nullable(); + $table->text('description')->nullable(); + + $table->engine = 'InnoDB'; + $table->collation = 'utf8_unicode_ci'; + $table->charset = 'utf8'; + //$table->foreign('user_id')->references('id')->on('users'); + $table->index('user_id'); + }); + } + } + + /** + * {@inheritDoc} + */ + public function down() { + $this->schema->drop('activities'); + } } \ No newline at end of file diff --git a/main/app/sprinkles/account/src/Database/Migrations/v400/GroupsTable.php b/main/app/sprinkles/account/src/Database/Migrations/v400/GroupsTable.php index 07583af..d8498f4 100644 --- a/main/app/sprinkles/account/src/Database/Migrations/v400/GroupsTable.php +++ b/main/app/sprinkles/account/src/Database/Migrations/v400/GroupsTable.php @@ -1,81 +1,81 @@ -schema->hasTable('groups')) { - $this->schema->create('groups', function (Blueprint $table) { - $table->increments('id'); - $table->string('slug'); - $table->string('name'); - $table->text('description')->nullable(); - $table->string('icon', 100)->nullable(FALSE)->default('fa fa-user')->comment('The icon representing users in this group.'); - $table->timestamps(); - - $table->engine = 'InnoDB'; - $table->collation = 'utf8_unicode_ci'; - $table->charset = 'utf8'; - $table->unique('slug'); - $table->index('slug'); - }); - - // Add default groups - $groups = [ - 'terran' => new Group([ - 'slug' => 'terran', - 'name' => 'Terran', - 'description' => 'The terrans are a young species with psionic potential. The terrans of the Koprulu sector descend from the survivors of a disastrous 23rd century colonization mission from Earth.', - 'icon' => 'sc sc-terran' - ]), - 'zerg' => new Group([ - 'slug' => 'zerg', - 'name' => 'Zerg', - 'description' => 'Dedicated to the pursuit of genetic perfection, the zerg relentlessly hunt down and assimilate advanced species across the galaxy, incorporating useful genetic code into their own.', - 'icon' => 'sc sc-zerg' - ]), - 'protoss' => new Group([ - 'slug' => 'protoss', - 'name' => 'Protoss', - 'description' => 'The protoss, a.k.a. the Firstborn, are a sapient humanoid race native to Aiur. Their advanced technology complements and enhances their psionic mastery.', - 'icon' => 'sc sc-protoss' - ]) - ]; - - foreach ($groups as $slug => $group) { - $group->save(); - } - } - } - - /** - * {@inheritDoc} - */ - public function down() { - $this->schema->drop('groups'); - } -} +schema->hasTable('groups')) { + $this->schema->create('groups', function (Blueprint $table) { + $table->increments('id'); + $table->string('slug'); + $table->string('name'); + $table->text('description')->nullable(); + $table->string('icon', 100)->nullable(FALSE)->default('fa fa-user')->comment('The icon representing users in this group.'); + $table->timestamps(); + + $table->engine = 'InnoDB'; + $table->collation = 'utf8_unicode_ci'; + $table->charset = 'utf8'; + $table->unique('slug'); + $table->index('slug'); + }); + + // Add default groups + $groups = [ + 'terran' => new Group([ + 'slug' => 'terran', + 'name' => 'Terran', + 'description' => 'The terrans are a young species with psionic potential. The terrans of the Koprulu sector descend from the survivors of a disastrous 23rd century colonization mission from Earth.', + 'icon' => 'sc sc-terran' + ]), + 'zerg' => new Group([ + 'slug' => 'zerg', + 'name' => 'Zerg', + 'description' => 'Dedicated to the pursuit of genetic perfection, the zerg relentlessly hunt down and assimilate advanced species across the galaxy, incorporating useful genetic code into their own.', + 'icon' => 'sc sc-zerg' + ]), + 'protoss' => new Group([ + 'slug' => 'protoss', + 'name' => 'Protoss', + 'description' => 'The protoss, a.k.a. the Firstborn, are a sapient humanoid race native to Aiur. Their advanced technology complements and enhances their psionic mastery.', + 'icon' => 'sc sc-protoss' + ]) + ]; + + foreach ($groups as $slug => $group) { + $group->save(); + } + } + } + + /** + * {@inheritDoc} + */ + public function down() { + $this->schema->drop('groups'); + } +} diff --git a/main/app/sprinkles/account/src/Database/Migrations/v400/PasswordResetsTable.php b/main/app/sprinkles/account/src/Database/Migrations/v400/PasswordResetsTable.php index 47eb00d..932ab47 100644 --- a/main/app/sprinkles/account/src/Database/Migrations/v400/PasswordResetsTable.php +++ b/main/app/sprinkles/account/src/Database/Migrations/v400/PasswordResetsTable.php @@ -1,56 +1,56 @@ -schema->hasTable('password_resets')) { - $this->schema->create('password_resets', function (Blueprint $table) { - $table->increments('id'); - $table->integer('user_id')->unsigned(); - $table->string('hash'); - $table->boolean('completed')->default(0); - $table->timestamp('expires_at')->nullable(); - $table->timestamp('completed_at')->nullable(); - $table->timestamps(); - - $table->engine = 'InnoDB'; - $table->collation = 'utf8_unicode_ci'; - $table->charset = 'utf8'; - //$table->foreign('user_id')->references('id')->on('users'); - $table->index('user_id'); - $table->index('hash'); - }); - } - } - - /** - * {@inheritDoc} - */ - public function down() { - $this->schema->drop('password_resets'); - } -} +schema->hasTable('password_resets')) { + $this->schema->create('password_resets', function (Blueprint $table) { + $table->increments('id'); + $table->integer('user_id')->unsigned(); + $table->string('hash'); + $table->boolean('completed')->default(0); + $table->timestamp('expires_at')->nullable(); + $table->timestamp('completed_at')->nullable(); + $table->timestamps(); + + $table->engine = 'InnoDB'; + $table->collation = 'utf8_unicode_ci'; + $table->charset = 'utf8'; + //$table->foreign('user_id')->references('id')->on('users'); + $table->index('user_id'); + $table->index('hash'); + }); + } + } + + /** + * {@inheritDoc} + */ + public function down() { + $this->schema->drop('password_resets'); + } +} diff --git a/main/app/sprinkles/account/src/Database/Migrations/v400/PermissionRolesTable.php b/main/app/sprinkles/account/src/Database/Migrations/v400/PermissionRolesTable.php index 8e06cd6..dca6639 100644 --- a/main/app/sprinkles/account/src/Database/Migrations/v400/PermissionRolesTable.php +++ b/main/app/sprinkles/account/src/Database/Migrations/v400/PermissionRolesTable.php @@ -1,54 +1,54 @@ -schema->hasTable('permission_roles')) { - $this->schema->create('permission_roles', function (Blueprint $table) { - $table->integer('permission_id')->unsigned(); - $table->integer('role_id')->unsigned(); - $table->timestamps(); - - $table->engine = 'InnoDB'; - $table->collation = 'utf8_unicode_ci'; - $table->charset = 'utf8'; - $table->primary(['permission_id', 'role_id']); - //$table->foreign('permission_id')->references('id')->on('permissions'); - //$table->foreign('role_id')->references('id')->on('roles'); - $table->index('permission_id'); - $table->index('role_id'); - }); - } - } - - /** - * {@inheritDoc} - */ - public function down() { - $this->schema->drop('permission_roles'); - } -} +schema->hasTable('permission_roles')) { + $this->schema->create('permission_roles', function (Blueprint $table) { + $table->integer('permission_id')->unsigned(); + $table->integer('role_id')->unsigned(); + $table->timestamps(); + + $table->engine = 'InnoDB'; + $table->collation = 'utf8_unicode_ci'; + $table->charset = 'utf8'; + $table->primary(['permission_id', 'role_id']); + //$table->foreign('permission_id')->references('id')->on('permissions'); + //$table->foreign('role_id')->references('id')->on('roles'); + $table->index('permission_id'); + $table->index('role_id'); + }); + } + } + + /** + * {@inheritDoc} + */ + public function down() { + $this->schema->drop('permission_roles'); + } +} diff --git a/main/app/sprinkles/account/src/Database/Migrations/v400/PermissionsTable.php b/main/app/sprinkles/account/src/Database/Migrations/v400/PermissionsTable.php index bef8cdd..efc014b 100644 --- a/main/app/sprinkles/account/src/Database/Migrations/v400/PermissionsTable.php +++ b/main/app/sprinkles/account/src/Database/Migrations/v400/PermissionsTable.php @@ -1,260 +1,260 @@ -schema->hasTable('permissions')) { - $this->schema->create('permissions', function (Blueprint $table) { - $table->increments('id'); - $table->string('slug')->comment('A code that references a specific action or URI that an assignee of this permission has access to.'); - $table->string('name'); - $table->text('conditions')->comment('The conditions under which members of this group have access to this hook.'); - $table->text('description')->nullable(); - $table->timestamps(); - - $table->engine = 'InnoDB'; - $table->collation = 'utf8_unicode_ci'; - $table->charset = 'utf8'; - }); - } - } - - /** - * {@inheritDoc} - */ - public function down() { - $this->schema->drop('permissions'); - } - - /** - * {@inheritDoc} - */ - public function seed() { - // Skip this if table is not empty - if (Permission::count() == 0) { - - $defaultRoleIds = [ - 'user' => Role::where('slug', 'user')->first()->id, - 'group-admin' => Role::where('slug', 'group-admin')->first()->id, - 'site-admin' => Role::where('slug', 'site-admin')->first()->id - ]; - - // Add default permissions - $permissions = [ - 'create_group' => new Permission([ - 'slug' => 'create_group', - 'name' => 'Create group', - 'conditions' => 'always()', - 'description' => 'Create a new group.' - ]), - 'create_user' => new Permission([ - 'slug' => 'create_user', - 'name' => 'Create user', - 'conditions' => 'always()', - 'description' => 'Create a new user in your own group and assign default roles.' - ]), - 'create_user_field' => new Permission([ - 'slug' => 'create_user_field', - 'name' => 'Set new user group', - 'conditions' => "subset(fields,['group'])", - 'description' => 'Set the group when creating a new user.' - ]), - 'delete_group' => new Permission([ - 'slug' => 'delete_group', - 'name' => 'Delete group', - 'conditions' => "always()", - 'description' => 'Delete a group.' - ]), - 'delete_user' => new Permission([ - 'slug' => 'delete_user', - 'name' => 'Delete user', - 'conditions' => "!has_role(user.id,{$defaultRoleIds['site-admin']}) && !is_master(user.id)", - 'description' => 'Delete users who are not Site Administrators.' - ]), - 'update_account_settings' => new Permission([ - 'slug' => 'update_account_settings', - 'name' => 'Edit user', - 'conditions' => 'always()', - 'description' => 'Edit your own account settings.' - ]), - 'update_group_field' => new Permission([ - 'slug' => 'update_group_field', - 'name' => 'Edit group', - 'conditions' => 'always()', - 'description' => 'Edit basic properties of any group.' - ]), - 'update_user_field' => new Permission([ - 'slug' => 'update_user_field', - 'name' => 'Edit user', - 'conditions' => "!has_role(user.id,{$defaultRoleIds['site-admin']}) && subset(fields,['name','email','locale','group','flag_enabled','flag_verified','password'])", - 'description' => 'Edit users who are not Site Administrators.' - ]), - 'update_user_field_group' => new Permission([ - 'slug' => 'update_user_field', - 'name' => 'Edit group user', - 'conditions' => "equals_num(self.group_id,user.group_id) && !is_master(user.id) && !has_role(user.id,{$defaultRoleIds['site-admin']}) && (!has_role(user.id,{$defaultRoleIds['group-admin']}) || equals_num(self.id,user.id)) && subset(fields,['name','email','locale','flag_enabled','flag_verified','password'])", - 'description' => 'Edit users in your own group who are not Site or Group Administrators, except yourself.' - ]), - 'uri_account_settings' => new Permission([ - 'slug' => 'uri_account_settings', - 'name' => 'Account settings page', - 'conditions' => 'always()', - 'description' => 'View the account settings page.' - ]), - 'uri_activities' => new Permission([ - 'slug' => 'uri_activities', - 'name' => 'Activity monitor', - 'conditions' => 'always()', - 'description' => 'View a list of all activities for all users.' - ]), - 'uri_dashboard' => new Permission([ - 'slug' => 'uri_dashboard', - 'name' => 'Admin dashboard', - 'conditions' => 'always()', - 'description' => 'View the administrative dashboard.' - ]), - 'uri_group' => new Permission([ - 'slug' => 'uri_group', - 'name' => 'View group', - 'conditions' => 'always()', - 'description' => 'View the group page of any group.' - ]), - 'uri_group_own' => new Permission([ - 'slug' => 'uri_group', - 'name' => 'View own group', - 'conditions' => 'equals_num(self.group_id,group.id)', - 'description' => 'View the group page of your own group.' - ]), - 'uri_groups' => new Permission([ - 'slug' => 'uri_groups', - 'name' => 'Group management page', - 'conditions' => 'always()', - 'description' => 'View a page containing a list of groups.' - ]), - 'uri_user' => new Permission([ - 'slug' => 'uri_user', - 'name' => 'View user', - 'conditions' => 'always()', - 'description' => 'View the user page of any user.' - ]), - 'uri_user_in_group' => new Permission([ - 'slug' => 'uri_user', - 'name' => 'View user', - 'conditions' => "equals_num(self.group_id,user.group_id) && !is_master(user.id) && !has_role(user.id,{$defaultRoleIds['site-admin']}) && (!has_role(user.id,{$defaultRoleIds['group-admin']}) || equals_num(self.id,user.id))", - 'description' => 'View the user page of any user in your group, except the master user and Site and Group Administrators (except yourself).' - ]), - 'uri_users' => new Permission([ - 'slug' => 'uri_users', - 'name' => 'User management page', - 'conditions' => 'always()', - 'description' => 'View a page containing a table of users.' - ]), - 'view_group_field' => new Permission([ - 'slug' => 'view_group_field', - 'name' => 'View group', - 'conditions' => "in(property,['name','icon','slug','description','users'])", - 'description' => 'View certain properties of any group.' - ]), - 'view_group_field_own' => new Permission([ - 'slug' => 'view_group_field', - 'name' => 'View group', - 'conditions' => "equals_num(self.group_id,group.id) && in(property,['name','icon','slug','description','users'])", - 'description' => 'View certain properties of your own group.' - ]), - 'view_user_field' => new Permission([ - 'slug' => 'view_user_field', - 'name' => 'View user', - 'conditions' => "in(property,['user_name','name','email','locale','theme','roles','group','activities'])", - 'description' => 'View certain properties of any user.' - ]), - 'view_user_field_group' => new Permission([ - 'slug' => 'view_user_field', - 'name' => 'View user', - 'conditions' => "equals_num(self.group_id,user.group_id) && !is_master(user.id) && !has_role(user.id,{$defaultRoleIds['site-admin']}) && (!has_role(user.id,{$defaultRoleIds['group-admin']}) || equals_num(self.id,user.id)) && in(property,['user_name','name','email','locale','roles','group','activities'])", - 'description' => 'View certain properties of any user in your own group, except the master user and Site and Group Administrators (except yourself).' - ]) - ]; - - foreach ($permissions as $slug => $permission) { - $permission->save(); - } - - // Add default mappings to permissions - $roleUser = Role::where('slug', 'user')->first(); - if ($roleUser) { - $roleUser->permissions()->sync([ - $permissions['update_account_settings']->id, - $permissions['uri_account_settings']->id, - $permissions['uri_dashboard']->id - ]); - } - - $roleSiteAdmin = Role::where('slug', 'site-admin')->first(); - if ($roleSiteAdmin) { - $roleSiteAdmin->permissions()->sync([ - $permissions['create_group']->id, - $permissions['create_user']->id, - $permissions['create_user_field']->id, - $permissions['delete_group']->id, - $permissions['delete_user']->id, - $permissions['update_user_field']->id, - $permissions['update_group_field']->id, - $permissions['uri_activities']->id, - $permissions['uri_group']->id, - $permissions['uri_groups']->id, - $permissions['uri_user']->id, - $permissions['uri_users']->id, - $permissions['view_group_field']->id, - $permissions['view_user_field']->id - ]); - } - - $roleGroupAdmin = Role::where('slug', 'group-admin')->first(); - if ($roleGroupAdmin) { - $roleGroupAdmin->permissions()->sync([ - $permissions['create_user']->id, - $permissions['update_user_field_group']->id, - $permissions['uri_group_own']->id, - $permissions['uri_user_in_group']->id, - $permissions['view_group_field_own']->id, - $permissions['view_user_field_group']->id - ]); - } - } - } -} +schema->hasTable('permissions')) { + $this->schema->create('permissions', function (Blueprint $table) { + $table->increments('id'); + $table->string('slug')->comment('A code that references a specific action or URI that an assignee of this permission has access to.'); + $table->string('name'); + $table->text('conditions')->comment('The conditions under which members of this group have access to this hook.'); + $table->text('description')->nullable(); + $table->timestamps(); + + $table->engine = 'InnoDB'; + $table->collation = 'utf8_unicode_ci'; + $table->charset = 'utf8'; + }); + } + } + + /** + * {@inheritDoc} + */ + public function down() { + $this->schema->drop('permissions'); + } + + /** + * {@inheritDoc} + */ + public function seed() { + // Skip this if table is not empty + if (Permission::count() == 0) { + + $defaultRoleIds = [ + 'user' => Role::where('slug', 'user')->first()->id, + 'group-admin' => Role::where('slug', 'group-admin')->first()->id, + 'site-admin' => Role::where('slug', 'site-admin')->first()->id + ]; + + // Add default permissions + $permissions = [ + 'create_group' => new Permission([ + 'slug' => 'create_group', + 'name' => 'Create group', + 'conditions' => 'always()', + 'description' => 'Create a new group.' + ]), + 'create_user' => new Permission([ + 'slug' => 'create_user', + 'name' => 'Create user', + 'conditions' => 'always()', + 'description' => 'Create a new user in your own group and assign default roles.' + ]), + 'create_user_field' => new Permission([ + 'slug' => 'create_user_field', + 'name' => 'Set new user group', + 'conditions' => "subset(fields,['group'])", + 'description' => 'Set the group when creating a new user.' + ]), + 'delete_group' => new Permission([ + 'slug' => 'delete_group', + 'name' => 'Delete group', + 'conditions' => "always()", + 'description' => 'Delete a group.' + ]), + 'delete_user' => new Permission([ + 'slug' => 'delete_user', + 'name' => 'Delete user', + 'conditions' => "!has_role(user.id,{$defaultRoleIds['site-admin']}) && !is_master(user.id)", + 'description' => 'Delete users who are not Site Administrators.' + ]), + 'update_account_settings' => new Permission([ + 'slug' => 'update_account_settings', + 'name' => 'Edit user', + 'conditions' => 'always()', + 'description' => 'Edit your own account settings.' + ]), + 'update_group_field' => new Permission([ + 'slug' => 'update_group_field', + 'name' => 'Edit group', + 'conditions' => 'always()', + 'description' => 'Edit basic properties of any group.' + ]), + 'update_user_field' => new Permission([ + 'slug' => 'update_user_field', + 'name' => 'Edit user', + 'conditions' => "!has_role(user.id,{$defaultRoleIds['site-admin']}) && subset(fields,['name','email','locale','group','flag_enabled','flag_verified','password'])", + 'description' => 'Edit users who are not Site Administrators.' + ]), + 'update_user_field_group' => new Permission([ + 'slug' => 'update_user_field', + 'name' => 'Edit group user', + 'conditions' => "equals_num(self.group_id,user.group_id) && !is_master(user.id) && !has_role(user.id,{$defaultRoleIds['site-admin']}) && (!has_role(user.id,{$defaultRoleIds['group-admin']}) || equals_num(self.id,user.id)) && subset(fields,['name','email','locale','flag_enabled','flag_verified','password'])", + 'description' => 'Edit users in your own group who are not Site or Group Administrators, except yourself.' + ]), + 'uri_account_settings' => new Permission([ + 'slug' => 'uri_account_settings', + 'name' => 'Account settings page', + 'conditions' => 'always()', + 'description' => 'View the account settings page.' + ]), + 'uri_activities' => new Permission([ + 'slug' => 'uri_activities', + 'name' => 'Activity monitor', + 'conditions' => 'always()', + 'description' => 'View a list of all activities for all users.' + ]), + 'uri_dashboard' => new Permission([ + 'slug' => 'uri_dashboard', + 'name' => 'Admin dashboard', + 'conditions' => 'always()', + 'description' => 'View the administrative dashboard.' + ]), + 'uri_group' => new Permission([ + 'slug' => 'uri_group', + 'name' => 'View group', + 'conditions' => 'always()', + 'description' => 'View the group page of any group.' + ]), + 'uri_group_own' => new Permission([ + 'slug' => 'uri_group', + 'name' => 'View own group', + 'conditions' => 'equals_num(self.group_id,group.id)', + 'description' => 'View the group page of your own group.' + ]), + 'uri_groups' => new Permission([ + 'slug' => 'uri_groups', + 'name' => 'Group management page', + 'conditions' => 'always()', + 'description' => 'View a page containing a list of groups.' + ]), + 'uri_user' => new Permission([ + 'slug' => 'uri_user', + 'name' => 'View user', + 'conditions' => 'always()', + 'description' => 'View the user page of any user.' + ]), + 'uri_user_in_group' => new Permission([ + 'slug' => 'uri_user', + 'name' => 'View user', + 'conditions' => "equals_num(self.group_id,user.group_id) && !is_master(user.id) && !has_role(user.id,{$defaultRoleIds['site-admin']}) && (!has_role(user.id,{$defaultRoleIds['group-admin']}) || equals_num(self.id,user.id))", + 'description' => 'View the user page of any user in your group, except the master user and Site and Group Administrators (except yourself).' + ]), + 'uri_users' => new Permission([ + 'slug' => 'uri_users', + 'name' => 'User management page', + 'conditions' => 'always()', + 'description' => 'View a page containing a table of users.' + ]), + 'view_group_field' => new Permission([ + 'slug' => 'view_group_field', + 'name' => 'View group', + 'conditions' => "in(property,['name','icon','slug','description','users'])", + 'description' => 'View certain properties of any group.' + ]), + 'view_group_field_own' => new Permission([ + 'slug' => 'view_group_field', + 'name' => 'View group', + 'conditions' => "equals_num(self.group_id,group.id) && in(property,['name','icon','slug','description','users'])", + 'description' => 'View certain properties of your own group.' + ]), + 'view_user_field' => new Permission([ + 'slug' => 'view_user_field', + 'name' => 'View user', + 'conditions' => "in(property,['user_name','name','email','locale','theme','roles','group','activities'])", + 'description' => 'View certain properties of any user.' + ]), + 'view_user_field_group' => new Permission([ + 'slug' => 'view_user_field', + 'name' => 'View user', + 'conditions' => "equals_num(self.group_id,user.group_id) && !is_master(user.id) && !has_role(user.id,{$defaultRoleIds['site-admin']}) && (!has_role(user.id,{$defaultRoleIds['group-admin']}) || equals_num(self.id,user.id)) && in(property,['user_name','name','email','locale','roles','group','activities'])", + 'description' => 'View certain properties of any user in your own group, except the master user and Site and Group Administrators (except yourself).' + ]) + ]; + + foreach ($permissions as $slug => $permission) { + $permission->save(); + } + + // Add default mappings to permissions + $roleUser = Role::where('slug', 'user')->first(); + if ($roleUser) { + $roleUser->permissions()->sync([ + $permissions['update_account_settings']->id, + $permissions['uri_account_settings']->id, + $permissions['uri_dashboard']->id + ]); + } + + $roleSiteAdmin = Role::where('slug', 'site-admin')->first(); + if ($roleSiteAdmin) { + $roleSiteAdmin->permissions()->sync([ + $permissions['create_group']->id, + $permissions['create_user']->id, + $permissions['create_user_field']->id, + $permissions['delete_group']->id, + $permissions['delete_user']->id, + $permissions['update_user_field']->id, + $permissions['update_group_field']->id, + $permissions['uri_activities']->id, + $permissions['uri_group']->id, + $permissions['uri_groups']->id, + $permissions['uri_user']->id, + $permissions['uri_users']->id, + $permissions['view_group_field']->id, + $permissions['view_user_field']->id + ]); + } + + $roleGroupAdmin = Role::where('slug', 'group-admin')->first(); + if ($roleGroupAdmin) { + $roleGroupAdmin->permissions()->sync([ + $permissions['create_user']->id, + $permissions['update_user_field_group']->id, + $permissions['uri_group_own']->id, + $permissions['uri_user_in_group']->id, + $permissions['view_group_field_own']->id, + $permissions['view_user_field_group']->id + ]); + } + } + } +} diff --git a/main/app/sprinkles/account/src/Database/Migrations/v400/PersistencesTable.php b/main/app/sprinkles/account/src/Database/Migrations/v400/PersistencesTable.php index 41378d3..c51461a 100644 --- a/main/app/sprinkles/account/src/Database/Migrations/v400/PersistencesTable.php +++ b/main/app/sprinkles/account/src/Database/Migrations/v400/PersistencesTable.php @@ -1,56 +1,56 @@ -schema->hasTable('persistences')) { - $this->schema->create('persistences', function (Blueprint $table) { - $table->increments('id'); - $table->integer('user_id')->unsigned(); - $table->string('token', 40); - $table->string('persistent_token', 40); - $table->timestamp('expires_at')->nullable(); - $table->timestamps(); - - $table->engine = 'InnoDB'; - $table->collation = 'utf8_unicode_ci'; - $table->charset = 'utf8'; - //$table->foreign('user_id')->references('id')->on('users'); - $table->index('user_id'); - $table->index('token'); - $table->index('persistent_token'); - }); - } - } - - /** - * {@inheritDoc} - */ - public function down() { - $this->schema->drop('persistences'); - } -} +schema->hasTable('persistences')) { + $this->schema->create('persistences', function (Blueprint $table) { + $table->increments('id'); + $table->integer('user_id')->unsigned(); + $table->string('token', 40); + $table->string('persistent_token', 40); + $table->timestamp('expires_at')->nullable(); + $table->timestamps(); + + $table->engine = 'InnoDB'; + $table->collation = 'utf8_unicode_ci'; + $table->charset = 'utf8'; + //$table->foreign('user_id')->references('id')->on('users'); + $table->index('user_id'); + $table->index('token'); + $table->index('persistent_token'); + }); + } + } + + /** + * {@inheritDoc} + */ + public function down() { + $this->schema->drop('persistences'); + } +} diff --git a/main/app/sprinkles/account/src/Database/Migrations/v400/RoleUsersTable.php b/main/app/sprinkles/account/src/Database/Migrations/v400/RoleUsersTable.php index 4c7ca06..0820a02 100644 --- a/main/app/sprinkles/account/src/Database/Migrations/v400/RoleUsersTable.php +++ b/main/app/sprinkles/account/src/Database/Migrations/v400/RoleUsersTable.php @@ -1,54 +1,54 @@ -schema->hasTable('role_users')) { - $this->schema->create('role_users', function (Blueprint $table) { - $table->integer('user_id')->unsigned(); - $table->integer('role_id')->unsigned(); - $table->timestamps(); - - $table->engine = 'InnoDB'; - $table->collation = 'utf8_unicode_ci'; - $table->charset = 'utf8'; - $table->primary(['user_id', 'role_id']); - //$table->foreign('user_id')->references('id')->on('users'); - //$table->foreign('role_id')->references('id')->on('roles'); - $table->index('user_id'); - $table->index('role_id'); - }); - } - } - - /** - * {@inheritDoc} - */ - public function down() { - $this->schema->drop('role_users'); - } -} +schema->hasTable('role_users')) { + $this->schema->create('role_users', function (Blueprint $table) { + $table->integer('user_id')->unsigned(); + $table->integer('role_id')->unsigned(); + $table->timestamps(); + + $table->engine = 'InnoDB'; + $table->collation = 'utf8_unicode_ci'; + $table->charset = 'utf8'; + $table->primary(['user_id', 'role_id']); + //$table->foreign('user_id')->references('id')->on('users'); + //$table->foreign('role_id')->references('id')->on('roles'); + $table->index('user_id'); + $table->index('role_id'); + }); + } + } + + /** + * {@inheritDoc} + */ + public function down() { + $this->schema->drop('role_users'); + } +} diff --git a/main/app/sprinkles/account/src/Database/Migrations/v400/RolesTable.php b/main/app/sprinkles/account/src/Database/Migrations/v400/RolesTable.php index 20fe699..3a524e2 100644 --- a/main/app/sprinkles/account/src/Database/Migrations/v400/RolesTable.php +++ b/main/app/sprinkles/account/src/Database/Migrations/v400/RolesTable.php @@ -1,77 +1,77 @@ -schema->hasTable('roles')) { - $this->schema->create('roles', function (Blueprint $table) { - $table->increments('id'); - $table->string('slug'); - $table->string('name'); - $table->text('description')->nullable(); - $table->timestamps(); - - $table->engine = 'InnoDB'; - $table->collation = 'utf8_unicode_ci'; - $table->charset = 'utf8'; - $table->unique('slug'); - $table->index('slug'); - }); - - // Add default roles - $roles = [ - 'user' => new Role([ - 'slug' => 'user', - 'name' => 'User', - 'description' => 'This role provides basic user functionality.' - ]), - 'site-admin' => new Role([ - 'slug' => 'site-admin', - 'name' => 'Site Administrator', - 'description' => 'This role is meant for "site administrators", who can basically do anything except create, edit, or delete other administrators.' - ]), - 'group-admin' => new Role([ - 'slug' => 'group-admin', - 'name' => 'Group Administrator', - 'description' => 'This role is meant for "group administrators", who can basically do anything with users in their own group, except other administrators of that group.' - ]) - ]; - - foreach ($roles as $slug => $role) { - $role->save(); - } - } - } - - /** - * {@inheritDoc} - */ - public function down() { - $this->schema->drop('roles'); - } -} +schema->hasTable('roles')) { + $this->schema->create('roles', function (Blueprint $table) { + $table->increments('id'); + $table->string('slug'); + $table->string('name'); + $table->text('description')->nullable(); + $table->timestamps(); + + $table->engine = 'InnoDB'; + $table->collation = 'utf8_unicode_ci'; + $table->charset = 'utf8'; + $table->unique('slug'); + $table->index('slug'); + }); + + // Add default roles + $roles = [ + 'user' => new Role([ + 'slug' => 'user', + 'name' => 'User', + 'description' => 'This role provides basic user functionality.' + ]), + 'site-admin' => new Role([ + 'slug' => 'site-admin', + 'name' => 'Site Administrator', + 'description' => 'This role is meant for "site administrators", who can basically do anything except create, edit, or delete other administrators.' + ]), + 'group-admin' => new Role([ + 'slug' => 'group-admin', + 'name' => 'Group Administrator', + 'description' => 'This role is meant for "group administrators", who can basically do anything with users in their own group, except other administrators of that group.' + ]) + ]; + + foreach ($roles as $slug => $role) { + $role->save(); + } + } + } + + /** + * {@inheritDoc} + */ + public function down() { + $this->schema->drop('roles'); + } +} diff --git a/main/app/sprinkles/account/src/Database/Migrations/v400/UsersTable.php b/main/app/sprinkles/account/src/Database/Migrations/v400/UsersTable.php index 9c634e8..694da5b 100644 --- a/main/app/sprinkles/account/src/Database/Migrations/v400/UsersTable.php +++ b/main/app/sprinkles/account/src/Database/Migrations/v400/UsersTable.php @@ -1,68 +1,68 @@ -schema->hasTable('users')) { - $this->schema->create('users', function (Blueprint $table) { - $table->increments('id'); - $table->string('user_name', 50); - $table->string('email', 254); - $table->string('first_name', 20); - $table->string('last_name', 30); - $table->string('locale', 10)->default('en_US')->comment('The language and locale to use for this user.'); - $table->string('theme', 100)->nullable()->comment("The user theme."); - $table->integer('group_id')->unsigned()->default(1)->comment("The id of the user group."); - $table->boolean('flag_verified')->default(1)->comment("Set to 1 if the user has verified their account via email, 0 otherwise."); - $table->boolean('flag_enabled')->default(1)->comment("Set to 1 if the user account is currently enabled, 0 otherwise. Disabled accounts cannot be logged in to, but they retain all of their data and settings."); - $table->integer('last_activity_id')->unsigned()->nullable()->comment("The id of the last activity performed by this user."); - $table->string('password', 255); - $table->softDeletes(); - $table->timestamps(); - - $table->engine = 'InnoDB'; - $table->collation = 'utf8_unicode_ci'; - $table->charset = 'utf8'; - //$table->foreign('group_id')->references('id')->on('groups'); - //$table->foreign('last_activity_id')->references('id')->on('activities'); - $table->unique('user_name'); - $table->index('user_name'); - $table->unique('email'); - $table->index('email'); - $table->index('group_id'); - $table->index('last_activity_id'); - }); - } - } - - /** - * {@inheritDoc} - */ - public function down() { - $this->schema->drop('users'); - } -} +schema->hasTable('users')) { + $this->schema->create('users', function (Blueprint $table) { + $table->increments('id'); + $table->string('user_name', 50); + $table->string('email', 254); + $table->string('first_name', 20); + $table->string('last_name', 30); + $table->string('locale', 10)->default('en_US')->comment('The language and locale to use for this user.'); + $table->string('theme', 100)->nullable()->comment("The user theme."); + $table->integer('group_id')->unsigned()->default(1)->comment("The id of the user group."); + $table->boolean('flag_verified')->default(1)->comment("Set to 1 if the user has verified their account via email, 0 otherwise."); + $table->boolean('flag_enabled')->default(1)->comment("Set to 1 if the user account is currently enabled, 0 otherwise. Disabled accounts cannot be logged in to, but they retain all of their data and settings."); + $table->integer('last_activity_id')->unsigned()->nullable()->comment("The id of the last activity performed by this user."); + $table->string('password', 255); + $table->softDeletes(); + $table->timestamps(); + + $table->engine = 'InnoDB'; + $table->collation = 'utf8_unicode_ci'; + $table->charset = 'utf8'; + //$table->foreign('group_id')->references('id')->on('groups'); + //$table->foreign('last_activity_id')->references('id')->on('activities'); + $table->unique('user_name'); + $table->index('user_name'); + $table->unique('email'); + $table->index('email'); + $table->index('group_id'); + $table->index('last_activity_id'); + }); + } + } + + /** + * {@inheritDoc} + */ + public function down() { + $this->schema->drop('users'); + } +} diff --git a/main/app/sprinkles/account/src/Database/Migrations/v400/VerificationsTable.php b/main/app/sprinkles/account/src/Database/Migrations/v400/VerificationsTable.php index e42114c..2c4d28f 100644 --- a/main/app/sprinkles/account/src/Database/Migrations/v400/VerificationsTable.php +++ b/main/app/sprinkles/account/src/Database/Migrations/v400/VerificationsTable.php @@ -1,56 +1,56 @@ -schema->hasTable('verifications')) { - $this->schema->create('verifications', function (Blueprint $table) { - $table->increments('id'); - $table->integer('user_id')->unsigned(); - $table->string('hash'); - $table->boolean('completed')->default(0); - $table->timestamp('expires_at')->nullable(); - $table->timestamp('completed_at')->nullable(); - $table->timestamps(); - - $table->engine = 'InnoDB'; - $table->collation = 'utf8_unicode_ci'; - $table->charset = 'utf8'; - //$table->foreign('user_id')->references('id')->on('users'); - $table->index('user_id'); - $table->index('hash'); - }); - } - } - - /** - * {@inheritDoc} - */ - public function down() { - $this->schema->drop('verifications'); - } -} +schema->hasTable('verifications')) { + $this->schema->create('verifications', function (Blueprint $table) { + $table->increments('id'); + $table->integer('user_id')->unsigned(); + $table->string('hash'); + $table->boolean('completed')->default(0); + $table->timestamp('expires_at')->nullable(); + $table->timestamp('completed_at')->nullable(); + $table->timestamps(); + + $table->engine = 'InnoDB'; + $table->collation = 'utf8_unicode_ci'; + $table->charset = 'utf8'; + //$table->foreign('user_id')->references('id')->on('users'); + $table->index('user_id'); + $table->index('hash'); + }); + } + } + + /** + * {@inheritDoc} + */ + public function down() { + $this->schema->drop('verifications'); + } +} diff --git a/main/app/sprinkles/account/src/Database/Models/Activity.php b/main/app/sprinkles/account/src/Database/Models/Activity.php index 4e5b609..8f6cd18 100644 --- a/main/app/sprinkles/account/src/Database/Models/Activity.php +++ b/main/app/sprinkles/account/src/Database/Models/Activity.php @@ -1,83 +1,83 @@ -select('activities.*'); - - $query = $query->leftJoin('users', 'activities.user_id', '=', 'users.id'); - - return $query; - } - - /** - * Add clauses to select the most recent event of each type for each user, to the query. - * - * @return \Illuminate\Database\Query\Builder - */ - public function scopeMostRecentEvents($query) { - return $query->select('user_id', 'event_type', Capsule::raw('MAX(occurred_at) as occurred_at')) - ->groupBy('user_id') - ->groupBy('type'); - } - - /** - * Add clauses to select the most recent event of a given type for each user, to the query. - * - * @param string $type The type of event, matching the `event_type` field in the user_event table. - * @return \Illuminate\Database\Query\Builder - */ - public function scopeMostRecentEventsByType($query, $type) { - return $query->select('user_id', Capsule::raw('MAX(occurred_at) as occurred_at')) - ->where('type', $type) - ->groupBy('user_id'); - } - - /** - * Get the user associated with this activity. - */ - public function user() { - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = static::$ci->classMapper; - - return $this->belongsTo($classMapper->getClassMapping('user'), 'user_id'); - } -} +select('activities.*'); + + $query = $query->leftJoin('users', 'activities.user_id', '=', 'users.id'); + + return $query; + } + + /** + * Add clauses to select the most recent event of each type for each user, to the query. + * + * @return \Illuminate\Database\Query\Builder + */ + public function scopeMostRecentEvents($query) { + return $query->select('user_id', 'event_type', Capsule::raw('MAX(occurred_at) as occurred_at')) + ->groupBy('user_id') + ->groupBy('type'); + } + + /** + * Add clauses to select the most recent event of a given type for each user, to the query. + * + * @param string $type The type of event, matching the `event_type` field in the user_event table. + * @return \Illuminate\Database\Query\Builder + */ + public function scopeMostRecentEventsByType($query, $type) { + return $query->select('user_id', Capsule::raw('MAX(occurred_at) as occurred_at')) + ->where('type', $type) + ->groupBy('user_id'); + } + + /** + * Get the user associated with this activity. + */ + public function user() { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->belongsTo($classMapper->getClassMapping('user'), 'user_id'); + } +} diff --git a/main/app/sprinkles/account/src/Database/Models/Group.php b/main/app/sprinkles/account/src/Database/Models/Group.php index f0a1e1f..abb0e36 100644 --- a/main/app/sprinkles/account/src/Database/Models/Group.php +++ b/main/app/sprinkles/account/src/Database/Models/Group.php @@ -1,68 +1,68 @@ -classMapper; - - return $this->hasMany($classMapper->getClassMapping('user'), 'group_id'); - } -} +classMapper; + + return $this->hasMany($classMapper->getClassMapping('user'), 'group_id'); + } +} diff --git a/main/app/sprinkles/account/src/Database/Models/PasswordReset.php b/main/app/sprinkles/account/src/Database/Models/PasswordReset.php index 3fc4e3c..99b1920 100644 --- a/main/app/sprinkles/account/src/Database/Models/PasswordReset.php +++ b/main/app/sprinkles/account/src/Database/Models/PasswordReset.php @@ -1,74 +1,74 @@ -token; - } - - /** - * @param string $value - */ - public function setToken($value) { - $this->token = $value; - return $this; - } - - /** - * Get the user associated with this reset request. - */ - public function user() { - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = static::$ci->classMapper; - - return $this->belongsTo($classMapper->getClassMapping('user'), 'user_id'); - } -} +token; + } + + /** + * @param string $value + */ + public function setToken($value) { + $this->token = $value; + return $this; + } + + /** + * Get the user associated with this reset request. + */ + public function user() { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->belongsTo($classMapper->getClassMapping('user'), 'user_id'); + } +} diff --git a/main/app/sprinkles/account/src/Database/Models/Permission.php b/main/app/sprinkles/account/src/Database/Models/Permission.php index 3035e56..da4391f 100644 --- a/main/app/sprinkles/account/src/Database/Models/Permission.php +++ b/main/app/sprinkles/account/src/Database/Models/Permission.php @@ -1,117 +1,117 @@ -roles()->detach(); - - // Delete the permission - $result = parent::delete(); - - return $result; - } - - /** - * Get a list of roles to which this permission is assigned. - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany - */ - public function roles() { - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = static::$ci->classMapper; - - return $this->belongsToMany($classMapper->getClassMapping('role'), 'permission_roles', 'permission_id', 'role_id')->withTimestamps(); - } - - /** - * Query scope to get all permissions assigned to a specific role. - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param int $roleId - * @return \Illuminate\Database\Eloquent\Builder - */ - public function scopeForRole($query, $roleId) { - return $query->join('permission_roles', function ($join) use ($roleId) { - $join->on('permission_roles.permission_id', 'permissions.id') - ->where('role_id', $roleId); - }); - } - - /** - * Query scope to get all permissions NOT associated with a specific role. - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param int $roleId - * @return \Illuminate\Database\Eloquent\Builder - */ - public function scopeNotForRole($query, $roleId) { - return $query->join('permission_roles', function ($join) use ($roleId) { - $join->on('permission_roles.permission_id', 'permissions.id') - ->where('role_id', '!=', $roleId); - }); - } - - /** - * Get a list of users who have this permission, along with a list of roles through which each user has the permission. - * - * @return \UserFrosting\Sprinkle\Core\Database\Relations\BelongsToManyThrough - */ - public function users() { - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = static::$ci->classMapper; - - return $this->belongsToManyThrough( - $classMapper->getClassMapping('user'), - $classMapper->getClassMapping('role'), - 'permission_roles', - 'permission_id', - 'role_id', - 'role_users', - 'role_id', - 'user_id' - ); - } -} +roles()->detach(); + + // Delete the permission + $result = parent::delete(); + + return $result; + } + + /** + * Get a list of roles to which this permission is assigned. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function roles() { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->belongsToMany($classMapper->getClassMapping('role'), 'permission_roles', 'permission_id', 'role_id')->withTimestamps(); + } + + /** + * Query scope to get all permissions assigned to a specific role. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param int $roleId + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeForRole($query, $roleId) { + return $query->join('permission_roles', function ($join) use ($roleId) { + $join->on('permission_roles.permission_id', 'permissions.id') + ->where('role_id', $roleId); + }); + } + + /** + * Query scope to get all permissions NOT associated with a specific role. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param int $roleId + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeNotForRole($query, $roleId) { + return $query->join('permission_roles', function ($join) use ($roleId) { + $join->on('permission_roles.permission_id', 'permissions.id') + ->where('role_id', '!=', $roleId); + }); + } + + /** + * Get a list of users who have this permission, along with a list of roles through which each user has the permission. + * + * @return \UserFrosting\Sprinkle\Core\Database\Relations\BelongsToManyThrough + */ + public function users() { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->belongsToManyThrough( + $classMapper->getClassMapping('user'), + $classMapper->getClassMapping('role'), + 'permission_roles', + 'permission_id', + 'role_id', + 'role_users', + 'role_id', + 'user_id' + ); + } +} diff --git a/main/app/sprinkles/account/src/Database/Models/Role.php b/main/app/sprinkles/account/src/Database/Models/Role.php index 4a58df0..f8e40b3 100644 --- a/main/app/sprinkles/account/src/Database/Models/Role.php +++ b/main/app/sprinkles/account/src/Database/Models/Role.php @@ -1,101 +1,101 @@ -permissions()->detach(); - - // Remove all user associations - $this->users()->detach(); - - // Delete the role - $result = parent::delete(); - - return $result; - } - - /** - * Get a list of default roles. - */ - public static function getDefaultSlugs() { - /** @var UserFrosting\Config $config */ - $config = static::$ci->config; - - return array_map('trim', array_keys($config['site.registration.user_defaults.roles'], TRUE)); - } - - /** - * Get a list of permissions assigned to this role. - */ - public function permissions() { - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = static::$ci->classMapper; - - return $this->belongsToMany($classMapper->getClassMapping('permission'), 'permission_roles', 'role_id', 'permission_id')->withTimestamps(); - } - - /** - * Query scope to get all roles assigned to a specific user. - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param int $userId - * @return \Illuminate\Database\Eloquent\Builder - */ - public function scopeForUser($query, $userId) { - return $query->join('role_users', function ($join) use ($userId) { - $join->on('role_users.role_id', 'roles.id') - ->where('user_id', $userId); - }); - } - - /** - * Get a list of users who have this role. - */ - public function users() { - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = static::$ci->classMapper; - - return $this->belongsToMany($classMapper->getClassMapping('user'), 'role_users', 'role_id', 'user_id'); - } -} +permissions()->detach(); + + // Remove all user associations + $this->users()->detach(); + + // Delete the role + $result = parent::delete(); + + return $result; + } + + /** + * Get a list of default roles. + */ + public static function getDefaultSlugs() { + /** @var UserFrosting\Config $config */ + $config = static::$ci->config; + + return array_map('trim', array_keys($config['site.registration.user_defaults.roles'], TRUE)); + } + + /** + * Get a list of permissions assigned to this role. + */ + public function permissions() { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->belongsToMany($classMapper->getClassMapping('permission'), 'permission_roles', 'role_id', 'permission_id')->withTimestamps(); + } + + /** + * Query scope to get all roles assigned to a specific user. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param int $userId + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeForUser($query, $userId) { + return $query->join('role_users', function ($join) use ($userId) { + $join->on('role_users.role_id', 'roles.id') + ->where('user_id', $userId); + }); + } + + /** + * Get a list of users who have this role. + */ + public function users() { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->belongsToMany($classMapper->getClassMapping('user'), 'role_users', 'role_id', 'user_id'); + } +} diff --git a/main/app/sprinkles/account/src/Database/Models/User.php b/main/app/sprinkles/account/src/Database/Models/User.php index b401db2..cccd307 100644 --- a/main/app/sprinkles/account/src/Database/Models/User.php +++ b/main/app/sprinkles/account/src/Database/Models/User.php @@ -1,469 +1,469 @@ -lastActivityTime('sign_in'); - } else if ($name == 'avatar') { - // Use Gravatar as the user avatar - $hash = md5(strtolower(trim($this->email))); - return 'https://www.gravatar.com/avatar/' . $hash . '?d=mm'; - } else { - return parent::__get($name); - } - } - - /** - * Get all activities for this user. - * - * @return \Illuminate\Database\Eloquent\Relations\HasMany - */ - public function activities() { - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = static::$ci->classMapper; - - return $this->hasMany($classMapper->getClassMapping('activity'), 'user_id'); - } - - /** - * Delete this user from the database, along with any linked roles and activities. - * - * @param bool $hardDelete Set to true to completely remove the user and all associated objects. - * @return bool true if the deletion was successful, false otherwise. - */ - public function delete($hardDelete = FALSE) { - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = static::$ci->classMapper; - - if ($hardDelete) { - // Remove all role associations - $this->roles()->detach(); - - // Remove all user activities - $classMapper->staticMethod('activity', 'where', 'user_id', $this->id)->delete(); - - // Remove all user tokens - $classMapper->staticMethod('password_reset', 'where', 'user_id', $this->id)->delete(); - $classMapper->staticMethod('verification', 'where', 'user_id', $this->id)->delete(); - - // Delete the user - $result = parent::forceDelete(); - } else { - // Soft delete the user, leaving all associated records alone - $result = parent::delete(); - } - - return $result; - } - - /** - * Determines whether a user exists, including checking soft-deleted records - * - * @deprecated since 4.1.7 This method conflicts with and overrides the Builder::exists() method. Use Model::findUnique instead. - * @param mixed $value - * @param string $identifier - * @param bool $checkDeleted set to true to include soft-deleted records - * @return User|null - */ - public static function exists($value, $identifier = 'user_name', $checkDeleted = TRUE) { - return static::findUnique($value, $identifier, $checkDeleted); - } - - /** - * Return a cache instance specific to that user - * - * @return \Illuminate\Contracts\Cache\Store - */ - public function getCache() { - return static::$ci->cache->tags('_u' . $this->id); - } - - /** - * Allows you to get the full name of the user using `$user->full_name` - * - * @return string - */ - public function getFullNameAttribute() { - return $this->first_name . ' ' . $this->last_name; - } - - /** - * Retrieve the cached permissions dictionary for this user. - * - * @return array - */ - public function getCachedPermissions() { - if (!isset($this->cachedPermissions)) { - $this->reloadCachedPermissions(); - } - - return $this->cachedPermissions; - } - - /** - * Retrieve the cached permissions dictionary for this user. - * - * @return User - */ - public function reloadCachedPermissions() { - $this->cachedPermissions = $this->buildPermissionsDictionary(); - - return $this; - } - - /** - * Get the amount of time, in seconds, that has elapsed since the last activity of a certain time for this user. - * - * @param string $type The type of activity to search for. - * @return int - */ - public function getSecondsSinceLastActivity($type) { - $time = $this->lastActivityTime($type); - $time = $time ? $time : '0000-00-00 00:00:00'; - $time = new Carbon($time); - - return $time->diffInSeconds(); - } - - /** - * Return this user's group. - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function group() { - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = static::$ci->classMapper; - - return $this->belongsTo($classMapper->getClassMapping('group'), 'group_id'); - } - - /** - * Returns whether or not this user is the master user. - * - * @return bool - */ - public function isMaster() { - $masterId = static::$ci->config['reserved_user_ids.master']; - - // Need to use loose comparison for now, because some DBs return `id` as a string - return ($this->id == $masterId); - } - - /** - * Get the most recent activity for this user, based on the user's last_activity_id. - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function lastActivity() { - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = static::$ci->classMapper; - - return $this->belongsTo($classMapper->getClassMapping('activity'), 'last_activity_id'); - } - - /** - * Find the most recent activity for this user of a particular type. - * - * @param string $type - * @return \Illuminate\Database\Eloquent\Builder - */ - public function lastActivityOfType($type = NULL) { - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = static::$ci->classMapper; - - $query = $this->hasOne($classMapper->getClassMapping('activity'), 'user_id'); - - if ($type) { - $query = $query->where('type', $type); - } - - return $query->latest('occurred_at'); - } - - /** - * Get the most recent time for a specified activity type for this user. - * - * @param string $type - * @return string|null The last activity time, as a SQL formatted time (YYYY-MM-DD HH:MM:SS), or null if an activity of this type doesn't exist. - */ - public function lastActivityTime($type) { - $result = $this->activities() - ->where('type', $type) - ->max('occurred_at'); - return $result ? $result : NULL; - } - - /** - * Performs tasks to be done after this user has been successfully authenticated. - * - * By default, adds a new sign-in activity and updates any legacy hash. - * @param mixed[] $params Optional array of parameters used for this event handler. - * @odo Transition to Laravel Event dispatcher to handle this - */ - public function onLogin($params = []) { - // Add a sign in activity (time is automatically set by database) - static::$ci->userActivityLogger->info("User {$this->user_name} signed in.", [ - 'type' => 'sign_in' - ]); - - // Update password if we had encountered an outdated hash - $passwordType = Password::getHashType($this->password); - - if ($passwordType != 'modern') { - if (!isset($params['password'])) { - Debug::debug('Notice: Unhashed password must be supplied to update to modern password hashing.'); - } else { - // Hash the user's password and update - $passwordHash = Password::hash($params['password']); - if ($passwordHash === NULL) { - Debug::debug('Notice: outdated password hash could not be updated because the new hashing algorithm is not supported. Are you running PHP >= 5.3.7?'); - } else { - $this->password = $passwordHash; - Debug::debug('Notice: outdated password hash has been automatically updated to modern hashing.'); - } - } - } - - // Save changes - $this->save(); - - return $this; - } - - /** - * Performs tasks to be done after this user has been logged out. - * - * By default, adds a new sign-out activity. - * @param mixed[] $params Optional array of parameters used for this event handler. - * @do Transition to Laravel Event dispatcher to handle this - */ - public function onLogout($params = []) { - static::$ci->userActivityLogger->info("User {$this->user_name} signed out.", [ - 'type' => 'sign_out' - ]); - - return $this; - } - - /** - * Get all password reset requests for this user. - * - * @return \Illuminate\Database\Eloquent\Relations\HasMany - */ - public function passwordResets() { - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = static::$ci->classMapper; - - return $this->hasMany($classMapper->getClassMapping('password_reset'), 'user_id'); - } - - /** - * Get all of the permissions this user has, via its roles. - * - * @return \UserFrosting\Sprinkle\Core\Database\Relations\BelongsToManyThrough - */ - public function permissions() { - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = static::$ci->classMapper; - - return $this->belongsToManyThrough( - $classMapper->getClassMapping('permission'), - $classMapper->getClassMapping('role'), - 'role_users', - 'user_id', - 'role_id', - 'permission_roles', - 'role_id', - 'permission_id' - ); - } - - /** - * Get all roles to which this user belongs. - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany - */ - public function roles() { - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = static::$ci->classMapper; - - return $this->belongsToMany($classMapper->getClassMapping('role'), 'role_users', 'user_id', 'role_id')->withTimestamps(); - } - - /** - * Query scope to get all users who have a specific role. - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param int $roleId - * @return \Illuminate\Database\Eloquent\Builder - */ - public function scopeForRole($query, $roleId) { - return $query->join('role_users', function ($join) use ($roleId) { - $join->on('role_users.user_id', 'users.id') - ->where('role_id', $roleId); - }); - } - - /** - * Joins the user's most recent activity directly, so we can do things like sort, search, paginate, etc. - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @return \Illuminate\Database\Eloquent\Builder - */ - public function scopeJoinLastActivity($query) { - $query = $query->select('users.*'); - - $query = $query->leftJoin('activities', 'activities.id', '=', 'users.last_activity_id'); - - return $query; - } - - /** - * Loads permissions for this user into a cached dictionary of slugs -> arrays of permissions, - * so we don't need to keep requerying the DB for every call of checkAccess. - * - * @return array - */ - protected function buildPermissionsDictionary() { - $permissions = $this->permissions()->get(); - $cachedPermissions = []; - - foreach ($permissions as $permission) { - $cachedPermissions[$permission->slug][] = $permission; - } - - return $cachedPermissions; - } -} +lastActivityTime('sign_in'); + } else if ($name == 'avatar') { + // Use Gravatar as the user avatar + $hash = md5(strtolower(trim($this->email))); + return 'https://www.gravatar.com/avatar/' . $hash . '?d=mm'; + } else { + return parent::__get($name); + } + } + + /** + * Get all activities for this user. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function activities() { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->hasMany($classMapper->getClassMapping('activity'), 'user_id'); + } + + /** + * Delete this user from the database, along with any linked roles and activities. + * + * @param bool $hardDelete Set to true to completely remove the user and all associated objects. + * @return bool true if the deletion was successful, false otherwise. + */ + public function delete($hardDelete = FALSE) { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + if ($hardDelete) { + // Remove all role associations + $this->roles()->detach(); + + // Remove all user activities + $classMapper->staticMethod('activity', 'where', 'user_id', $this->id)->delete(); + + // Remove all user tokens + $classMapper->staticMethod('password_reset', 'where', 'user_id', $this->id)->delete(); + $classMapper->staticMethod('verification', 'where', 'user_id', $this->id)->delete(); + + // Delete the user + $result = parent::forceDelete(); + } else { + // Soft delete the user, leaving all associated records alone + $result = parent::delete(); + } + + return $result; + } + + /** + * Determines whether a user exists, including checking soft-deleted records + * + * @deprecated since 4.1.7 This method conflicts with and overrides the Builder::exists() method. Use Model::findUnique instead. + * @param mixed $value + * @param string $identifier + * @param bool $checkDeleted set to true to include soft-deleted records + * @return User|null + */ + public static function exists($value, $identifier = 'user_name', $checkDeleted = TRUE) { + return static::findUnique($value, $identifier, $checkDeleted); + } + + /** + * Return a cache instance specific to that user + * + * @return \Illuminate\Contracts\Cache\Store + */ + public function getCache() { + return static::$ci->cache->tags('_u' . $this->id); + } + + /** + * Allows you to get the full name of the user using `$user->full_name` + * + * @return string + */ + public function getFullNameAttribute() { + return $this->first_name . ' ' . $this->last_name; + } + + /** + * Retrieve the cached permissions dictionary for this user. + * + * @return array + */ + public function getCachedPermissions() { + if (!isset($this->cachedPermissions)) { + $this->reloadCachedPermissions(); + } + + return $this->cachedPermissions; + } + + /** + * Retrieve the cached permissions dictionary for this user. + * + * @return User + */ + public function reloadCachedPermissions() { + $this->cachedPermissions = $this->buildPermissionsDictionary(); + + return $this; + } + + /** + * Get the amount of time, in seconds, that has elapsed since the last activity of a certain time for this user. + * + * @param string $type The type of activity to search for. + * @return int + */ + public function getSecondsSinceLastActivity($type) { + $time = $this->lastActivityTime($type); + $time = $time ? $time : '0000-00-00 00:00:00'; + $time = new Carbon($time); + + return $time->diffInSeconds(); + } + + /** + * Return this user's group. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function group() { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->belongsTo($classMapper->getClassMapping('group'), 'group_id'); + } + + /** + * Returns whether or not this user is the master user. + * + * @return bool + */ + public function isMaster() { + $masterId = static::$ci->config['reserved_user_ids.master']; + + // Need to use loose comparison for now, because some DBs return `id` as a string + return ($this->id == $masterId); + } + + /** + * Get the most recent activity for this user, based on the user's last_activity_id. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function lastActivity() { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->belongsTo($classMapper->getClassMapping('activity'), 'last_activity_id'); + } + + /** + * Find the most recent activity for this user of a particular type. + * + * @param string $type + * @return \Illuminate\Database\Eloquent\Builder + */ + public function lastActivityOfType($type = NULL) { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + $query = $this->hasOne($classMapper->getClassMapping('activity'), 'user_id'); + + if ($type) { + $query = $query->where('type', $type); + } + + return $query->latest('occurred_at'); + } + + /** + * Get the most recent time for a specified activity type for this user. + * + * @param string $type + * @return string|null The last activity time, as a SQL formatted time (YYYY-MM-DD HH:MM:SS), or null if an activity of this type doesn't exist. + */ + public function lastActivityTime($type) { + $result = $this->activities() + ->where('type', $type) + ->max('occurred_at'); + return $result ? $result : NULL; + } + + /** + * Performs tasks to be done after this user has been successfully authenticated. + * + * By default, adds a new sign-in activity and updates any legacy hash. + * @param mixed[] $params Optional array of parameters used for this event handler. + * @odo Transition to Laravel Event dispatcher to handle this + */ + public function onLogin($params = []) { + // Add a sign in activity (time is automatically set by database) + static::$ci->userActivityLogger->info("User {$this->user_name} signed in.", [ + 'type' => 'sign_in' + ]); + + // Update password if we had encountered an outdated hash + $passwordType = Password::getHashType($this->password); + + if ($passwordType != 'modern') { + if (!isset($params['password'])) { + Debug::debug('Notice: Unhashed password must be supplied to update to modern password hashing.'); + } else { + // Hash the user's password and update + $passwordHash = Password::hash($params['password']); + if ($passwordHash === NULL) { + Debug::debug('Notice: outdated password hash could not be updated because the new hashing algorithm is not supported. Are you running PHP >= 5.3.7?'); + } else { + $this->password = $passwordHash; + Debug::debug('Notice: outdated password hash has been automatically updated to modern hashing.'); + } + } + } + + // Save changes + $this->save(); + + return $this; + } + + /** + * Performs tasks to be done after this user has been logged out. + * + * By default, adds a new sign-out activity. + * @param mixed[] $params Optional array of parameters used for this event handler. + * @do Transition to Laravel Event dispatcher to handle this + */ + public function onLogout($params = []) { + static::$ci->userActivityLogger->info("User {$this->user_name} signed out.", [ + 'type' => 'sign_out' + ]); + + return $this; + } + + /** + * Get all password reset requests for this user. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function passwordResets() { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->hasMany($classMapper->getClassMapping('password_reset'), 'user_id'); + } + + /** + * Get all of the permissions this user has, via its roles. + * + * @return \UserFrosting\Sprinkle\Core\Database\Relations\BelongsToManyThrough + */ + public function permissions() { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->belongsToManyThrough( + $classMapper->getClassMapping('permission'), + $classMapper->getClassMapping('role'), + 'role_users', + 'user_id', + 'role_id', + 'permission_roles', + 'role_id', + 'permission_id' + ); + } + + /** + * Get all roles to which this user belongs. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function roles() { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->belongsToMany($classMapper->getClassMapping('role'), 'role_users', 'user_id', 'role_id')->withTimestamps(); + } + + /** + * Query scope to get all users who have a specific role. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param int $roleId + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeForRole($query, $roleId) { + return $query->join('role_users', function ($join) use ($roleId) { + $join->on('role_users.user_id', 'users.id') + ->where('role_id', $roleId); + }); + } + + /** + * Joins the user's most recent activity directly, so we can do things like sort, search, paginate, etc. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeJoinLastActivity($query) { + $query = $query->select('users.*'); + + $query = $query->leftJoin('activities', 'activities.id', '=', 'users.last_activity_id'); + + return $query; + } + + /** + * Loads permissions for this user into a cached dictionary of slugs -> arrays of permissions, + * so we don't need to keep requerying the DB for every call of checkAccess. + * + * @return array + */ + protected function buildPermissionsDictionary() { + $permissions = $this->permissions()->get(); + $cachedPermissions = []; + + foreach ($permissions as $permission) { + $cachedPermissions[$permission->slug][] = $permission; + } + + return $cachedPermissions; + } +} diff --git a/main/app/sprinkles/account/src/Database/Models/Verification.php b/main/app/sprinkles/account/src/Database/Models/Verification.php index f6697b6..f642d77 100644 --- a/main/app/sprinkles/account/src/Database/Models/Verification.php +++ b/main/app/sprinkles/account/src/Database/Models/Verification.php @@ -1,68 +1,68 @@ -token; - } - - public function setToken($value) { - $this->token = $value; - return $this; - } - - /** - * Get the user associated with this verification request. - */ - public function user() { - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = static::$ci->classMapper; - - return $this->belongsTo($classMapper->getClassMapping('user'), 'user_id'); - } -} +token; + } + + public function setToken($value) { + $this->token = $value; + return $this; + } + + /** + * Get the user associated with this verification request. + */ + public function user() { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->belongsTo($classMapper->getClassMapping('user'), 'user_id'); + } +} diff --git a/main/app/sprinkles/account/src/Error/Handler/AuthCompromisedExceptionHandler.php b/main/app/sprinkles/account/src/Error/Handler/AuthCompromisedExceptionHandler.php index 4c3b100..ccefe72 100644 --- a/main/app/sprinkles/account/src/Error/Handler/AuthCompromisedExceptionHandler.php +++ b/main/app/sprinkles/account/src/Error/Handler/AuthCompromisedExceptionHandler.php @@ -1,34 +1,34 @@ -ci->view->getEnvironment()->loadTemplate('pages/error/compromised.html.twig'); - - return $this->response - ->withStatus($this->statusCode) - ->withHeader('Content-type', $this->contentType) - ->write($template->render()); - } -} +ci->view->getEnvironment()->loadTemplate('pages/error/compromised.html.twig'); + + return $this->response + ->withStatus($this->statusCode) + ->withHeader('Content-type', $this->contentType) + ->write($template->render()); + } +} diff --git a/main/app/sprinkles/account/src/Error/Handler/AuthExpiredExceptionHandler.php b/main/app/sprinkles/account/src/Error/Handler/AuthExpiredExceptionHandler.php index fd3ca1f..fb04bd1 100644 --- a/main/app/sprinkles/account/src/Error/Handler/AuthExpiredExceptionHandler.php +++ b/main/app/sprinkles/account/src/Error/Handler/AuthExpiredExceptionHandler.php @@ -1,50 +1,50 @@ -writeAlerts(); - - $response = $this->response; - - // For non-AJAX requests, we forward the user to the login page. - if (!$this->request->isXhr()) { - $uri = $this->request->getUri(); - $path = $uri->getPath(); - $query = $uri->getQuery(); - $fragment = $uri->getFragment(); - - $path = $path - . ($query ? '?' . $query : '') - . ($fragment ? '#' . $fragment : ''); - - $loginPage = $this->ci->router->pathFor('login', [], [ - 'redirect' => $path - ]); - - $response = $response->withRedirect($loginPage); - } - - return $response; - } -} +writeAlerts(); + + $response = $this->response; + + // For non-AJAX requests, we forward the user to the login page. + if (!$this->request->isXhr()) { + $uri = $this->request->getUri(); + $path = $uri->getPath(); + $query = $uri->getQuery(); + $fragment = $uri->getFragment(); + + $path = $path + . ($query ? '?' . $query : '') + . ($fragment ? '#' . $fragment : ''); + + $loginPage = $this->ci->router->pathFor('login', [], [ + 'redirect' => $path + ]); + + $response = $response->withRedirect($loginPage); + } + + return $response; + } +} diff --git a/main/app/sprinkles/account/src/Error/Handler/ForbiddenExceptionHandler.php b/main/app/sprinkles/account/src/Error/Handler/ForbiddenExceptionHandler.php index b418dde..0166d2a 100644 --- a/main/app/sprinkles/account/src/Error/Handler/ForbiddenExceptionHandler.php +++ b/main/app/sprinkles/account/src/Error/Handler/ForbiddenExceptionHandler.php @@ -1,31 +1,31 @@ -classMapper->createInstance($this->modelName, $record['extra']); - $log->save(); - - if (isset($record['extra']['user_id'])) { - $user = $this->classMapper->staticMethod('user', 'find', $record['extra']['user_id']); - $user->last_activity_id = $log->id; - $user->save(); - } - } -} +classMapper->createInstance($this->modelName, $record['extra']); + $log->save(); + + if (isset($record['extra']['user_id'])) { + $user = $this->classMapper->staticMethod('user', 'find', $record['extra']['user_id']); + $user->last_activity_id = $log->id; + $user->save(); + } + } +} diff --git a/main/app/sprinkles/account/src/Log/UserActivityProcessor.php b/main/app/sprinkles/account/src/Log/UserActivityProcessor.php index f1aa8c7..a5c0d98 100644 --- a/main/app/sprinkles/account/src/Log/UserActivityProcessor.php +++ b/main/app/sprinkles/account/src/Log/UserActivityProcessor.php @@ -1,44 +1,44 @@ -userId = $userId; - } - - public function __invoke(array $record) { - $additionalFields = [ - 'ip_address' => $_SERVER['REMOTE_ADDR'], - 'user_id' => $this->userId, - 'occurred_at' => $record['datetime'], - 'description' => $record['message'] - ]; - - $record['extra'] = array_replace_recursive($record['extra'], $additionalFields, $record['context']); - - return $record; - } -} +userId = $userId; + } + + public function __invoke(array $record) { + $additionalFields = [ + 'ip_address' => $_SERVER['REMOTE_ADDR'], + 'user_id' => $this->userId, + 'occurred_at' => $record['datetime'], + 'description' => $record['message'] + ]; + + $record['extra'] = array_replace_recursive($record['extra'], $additionalFields, $record['context']); + + return $record; + } +} diff --git a/main/app/sprinkles/account/src/Repository/PasswordResetRepository.php b/main/app/sprinkles/account/src/Repository/PasswordResetRepository.php index 21ff548..06a37a8 100644 --- a/main/app/sprinkles/account/src/Repository/PasswordResetRepository.php +++ b/main/app/sprinkles/account/src/Repository/PasswordResetRepository.php @@ -1,34 +1,34 @@ -password = Password::hash($args['password']); - // DO: generate user activity? or do this in controller? - $user->save(); - } -} +password = Password::hash($args['password']); + // DO: generate user activity? or do this in controller? + $user->save(); + } +} diff --git a/main/app/sprinkles/account/src/Repository/TokenRepository.php b/main/app/sprinkles/account/src/Repository/TokenRepository.php index 5c2e34a..6b289bf 100644 --- a/main/app/sprinkles/account/src/Repository/TokenRepository.php +++ b/main/app/sprinkles/account/src/Repository/TokenRepository.php @@ -1,223 +1,223 @@ -classMapper = $classMapper; - $this->algorithm = $algorithm; - } - - /** - * Cancels a specified token by removing it from the database. - * - * @param int $token The token to remove. - * @return Model|false - */ - public function cancel($token) { - // Hash the password reset token for the stored version - $hash = hash($this->algorithm, $token); - - // Find an incomplete reset request for the specified hash - $model = $this->classMapper - ->staticMethod($this->modelIdentifier, 'where', 'hash', $hash) - ->where('completed', FALSE) - ->first(); - - if ($model === NULL) { - return FALSE; - } - - $model->delete(); - - return $model; - } - - /** - * Completes a token-based process, invoking updateUser() in the child object to do the actual action. - * - * @param int $token The token to complete. - * @param mixed[] $userParams An optional list of parameters to pass to updateUser(). - * @return Model|false - */ - public function complete($token, $userParams = []) { - // Hash the token for the stored version - $hash = hash($this->algorithm, $token); - - // Find an unexpired, incomplete token for the specified hash - $model = $this->classMapper - ->staticMethod($this->modelIdentifier, 'where', 'hash', $hash) - ->where('completed', FALSE) - ->where('expires_at', '>', Carbon::now()) - ->first(); - - if ($model === NULL) { - return FALSE; - } - - // Fetch user for this token - $user = $this->classMapper->staticMethod('user', 'find', $model->user_id); - - if (is_null($user)) { - return FALSE; - } - - $this->updateUser($user, $userParams); - - $model->fill([ - 'completed' => TRUE, - 'completed_at' => Carbon::now() - ]); - - $model->save(); - - return $model; - } - - /** - * Create a new token for a specified user. - * - * @param User $user The user object to associate with this token. - * @param int $timeout The time, in seconds, after which this token should expire. - * @return Model The model (PasswordReset, Verification, etc) object that stores the token. - */ - public function create(User $user, $timeout) { - // Remove any previous tokens for this user - $this->removeExisting($user); - - // Compute expiration time - $expiresAt = Carbon::now()->addSeconds($timeout); - - $model = $this->classMapper->createInstance($this->modelIdentifier); - - // Generate a random token - $model->setToken($this->generateRandomToken()); - - // Hash the password reset token for the stored version - $hash = hash($this->algorithm, $model->getToken()); - - $model->fill([ - 'hash' => $hash, - 'completed' => FALSE, - 'expires_at' => $expiresAt - ]); - - $model->user_id = $user->id; - - $model->save(); - - return $model; - } - - /** - * Determine if a specified user has an incomplete and unexpired token. - * - * @param User $user The user object to look up. - * @param int $token Optionally, try to match a specific token. - * @return Model|false - */ - public function exists(User $user, $token = NULL) { - $model = $this->classMapper - ->staticMethod($this->modelIdentifier, 'where', 'user_id', $user->id) - ->where('completed', FALSE) - ->where('expires_at', '>', Carbon::now()); - - if ($token) { - // get token hash - $hash = hash($this->algorithm, $token); - $model->where('hash', $hash); - } - - return $model->first() ?: FALSE; - } - - /** - * Delete all existing tokens from the database for a particular user. - * - * @param User $user - * @return int - */ - protected function removeExisting(User $user) { - return $this->classMapper - ->staticMethod($this->modelIdentifier, 'where', 'user_id', $user->id) - ->delete(); - } - - /** - * Remove all expired tokens from the database. - * - * @return bool|null - */ - public function removeExpired() { - return $this->classMapper - ->staticMethod($this->modelIdentifier, 'where', 'completed', FALSE) - ->where('expires_at', '<', Carbon::now()) - ->delete(); - } - - /** - * Generate a new random token for this user. - * - * This generates a token to use for verifying a new account, resetting a lost password, etc. - * @param string $gen specify an existing token that, if we happen to generate the same value, we should regenerate on. - * @return string - */ - protected function generateRandomToken($gen = NULL) { - do { - $gen = md5(uniqid(mt_rand(), FALSE)); - } while ($this->classMapper - ->staticMethod($this->modelIdentifier, 'where', 'hash', hash($this->algorithm, $gen)) - ->first()); - return $gen; - } - - /** - * Modify the user during the token completion process. - * - * This method is called during complete(), and is a way for concrete implementations to modify the user. - * @param User $user the user object to modify. - * @return mixed[] $args the list of parameters that were supplied to the call to `complete()` - */ - abstract protected function updateUser($user, $args); -} +classMapper = $classMapper; + $this->algorithm = $algorithm; + } + + /** + * Cancels a specified token by removing it from the database. + * + * @param int $token The token to remove. + * @return Model|false + */ + public function cancel($token) { + // Hash the password reset token for the stored version + $hash = hash($this->algorithm, $token); + + // Find an incomplete reset request for the specified hash + $model = $this->classMapper + ->staticMethod($this->modelIdentifier, 'where', 'hash', $hash) + ->where('completed', FALSE) + ->first(); + + if ($model === NULL) { + return FALSE; + } + + $model->delete(); + + return $model; + } + + /** + * Completes a token-based process, invoking updateUser() in the child object to do the actual action. + * + * @param int $token The token to complete. + * @param mixed[] $userParams An optional list of parameters to pass to updateUser(). + * @return Model|false + */ + public function complete($token, $userParams = []) { + // Hash the token for the stored version + $hash = hash($this->algorithm, $token); + + // Find an unexpired, incomplete token for the specified hash + $model = $this->classMapper + ->staticMethod($this->modelIdentifier, 'where', 'hash', $hash) + ->where('completed', FALSE) + ->where('expires_at', '>', Carbon::now()) + ->first(); + + if ($model === NULL) { + return FALSE; + } + + // Fetch user for this token + $user = $this->classMapper->staticMethod('user', 'find', $model->user_id); + + if (is_null($user)) { + return FALSE; + } + + $this->updateUser($user, $userParams); + + $model->fill([ + 'completed' => TRUE, + 'completed_at' => Carbon::now() + ]); + + $model->save(); + + return $model; + } + + /** + * Create a new token for a specified user. + * + * @param User $user The user object to associate with this token. + * @param int $timeout The time, in seconds, after which this token should expire. + * @return Model The model (PasswordReset, Verification, etc) object that stores the token. + */ + public function create(User $user, $timeout) { + // Remove any previous tokens for this user + $this->removeExisting($user); + + // Compute expiration time + $expiresAt = Carbon::now()->addSeconds($timeout); + + $model = $this->classMapper->createInstance($this->modelIdentifier); + + // Generate a random token + $model->setToken($this->generateRandomToken()); + + // Hash the password reset token for the stored version + $hash = hash($this->algorithm, $model->getToken()); + + $model->fill([ + 'hash' => $hash, + 'completed' => FALSE, + 'expires_at' => $expiresAt + ]); + + $model->user_id = $user->id; + + $model->save(); + + return $model; + } + + /** + * Determine if a specified user has an incomplete and unexpired token. + * + * @param User $user The user object to look up. + * @param int $token Optionally, try to match a specific token. + * @return Model|false + */ + public function exists(User $user, $token = NULL) { + $model = $this->classMapper + ->staticMethod($this->modelIdentifier, 'where', 'user_id', $user->id) + ->where('completed', FALSE) + ->where('expires_at', '>', Carbon::now()); + + if ($token) { + // get token hash + $hash = hash($this->algorithm, $token); + $model->where('hash', $hash); + } + + return $model->first() ?: FALSE; + } + + /** + * Delete all existing tokens from the database for a particular user. + * + * @param User $user + * @return int + */ + protected function removeExisting(User $user) { + return $this->classMapper + ->staticMethod($this->modelIdentifier, 'where', 'user_id', $user->id) + ->delete(); + } + + /** + * Remove all expired tokens from the database. + * + * @return bool|null + */ + public function removeExpired() { + return $this->classMapper + ->staticMethod($this->modelIdentifier, 'where', 'completed', FALSE) + ->where('expires_at', '<', Carbon::now()) + ->delete(); + } + + /** + * Generate a new random token for this user. + * + * This generates a token to use for verifying a new account, resetting a lost password, etc. + * @param string $gen specify an existing token that, if we happen to generate the same value, we should regenerate on. + * @return string + */ + protected function generateRandomToken($gen = NULL) { + do { + $gen = md5(uniqid(mt_rand(), FALSE)); + } while ($this->classMapper + ->staticMethod($this->modelIdentifier, 'where', 'hash', hash($this->algorithm, $gen)) + ->first()); + return $gen; + } + + /** + * Modify the user during the token completion process. + * + * This method is called during complete(), and is a way for concrete implementations to modify the user. + * @param User $user the user object to modify. + * @return mixed[] $args the list of parameters that were supplied to the call to `complete()` + */ + abstract protected function updateUser($user, $args); +} diff --git a/main/app/sprinkles/account/src/Repository/VerificationRepository.php b/main/app/sprinkles/account/src/Repository/VerificationRepository.php index d714dce..f7ee3e7 100644 --- a/main/app/sprinkles/account/src/Repository/VerificationRepository.php +++ b/main/app/sprinkles/account/src/Repository/VerificationRepository.php @@ -1,31 +1,31 @@ -flag_verified = 1; - $user->save(); - } -} +flag_verified = 1; + $user->save(); + } +} diff --git a/main/app/sprinkles/account/src/ServicesProvider/ServicesProvider.php b/main/app/sprinkles/account/src/ServicesProvider/ServicesProvider.php index 38d81d5..1615d2e 100644 --- a/main/app/sprinkles/account/src/ServicesProvider/ServicesProvider.php +++ b/main/app/sprinkles/account/src/ServicesProvider/ServicesProvider.php @@ -1,444 +1,444 @@ -extend('assets', function ($assets, $c) { - - // Register paths for user theme, if a user is logged in - // We catch any authorization-related exceptions, so that error pages can be rendered. - try { - /** @var UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ - $authenticator = $c->authenticator; - $currentUser = $c->currentUser; - } catch (\Exception $e) { - return $assets; - } - - if ($authenticator->check()) { - $c->sprinkleManager->addResource('assets', $currentUser->theme); - } - - return $assets; - }); - - /** - * Extend the 'classMapper' service to register model classes. - * - * Mappings added: User, Group, Role, Permission, Activity, PasswordReset, Verification - */ - $container->extend('classMapper', function ($classMapper, $c) { - $classMapper->setClassMapping('user', 'UserFrosting\Sprinkle\Account\Database\Models\User'); - $classMapper->setClassMapping('group', 'UserFrosting\Sprinkle\Account\Database\Models\Group'); - $classMapper->setClassMapping('role', 'UserFrosting\Sprinkle\Account\Database\Models\Role'); - $classMapper->setClassMapping('permission', 'UserFrosting\Sprinkle\Account\Database\Models\Permission'); - $classMapper->setClassMapping('activity', 'UserFrosting\Sprinkle\Account\Database\Models\Activity'); - $classMapper->setClassMapping('password_reset', 'UserFrosting\Sprinkle\Account\Database\Models\PasswordReset'); - $classMapper->setClassMapping('verification', 'UserFrosting\Sprinkle\Account\Database\Models\Verification'); - return $classMapper; - }); - - /** - * Extends the 'errorHandler' service with custom exception handlers. - * - * Custom handlers added: ForbiddenExceptionHandler - */ - $container->extend('errorHandler', function ($handler, $c) { - // Register the ForbiddenExceptionHandler. - $handler->registerHandler('\UserFrosting\Support\Exception\ForbiddenException', '\UserFrosting\Sprinkle\Account\Error\Handler\ForbiddenExceptionHandler'); - // Register the AuthExpiredExceptionHandler - $handler->registerHandler('\UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthExpiredException', '\UserFrosting\Sprinkle\Account\Error\Handler\AuthExpiredExceptionHandler'); - // Register the AuthCompromisedExceptionHandler. - $handler->registerHandler('\UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthCompromisedException', '\UserFrosting\Sprinkle\Account\Error\Handler\AuthCompromisedExceptionHandler'); - return $handler; - }); - - /** - * Extends the 'localePathBuilder' service, adding any locale files from the user theme. - * - */ - $container->extend('localePathBuilder', function ($pathBuilder, $c) { - // Add paths for user theme, if a user is logged in - // We catch any authorization-related exceptions, so that error pages can be rendered. - try { - /** @var UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ - $authenticator = $c->authenticator; - $currentUser = $c->currentUser; - } catch (\Exception $e) { - return $pathBuilder; - } - - if ($authenticator->check()) { - // Add paths to locale files for user theme - $themePath = $c->sprinkleManager->addResource('locale', $currentUser->theme); - - // Add user locale - $pathBuilder->addLocales($currentUser->locale); - } - - return $pathBuilder; - }); - - /** - * Extends the 'view' service with the AccountExtension for Twig. - * - * Adds account-specific functions, globals, filters, etc to Twig, and the path to templates for the user theme. - */ - $container->extend('view', function ($view, $c) { - $twig = $view->getEnvironment(); - $extension = new AccountExtension($c); - $twig->addExtension($extension); - - // Add paths for user theme, if a user is logged in - // We catch any authorization-related exceptions, so that error pages can be rendered. - try { - /** @var UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ - $authenticator = $c->authenticator; - $currentUser = $c->currentUser; - } catch (\Exception $e) { - return $view; - } - - if ($authenticator->check()) { - $theme = $currentUser->theme; - $themePath = $c->sprinkleManager->addResource('templates', $theme); - if ($themePath) { - $loader = $twig->getLoader(); - $loader->prependPath($themePath); - // Add namespaced path as well - $loader->addPath($themePath, $theme); - } - } - - return $view; - }); - - /** - * Authentication service. - * - * Supports logging in users, remembering their sessions, etc. - */ - $container['authenticator'] = function ($c) { - $classMapper = $c->classMapper; - $config = $c->config; - $session = $c->session; - $cache = $c->cache; - - // Force database connection to boot up - $c->db; - - // Fix RememberMe table name - $config['remember_me.table.tableName'] = Capsule::connection()->getTablePrefix() . $config['remember_me.table.tableName']; - - $authenticator = new Authenticator($classMapper, $session, $config, $cache); - return $authenticator; - }; - - /** - * Sets up the AuthGuard middleware, used to limit access to authenticated users for certain routes. - */ - $container['authGuard'] = function ($c) { - $authenticator = $c->authenticator; - return new AuthGuard($authenticator); - }; - - /** - * Authorization check logging with Monolog. - * - * Extend this service to push additional handlers onto the 'auth' log stack. - */ - $container['authLogger'] = function ($c) { - $logger = new Logger('auth'); - - $logFile = $c->get('locator')->findResource('log://userfrosting.log', TRUE, TRUE); - - $handler = new StreamHandler($logFile); - - $formatter = new MixedFormatter(NULL, NULL, TRUE); - - $handler->setFormatter($formatter); - $logger->pushHandler($handler); - - return $logger; - }; - - /** - * Authorization service. - * - * Determines permissions for user actions. Extend this service to add additional access condition callbacks. - */ - $container['authorizer'] = function ($c) { - $config = $c->config; - - // Default access condition callbacks. Add more in your sprinkle by using $container->extend(...) - $callbacks = [ - /** - * Unconditionally grant permission - use carefully! - * @return bool returns true no matter what. - */ - 'always' => function () { - return TRUE; - }, - - /** - * Check if the specified values are identical to one another (strict comparison). - * @param mixed $val1 the first value to compare. - * @param mixed $val2 the second value to compare. - * @return bool true if the values are strictly equal, false otherwise. - */ - 'equals' => function ($val1, $val2) { - return ($val1 === $val2); - }, - - /** - * Check if the specified values are numeric, and if so, if they are equal to each other. - * @param mixed $val1 the first value to compare. - * @param mixed $val2 the second value to compare. - * @return bool true if the values are numeric and equal, false otherwise. - */ - 'equals_num' => function ($val1, $val2) { - if (!is_numeric($val1)) { - return FALSE; - } - if (!is_numeric($val2)) { - return FALSE; - } - - return ($val1 == $val2); - }, - - /** - * Check if the specified user (by user_id) has a particular role. - * - * @param int $user_id the id of the user. - * @param int $role_id the id of the role. - * @return bool true if the user has the role, false otherwise. - */ - 'has_role' => function ($user_id, $role_id) { - return Capsule::table('role_users') - ->where('user_id', $user_id) - ->where('role_id', $role_id) - ->count() > 0; - }, - - /** - * Check if the specified value $needle is in the values of $haystack. - * - * @param mixed $needle the value to look for in $haystack - * @param array[mixed] $haystack the array of values to search. - * @return bool true if $needle is present in the values of $haystack, false otherwise. - */ - 'in' => function ($needle, $haystack) { - return in_array($needle, $haystack); - }, - - /** - * Check if the specified user (by user_id) is in a particular group. - * - * @param int $user_id the id of the user. - * @param int $group_id the id of the group. - * @return bool true if the user is in the group, false otherwise. - */ - 'in_group' => function ($user_id, $group_id) { - $user = User::find($user_id); - return ($user->group_id == $group_id); - }, - - /** - * Check if the specified user (by user_id) is the master user. - * - * @param int $user_id the id of the user. - * @return bool true if the user id is equal to the id of the master account, false otherwise. - */ - 'is_master' => function ($user_id) use ($config) { - // Need to use loose comparison for now, because some DBs return `id` as a string - return ($user_id == $config['reserved_user_ids.master']); - }, - - /** - * Check if all values in the array $needle are present in the values of $haystack. - * - * @param array[mixed] $needle the array whose values we should look for in $haystack - * @param array[mixed] $haystack the array of values to search. - * @return bool true if every value in $needle is present in the values of $haystack, false otherwise. - */ - 'subset' => function ($needle, $haystack) { - return count($needle) == count(array_intersect($needle, $haystack)); - }, - - /** - * Check if all keys of the array $needle are present in the values of $haystack. - * - * This function is useful for whitelisting an array of key-value parameters. - * @param array[mixed] $needle the array whose keys we should look for in $haystack - * @param array[mixed] $haystack the array of values to search. - * @return bool true if every key in $needle is present in the values of $haystack, false otherwise. - */ - 'subset_keys' => function ($needle, $haystack) { - return count($needle) == count(array_intersect(array_keys($needle), $haystack)); - } - ]; - - $authorizer = new AuthorizationManager($c, $callbacks); - return $authorizer; - }; - - /** - * Loads the User object for the currently logged-in user. - */ - $container['currentUser'] = function ($c) { - $authenticator = $c->authenticator; - - return $authenticator->user(); - }; - - $container['passwordHasher'] = function ($c) { - $hasher = new Hasher(); - return $hasher; - }; - - /** - * Returns a callback that forwards to dashboard if user is already logged in. - */ - $container['redirect.onAlreadyLoggedIn'] = function ($c) { - /** - * This method is invoked when a user attempts to perform certain public actions when they are already logged in. - * - * Forward to user's landing page or last visited page - * @param \Psr\Http\Message\ServerRequestInterface $request - * @param \Psr\Http\Message\ResponseInterface $response - * @param array $args - * @return \Psr\Http\Message\ResponseInterface - */ - return function (Request $request, Response $response, array $args) use ($c) { - $redirect = $c->router->pathFor('dashboard'); - - return $response->withRedirect($redirect, 302); - }; - }; - - /** - * Returns a callback that handles setting the `UF-Redirect` header after a successful login. - */ - $container['redirect.onLogin'] = function ($c) { - /** - * This method is invoked when a user completes the login process. - * - * Returns a callback that handles setting the `UF-Redirect` header after a successful login. - * @param \Psr\Http\Message\ServerRequestInterface $request - * @param \Psr\Http\Message\ResponseInterface $response - * @param array $args - * @return \Psr\Http\Message\ResponseInterface - */ - return function (Request $request, Response $response, array $args) use ($c) { - // Backwards compatibility for the deprecated determineRedirectOnLogin service - if ($c->has('determineRedirectOnLogin')) { - $determineRedirectOnLogin = $c->determineRedirectOnLogin; - - return $determineRedirectOnLogin($response)->withStatus(200); - } - - /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ - $authorizer = $c->authorizer; - - $currentUser = $c->authenticator->user(); - - if ($authorizer->checkAccess($currentUser, 'uri_account_settings')) { - return $response->withHeader('UF-Redirect', $c->router->pathFor('settings')); - } else { - return $response->withHeader('UF-Redirect', $c->router->pathFor('index')); - } - }; - }; - - /** - * Repository for password reset requests. - */ - $container['repoPasswordReset'] = function ($c) { - $classMapper = $c->classMapper; - $config = $c->config; - - $repo = new PasswordResetRepository($classMapper, $config['password_reset.algorithm']); - return $repo; - }; - - /** - * Repository for verification requests. - */ - $container['repoVerification'] = function ($c) { - $classMapper = $c->classMapper; - $config = $c->config; - - $repo = new VerificationRepository($classMapper, $config['verification.algorithm']); - return $repo; - }; - - /** - * Logger for logging the current user's activities to the database. - * - * Extend this service to push additional handlers onto the 'userActivity' log stack. - */ - $container['userActivityLogger'] = function ($c) { - $classMapper = $c->classMapper; - $config = $c->config; - $session = $c->session; - - $logger = new Logger('userActivity'); - - $handler = new UserActivityDatabaseHandler($classMapper, 'activity'); - - // Note that we get the user id from the session, not the currentUser service. - // This is because the currentUser service may not reflect the actual user during login/logout requests. - $currentUserIdKey = $config['session.keys.current_user_id']; - $userId = isset($session[$currentUserIdKey]) ? $session[$currentUserIdKey] : $config['reserved_user_ids.guest']; - $processor = new UserActivityProcessor($userId); - - $logger->pushProcessor($processor); - $logger->pushHandler($handler); - - return $logger; - }; - } -} +extend('assets', function ($assets, $c) { + + // Register paths for user theme, if a user is logged in + // We catch any authorization-related exceptions, so that error pages can be rendered. + try { + /** @var UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ + $authenticator = $c->authenticator; + $currentUser = $c->currentUser; + } catch (\Exception $e) { + return $assets; + } + + if ($authenticator->check()) { + $c->sprinkleManager->addResource('assets', $currentUser->theme); + } + + return $assets; + }); + + /** + * Extend the 'classMapper' service to register model classes. + * + * Mappings added: User, Group, Role, Permission, Activity, PasswordReset, Verification + */ + $container->extend('classMapper', function ($classMapper, $c) { + $classMapper->setClassMapping('user', 'UserFrosting\Sprinkle\Account\Database\Models\User'); + $classMapper->setClassMapping('group', 'UserFrosting\Sprinkle\Account\Database\Models\Group'); + $classMapper->setClassMapping('role', 'UserFrosting\Sprinkle\Account\Database\Models\Role'); + $classMapper->setClassMapping('permission', 'UserFrosting\Sprinkle\Account\Database\Models\Permission'); + $classMapper->setClassMapping('activity', 'UserFrosting\Sprinkle\Account\Database\Models\Activity'); + $classMapper->setClassMapping('password_reset', 'UserFrosting\Sprinkle\Account\Database\Models\PasswordReset'); + $classMapper->setClassMapping('verification', 'UserFrosting\Sprinkle\Account\Database\Models\Verification'); + return $classMapper; + }); + + /** + * Extends the 'errorHandler' service with custom exception handlers. + * + * Custom handlers added: ForbiddenExceptionHandler + */ + $container->extend('errorHandler', function ($handler, $c) { + // Register the ForbiddenExceptionHandler. + $handler->registerHandler('\UserFrosting\Support\Exception\ForbiddenException', '\UserFrosting\Sprinkle\Account\Error\Handler\ForbiddenExceptionHandler'); + // Register the AuthExpiredExceptionHandler + $handler->registerHandler('\UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthExpiredException', '\UserFrosting\Sprinkle\Account\Error\Handler\AuthExpiredExceptionHandler'); + // Register the AuthCompromisedExceptionHandler. + $handler->registerHandler('\UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthCompromisedException', '\UserFrosting\Sprinkle\Account\Error\Handler\AuthCompromisedExceptionHandler'); + return $handler; + }); + + /** + * Extends the 'localePathBuilder' service, adding any locale files from the user theme. + * + */ + $container->extend('localePathBuilder', function ($pathBuilder, $c) { + // Add paths for user theme, if a user is logged in + // We catch any authorization-related exceptions, so that error pages can be rendered. + try { + /** @var UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ + $authenticator = $c->authenticator; + $currentUser = $c->currentUser; + } catch (\Exception $e) { + return $pathBuilder; + } + + if ($authenticator->check()) { + // Add paths to locale files for user theme + $themePath = $c->sprinkleManager->addResource('locale', $currentUser->theme); + + // Add user locale + $pathBuilder->addLocales($currentUser->locale); + } + + return $pathBuilder; + }); + + /** + * Extends the 'view' service with the AccountExtension for Twig. + * + * Adds account-specific functions, globals, filters, etc to Twig, and the path to templates for the user theme. + */ + $container->extend('view', function ($view, $c) { + $twig = $view->getEnvironment(); + $extension = new AccountExtension($c); + $twig->addExtension($extension); + + // Add paths for user theme, if a user is logged in + // We catch any authorization-related exceptions, so that error pages can be rendered. + try { + /** @var UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ + $authenticator = $c->authenticator; + $currentUser = $c->currentUser; + } catch (\Exception $e) { + return $view; + } + + if ($authenticator->check()) { + $theme = $currentUser->theme; + $themePath = $c->sprinkleManager->addResource('templates', $theme); + if ($themePath) { + $loader = $twig->getLoader(); + $loader->prependPath($themePath); + // Add namespaced path as well + $loader->addPath($themePath, $theme); + } + } + + return $view; + }); + + /** + * Authentication service. + * + * Supports logging in users, remembering their sessions, etc. + */ + $container['authenticator'] = function ($c) { + $classMapper = $c->classMapper; + $config = $c->config; + $session = $c->session; + $cache = $c->cache; + + // Force database connection to boot up + $c->db; + + // Fix RememberMe table name + $config['remember_me.table.tableName'] = Capsule::connection()->getTablePrefix() . $config['remember_me.table.tableName']; + + $authenticator = new Authenticator($classMapper, $session, $config, $cache); + return $authenticator; + }; + + /** + * Sets up the AuthGuard middleware, used to limit access to authenticated users for certain routes. + */ + $container['authGuard'] = function ($c) { + $authenticator = $c->authenticator; + return new AuthGuard($authenticator); + }; + + /** + * Authorization check logging with Monolog. + * + * Extend this service to push additional handlers onto the 'auth' log stack. + */ + $container['authLogger'] = function ($c) { + $logger = new Logger('auth'); + + $logFile = $c->get('locator')->findResource('log://userfrosting.log', TRUE, TRUE); + + $handler = new StreamHandler($logFile); + + $formatter = new MixedFormatter(NULL, NULL, TRUE); + + $handler->setFormatter($formatter); + $logger->pushHandler($handler); + + return $logger; + }; + + /** + * Authorization service. + * + * Determines permissions for user actions. Extend this service to add additional access condition callbacks. + */ + $container['authorizer'] = function ($c) { + $config = $c->config; + + // Default access condition callbacks. Add more in your sprinkle by using $container->extend(...) + $callbacks = [ + /** + * Unconditionally grant permission - use carefully! + * @return bool returns true no matter what. + */ + 'always' => function () { + return TRUE; + }, + + /** + * Check if the specified values are identical to one another (strict comparison). + * @param mixed $val1 the first value to compare. + * @param mixed $val2 the second value to compare. + * @return bool true if the values are strictly equal, false otherwise. + */ + 'equals' => function ($val1, $val2) { + return ($val1 === $val2); + }, + + /** + * Check if the specified values are numeric, and if so, if they are equal to each other. + * @param mixed $val1 the first value to compare. + * @param mixed $val2 the second value to compare. + * @return bool true if the values are numeric and equal, false otherwise. + */ + 'equals_num' => function ($val1, $val2) { + if (!is_numeric($val1)) { + return FALSE; + } + if (!is_numeric($val2)) { + return FALSE; + } + + return ($val1 == $val2); + }, + + /** + * Check if the specified user (by user_id) has a particular role. + * + * @param int $user_id the id of the user. + * @param int $role_id the id of the role. + * @return bool true if the user has the role, false otherwise. + */ + 'has_role' => function ($user_id, $role_id) { + return Capsule::table('role_users') + ->where('user_id', $user_id) + ->where('role_id', $role_id) + ->count() > 0; + }, + + /** + * Check if the specified value $needle is in the values of $haystack. + * + * @param mixed $needle the value to look for in $haystack + * @param array[mixed] $haystack the array of values to search. + * @return bool true if $needle is present in the values of $haystack, false otherwise. + */ + 'in' => function ($needle, $haystack) { + return in_array($needle, $haystack); + }, + + /** + * Check if the specified user (by user_id) is in a particular group. + * + * @param int $user_id the id of the user. + * @param int $group_id the id of the group. + * @return bool true if the user is in the group, false otherwise. + */ + 'in_group' => function ($user_id, $group_id) { + $user = User::find($user_id); + return ($user->group_id == $group_id); + }, + + /** + * Check if the specified user (by user_id) is the master user. + * + * @param int $user_id the id of the user. + * @return bool true if the user id is equal to the id of the master account, false otherwise. + */ + 'is_master' => function ($user_id) use ($config) { + // Need to use loose comparison for now, because some DBs return `id` as a string + return ($user_id == $config['reserved_user_ids.master']); + }, + + /** + * Check if all values in the array $needle are present in the values of $haystack. + * + * @param array[mixed] $needle the array whose values we should look for in $haystack + * @param array[mixed] $haystack the array of values to search. + * @return bool true if every value in $needle is present in the values of $haystack, false otherwise. + */ + 'subset' => function ($needle, $haystack) { + return count($needle) == count(array_intersect($needle, $haystack)); + }, + + /** + * Check if all keys of the array $needle are present in the values of $haystack. + * + * This function is useful for whitelisting an array of key-value parameters. + * @param array[mixed] $needle the array whose keys we should look for in $haystack + * @param array[mixed] $haystack the array of values to search. + * @return bool true if every key in $needle is present in the values of $haystack, false otherwise. + */ + 'subset_keys' => function ($needle, $haystack) { + return count($needle) == count(array_intersect(array_keys($needle), $haystack)); + } + ]; + + $authorizer = new AuthorizationManager($c, $callbacks); + return $authorizer; + }; + + /** + * Loads the User object for the currently logged-in user. + */ + $container['currentUser'] = function ($c) { + $authenticator = $c->authenticator; + + return $authenticator->user(); + }; + + $container['passwordHasher'] = function ($c) { + $hasher = new Hasher(); + return $hasher; + }; + + /** + * Returns a callback that forwards to dashboard if user is already logged in. + */ + $container['redirect.onAlreadyLoggedIn'] = function ($c) { + /** + * This method is invoked when a user attempts to perform certain public actions when they are already logged in. + * + * Forward to user's landing page or last visited page + * @param \Psr\Http\Message\ServerRequestInterface $request + * @param \Psr\Http\Message\ResponseInterface $response + * @param array $args + * @return \Psr\Http\Message\ResponseInterface + */ + return function (Request $request, Response $response, array $args) use ($c) { + $redirect = $c->router->pathFor('dashboard'); + + return $response->withRedirect($redirect, 302); + }; + }; + + /** + * Returns a callback that handles setting the `UF-Redirect` header after a successful login. + */ + $container['redirect.onLogin'] = function ($c) { + /** + * This method is invoked when a user completes the login process. + * + * Returns a callback that handles setting the `UF-Redirect` header after a successful login. + * @param \Psr\Http\Message\ServerRequestInterface $request + * @param \Psr\Http\Message\ResponseInterface $response + * @param array $args + * @return \Psr\Http\Message\ResponseInterface + */ + return function (Request $request, Response $response, array $args) use ($c) { + // Backwards compatibility for the deprecated determineRedirectOnLogin service + if ($c->has('determineRedirectOnLogin')) { + $determineRedirectOnLogin = $c->determineRedirectOnLogin; + + return $determineRedirectOnLogin($response)->withStatus(200); + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $c->authorizer; + + $currentUser = $c->authenticator->user(); + + if ($authorizer->checkAccess($currentUser, 'uri_account_settings')) { + return $response->withHeader('UF-Redirect', $c->router->pathFor('settings')); + } else { + return $response->withHeader('UF-Redirect', $c->router->pathFor('index')); + } + }; + }; + + /** + * Repository for password reset requests. + */ + $container['repoPasswordReset'] = function ($c) { + $classMapper = $c->classMapper; + $config = $c->config; + + $repo = new PasswordResetRepository($classMapper, $config['password_reset.algorithm']); + return $repo; + }; + + /** + * Repository for verification requests. + */ + $container['repoVerification'] = function ($c) { + $classMapper = $c->classMapper; + $config = $c->config; + + $repo = new VerificationRepository($classMapper, $config['verification.algorithm']); + return $repo; + }; + + /** + * Logger for logging the current user's activities to the database. + * + * Extend this service to push additional handlers onto the 'userActivity' log stack. + */ + $container['userActivityLogger'] = function ($c) { + $classMapper = $c->classMapper; + $config = $c->config; + $session = $c->session; + + $logger = new Logger('userActivity'); + + $handler = new UserActivityDatabaseHandler($classMapper, 'activity'); + + // Note that we get the user id from the session, not the currentUser service. + // This is because the currentUser service may not reflect the actual user during login/logout requests. + $currentUserIdKey = $config['session.keys.current_user_id']; + $userId = isset($session[$currentUserIdKey]) ? $session[$currentUserIdKey] : $config['reserved_user_ids.guest']; + $processor = new UserActivityProcessor($userId); + + $logger->pushProcessor($processor); + $logger->pushHandler($handler); + + return $logger; + }; + } +} diff --git a/main/app/sprinkles/account/src/Twig/AccountExtension.php b/main/app/sprinkles/account/src/Twig/AccountExtension.php index 287f879..fc94a1a 100644 --- a/main/app/sprinkles/account/src/Twig/AccountExtension.php +++ b/main/app/sprinkles/account/src/Twig/AccountExtension.php @@ -1,62 +1,62 @@ -services = $services; - $this->config = $services->config; - } - - public function getName() { - return 'userfrosting/account'; - } - - public function getFunctions() { - return array( - // Add Twig function for checking permissions during dynamic menu rendering - new \Twig_SimpleFunction('checkAccess', function ($slug, $params = []) { - $authorizer = $this->services->authorizer; - $currentUser = $this->services->currentUser; - - return $authorizer->checkAccess($currentUser, $slug, $params); - }), - new \Twig_SimpleFunction('checkAuthenticated', function () { - $authenticator = $this->services->authenticator; - return $authenticator->check(); - }) - ); - } - - public function getGlobals() { - try { - $currentUser = $this->services->currentUser; - } catch (\Exception $e) { - $currentUser = NULL; - } - - return [ - 'current_user' => $currentUser - ]; - } -} +services = $services; + $this->config = $services->config; + } + + public function getName() { + return 'userfrosting/account'; + } + + public function getFunctions() { + return array( + // Add Twig function for checking permissions during dynamic menu rendering + new \Twig_SimpleFunction('checkAccess', function ($slug, $params = []) { + $authorizer = $this->services->authorizer; + $currentUser = $this->services->currentUser; + + return $authorizer->checkAccess($currentUser, $slug, $params); + }), + new \Twig_SimpleFunction('checkAuthenticated', function () { + $authenticator = $this->services->authenticator; + return $authenticator->check(); + }) + ); + } + + public function getGlobals() { + try { + $currentUser = $this->services->currentUser; + } catch (\Exception $e) { + $currentUser = NULL; + } + + return [ + 'current_user' => $currentUser + ]; + } +} diff --git a/main/app/sprinkles/account/src/Util/HashFailedException.php b/main/app/sprinkles/account/src/Util/HashFailedException.php index 765096b..580bed1 100644 --- a/main/app/sprinkles/account/src/Util/HashFailedException.php +++ b/main/app/sprinkles/account/src/Util/HashFailedException.php @@ -1,22 +1,22 @@ -staticMethod('user', 'where', 'user_name', $suggestion)->first()) { - return $suggestion; - } - } - } - - return ''; - } - -} +staticMethod('user', 'where', 'user_name', $suggestion)->first()) { + return $suggestion; + } + } + } + + return ''; + } + +} -- cgit v1.2.3