diff --git a/Libraries/Aauth.php b/Libraries/Aauth.php index 8dcbfa6..1839dc5 100644 --- a/Libraries/Aauth.php +++ b/Libraries/Aauth.php @@ -1,20 +1,108 @@ config = new Magefly\Aauth\Config\Aauth(); +/** + * Aauth is a User Authorization Library for CodeIgniter 4.x, which aims to make + * easy some essential jobs such as login, permissions and access operations. + * Despite ease of use, it has also very advanced features like groupping, + * access management, public access etc.. + * + * @package Aauth + * @author Magefly Team + * @copyright 2018 Magefly + * @copyright 2014-2018 British Columbia Institute of Technology (https://bcit.ca/) + * @license https://opensource.org/licenses/MIT MIT License + * @link https://github.com/magefly/CodeIgniter-Aauth + * @version 3.0.0-pre + * + * @todo separate (on some level) the unvalidated users from the "banned" users + */ +class Aauth +{ + /** + * Variable for loading the config array into + * + * @var object + */ + private $config; + + /** + * Variable for loading the session service into + * + * @var object + */ + private $session; + + /** + * Array to store error messages + * + * @var array + */ + private $errors = []; + + /** + * Local temporary storage for current flash errors + * + * Used to update current flash data list since flash data is only available on the next page refresh + * + * @var array + */ + private $flashErrors = []; + + /** + * Array to store info messages + * + * @var array + */ + private $infos = []; + + /** + * Local temporary storage for current flash infos + * + * Used to update current flash data list since flash data is only available on the next page refresh + * + * @var array + */ + private $flashInfos = []; + + /** + * Array to cache permission-ids. + * + * @var array + */ + private $cachePermId = []; + + /** + * Array to cache group-ids. + * + * @var array + */ + private $cacheGroupId = []; + + /** + * Constructor + * + * Prepares config & session variable. + */ + function __construct() + { + $this->config = new \Magefly\Aauth\Config\Aauth(); $this->session = \Config\Services::session(); } + /** + * Create user + * + * Creates a new user + * + * @param string $email User's email address + * @param string $password User's password + * @param string $username User's username + * + * @return int|bool False if create fails or returns user id if successful + */ public function createUser($email, $password, $username = null) { - $user = new \Magefly\Aauth\Models\UserModel(); + $userModel = new \Magefly\Aauth\Models\UserModel(); $data['email'] = $email; $data['password'] = $password; @@ -24,32 +112,43 @@ class Aauth { $data['username'] = $username; } - if ($user_id = $user->insert($data)) + if ($userId = $userModel->insert($data)) { - return $user_id; + return $userId; } - $this->error($user->errors()); + $this->error($userModel->errors()); return false; } - public function updateUser($user_id, $email = null, $password = null, $username = null) + /** + * Update user + * + * Updates existing user details + * + * @param int $user_id User id to update + * @param string|bool $email User's email address, or FALSE if not to be updated + * @param string|bool $password User's password, or FALSE if not to be updated + * @param string|bool $name User's name, or FALSE if not to be updated + * + * @return bool Update fails/succeeds + */ + public function updateUser($userId, $email = null, $password = null, $username = null) { - $user = new \Magefly\Aauth\Models\UserModel(); + $userModel = new \Magefly\Aauth\Models\UserModel(); $data = []; - if ( ! $user->exists($user_id)) + if ( ! $userModel->existsById($userId)) { $this->error(lang('Aauth.notFoundUser')); return false; } - - if ( ! $email && ! $password && ! $username) + else if ( ! $email && ! $password && ! $username) { - return false; + return false; } - $data['id'] = $user_id; + $data['id'] = $userId; if ($email) { @@ -66,149 +165,192 @@ class Aauth { $data['username'] = $username; } - if ($user->update($user_id, $data)) + if ($userModel->update($userId, $data)) { - return $user_id; + return true; } - $this->error($user->errors()); + $this->error($userModel->errors()); return false; } - public function deleteUser(int $user_id) + /** + * Delete user + * + */ + public function deleteUser(int $userId) { - $user = new \Magefly\Aauth\Models\UserModel(); + $userModel = new \Magefly\Aauth\Models\UserModel(); $data = []; - if ( ! $user->exists($user_id)) + if ( ! $userModel->existsById($userId)) { $this->error(lang('Aauth.notFoundUser')); return false; } - - if ($user->delete($user_id)) + else if ($userModel->delete($userId)) { return true; } } - public function listUsers(int $limit = 0, int $offset = 0, bool $include_banneds = null, array $order_by = null) + /** + * List users + * + * Return users as an object array + * + * @todo bool|int $group_par Specify group id to list group or FALSE for all users + * + * @param string $limit Limit of users to be returned + * @param bool $offset Offset for limited number of users + * @param bool $include_banneds Include banned users + * @param string $sort Order by MYSQL string (e.g. 'name ASC', 'email DESC') + * + * @return array Array of users + */ + public function listUsers(int $limit = 0, int $offset = 0, bool $includeBanneds = null, array $orderBy = null) { - $user = new \Magefly\Aauth\Models\UserModel(); + $userModel = new \Magefly\Aauth\Models\UserModel(); $options = []; + $user = $userModel->limit($limit, $offset); // bool $group_par = null, - if ( ! $include_banneds) + if ( ! $includeBanneds) { - $options['where'] = ['banned' => 0]; + $user->where('banned', 0); } - if ($order_by) + if ($orderBy) { - $options['order_by'] = $order_by; + $user->orderBy($orderBy[0], $orderBy[1]); } - return $user->findAllExtra($limit, $offset, $options); + return $user->findAll(); } - public function login(string $identifier, string $password, bool $remember = null, bool $totp_code = null) + + /** + * Login user + * + * Check provided details against the database. Add items to error array on fail, create session if success + * + * @todo add TOTP + * @todo add reCAPTCHA + * @todo add Remeber Cookie aka LoginToken (new DB) + * + * @param string $email + * @param string $pass + * @param bool $remember + * @param bool $totpCode + * + * @return bool Indicates successful login. + */ + public function login(string $identifier, string $password, bool $remember = null, bool $totpCode = null) { - $user = new \Magefly\Aauth\Models\UserModel(); - $loginAttempt = new \Magefly\Aauth\Models\LoginAttemptModel(); + $userModel = new \Magefly\Aauth\Models\UserModel(); + $loginAttemptModel = new \Magefly\Aauth\Models\LoginAttemptModel(); + $userVariableModel = new \Magefly\Aauth\Models\UserVariableModel(); helper('cookie'); delete_cookie('user'); - - if ($this->config->loginProtection && ! $loginAttempt->update()) + if ($this->config->loginProtection && ! $loginAttemptModel->update()) { $this->error(lang('Aauth.loginAttemptsExceeded')); return false; } - // if($this->config_vars['ddos_protection'] && $this->config_vars['recaptcha_active'] && $loginAttempt->get() > $this->config_vars['recaptcha_login_attempts']){ + // if($this->config->ddos_protection && $this->config->recaptcha_active && $loginAttempts->get() > $this->config->recaptcha_login_attempts){ // $this->CI->load->helper('recaptchalib'); - // $reCaptcha = new ReCaptcha( $this->config_vars['recaptcha_secret']); + // $reCaptcha = new ReCaptcha( $this->config->recaptcha_secret); // $resp = $reCaptcha->verifyResponse( $this->CI->input->server("REMOTE_ADDR"), $this->CI->input->post("g-recaptcha-response") ); // if( ! $resp->success){ // $this->error($this->CI->lang->line('aauth_error_recaptcha_not_correct')); - // return FALSE; + // return false; // } // } - if ($this->config->loginUseUsername) - { + + if ($this->config->loginUseUsername) + { if ( ! $identifier OR strlen($password) < $this->config->passwordMin OR strlen($password) > $this->config->passwordMax) { $this->error(lang('Aauth.loginFailedName')); - return FALSE; + return false; } - $db_identifier = 'username'; - - }else{ - $validation = \Config\Services::validation(); + if ($user = $userModel->where('username', $identifier)->first()) + { + $this->error(lang('Aauth.notFoundUser')); + return false; + } + } + else + { + $validation = \Config\Services::validation(); - if( ! $validation->check($identifier, 'valid_email') OR strlen($password) < $this->config->passwordMin OR strlen($password) > $this->config->passwordMax) + if ( ! $validation->check($identifier, 'valid_email') OR strlen($password) < $this->config->passwordMin OR strlen($password) > $this->config->passwordMax) { $this->error(lang('Aauth.loginFailedEmail')); - return FALSE; + return false; + } + + if ( ! $user = $userModel->where('email', $identifier)->first()) + { + $this->error(lang('Aauth.notFoundUser')); + return false; } - $db_identifier = 'email'; - } - - $query = null; - $query = $this->aauth_db->where($db_identifier, $identifier); - $query = $this->aauth_db->where('banned', 1); - $query = $this->aauth_db->where('verification_code !=', ''); - $query = $this->aauth_db->get($this->config_vars['users']); - if ($query->num_rows() > 0) { - $this->error($this->CI->lang->line('aauth_error_account_not_verified')); - return FALSE; } - // to find user id, create sessions and cookies - $query = $this->aauth_db->where($db_identifier, $identifier); - $query = $this->aauth_db->get($this->config_vars['users']); - if($query->num_rows() == 0){ - $this->error($this->CI->lang->line('aauth_error_no_user')); - return FALSE; + + + if ($user['banned'] && ! empty($userVariableModel->get($user['id'], 'verification_code', true))) + { + $this->error(lang('Aauth.notVerified')); + return false; } - if($this->config_vars['totp_active'] == TRUE AND $this->config_vars['totp_only_on_ip_change'] == FALSE AND $this->config_vars['totp_two_step_login_active'] == TRUE){ - if($this->config_vars['totp_two_step_login_active'] == TRUE){ - $this->CI->session->set_userdata('totp_required', true); + else if ($user['banned']) + { + $this->error(lang('Aauth.invalidUserBanned')); + return false; + } + + if ($this->config->totpEnabled && ! $this->config->totpOnIpChange && $this->config->totpLogin) + { + if ($this->config->totpLogin == true) + { + $this->session->set('totp_required', true); } - $query = null; - $query = $this->aauth_db->where($db_identifier, $identifier); - $query = $this->aauth_db->get($this->config_vars['users']); - $totp_secret = $query->row()->totp_secret; - if ($query->num_rows() > 0 AND !$totp_code) { - $this->error($this->CI->lang->line('aauth_error_totp_code_required')); - return FALSE; - }else { - if(!empty($totp_secret)){ + + $totp_secret = $userVariableModel->get($user['id'], 'totp_secret', true); + if ( ! empty($totp_secret) && ! $totp_code) { + $this->error(lang('Aauth.requiredTOTPCode')); + return false; + } else { + if( ! empty($totp_secret)){ $this->CI->load->helper('googleauthenticator'); $ga = new PHPGangsta_GoogleAuthenticator(); $checkResult = $ga->verifyCode($totp_secret, $totp_code, 0); - if (!$checkResult) { - $this->error($this->CI->lang->line('aauth_error_totp_code_invalid')); - return FALSE; + if ( ! $checkResult) { + $this->error(lang('Aauth.invalidTOTPCode')); + return false; } } } - } - if($this->config_vars['totp_active'] == TRUE AND $this->config_vars['totp_only_on_ip_change'] == TRUE){ + } + else if ($this->config->totpEnabled && $this->config->totpOnIpChange) + { $query = null; $query = $this->aauth_db->where($db_identifier, $identifier); - $query = $this->aauth_db->get($this->config_vars['users']); + $query = $this->aauth_db->get($this->config->users); $totp_secret = $query->row()->totp_secret; $ip_address = $query->row()->ip_address; $current_ip_address = $this->CI->input->ip_address(); if ($query->num_rows() > 0 AND !$totp_code) { if($ip_address != $current_ip_address ){ - if($this->config_vars['totp_two_step_login_active'] == FALSE){ + if($this->config->totpLogin == false){ $this->error($this->CI->lang->line('aauth_error_totp_code_required')); - return FALSE; - } else if($this->config_vars['totp_two_step_login_active'] == TRUE){ - $this->CI->session->set_userdata('totp_required', true); + return false; + } else if($this->config->totpLogin == true){ + $this->session->set('totp_required', true); } } }else { @@ -219,87 +361,119 @@ class Aauth { $checkResult = $ga->verifyCode($totp_secret, $totp_code, 0); if (!$checkResult) { $this->error($this->CI->lang->line('aauth_error_totp_code_invalid')); - return FALSE; + return false; } } } } - } - $query = null; - $query = $this->aauth_db->where($db_identifier, $identifier); - $query = $this->aauth_db->where('banned', 0); - $query = $this->aauth_db->get($this->config_vars['users']); - $row = $query->row(); - // if email and pass matches and not banned - $password = ($this->config_vars['use_password_hash'] ? $password : $this->hash_password($password, $row->id)); - if ( $query->num_rows() != 0 && $this->verify_password($password, $row->password) ) { - // If email and pass matches - // create session - $data = array( - 'id' => $row->id, - 'username' => $row->username, - 'email' => $row->email, - 'loggedin' => TRUE - ); - $this->CI->session->set_userdata($data); - if ( $remember ){ - $this->CI->load->helper('string'); - $expire = $this->config_vars['remember']; - $today = date("Y-m-d"); - $remember_date = date("Y-m-d", strtotime($today . $expire) ); - $random_string = random_string('alnum', 16); - $this->update_remember($row->id, $random_string, $remember_date ); - $cookie = array( - 'name' => 'user', - 'value' => $row->id . "-" . $random_string, - 'expire' => 99*999*999, - 'path' => '/', - ); - $this->CI->input->set_cookie($cookie); - } - // update last login - $this->update_last_login($row->id); - $this->update_activity(); - if($this->config_vars['remove_successful_attempts'] == TRUE){ - $this->reset_login_attempts(); + } + + if ( ! $user['banned'] && password_verify($password, $user['password'])) + { + $data = [ + 'id' => $user['id'], + 'username' => $user['username'], + 'email' => $user['email'], + 'loggedin' => true + ]; + + $this->session->set($data); + + // if ( $remember ){ + // helper('text'); + // $this->CI->load->helper('string'); + // $expire = $this->config->loginRemember; + // $remember_date = date("Y-m-d", strtotime($expire) ); + // $random_string = random_string('alnum', 16); + // $this->updateRemember($row->id, $random_string, $remember_date ); + // $cookie = array( + // 'name' => 'user', + // 'value' => $row->id . "-" . $random_string, + // 'expire' => 99*999*999, + // 'path' => '/', + // ); + // $this->CI->input->set_cookie($cookie); + // } + + $userModel->updateLastLogin($user['id']); + $userModel->updateLastActivity($user['id']); + + if ($this->config->loginAttemptRemoveSuccessful) + { + $loginAttemptModel->delete(); } - return TRUE; + + return true; } - // if not matches - else { - $this->error($this->CI->lang->line('aauth_error_login_failed_all')); - return FALSE; + else + { + $this->error(lang('Aauth.loginFailedAll')); + return false; } } + /** + * Error + * + * Add message to error array and set flash data + * + * @param string $message Message to add to array + * @param bool $flashdata if TRUE add $message to CI flashdata (deflault: FALSE) + */ public function error($message = '', $flashdata = null) { $this->errors[] = $message; if ($flashdata) { - $this->flash_errors[] = $message; - $this->session->set('errors', $this->flash_errors); + $this->flashErrors[] = $message; + $this->session->set('errors', $this->flashErrors); $this->session->setFlashdata('errors'); } } + /** + * Keep Errors + * + * Keeps the flashdata errors for one more page refresh. Optionally adds the default errors into the + * flashdata list. This should be called last in your controller, and with care as it could continue + * to revive all errors and not let them expire as intended. + * Benefitial when using Ajax Requests + * + * @see http://ellislab.com/codeigniter/user-guide/libraries/sessions.html + * + * @param bool $include_non_flash TRUE if it should stow basic errors as flashdata (default = FALSE) + */ public function keepErrors($includeNonFlash = null) { if ($includeNonFlash) - $this->flash_errors = array_merge($this->flash_errors, $this->errors); + $this->flashErrors = array_merge($this->flashErrors, $this->errors); - $this->flash_errors = array_merge($this->flash_errors, (array)$this->session->getFlashdata('errors')); - $this->session->set('errors', $this->flash_errors); + $this->flashErrors = array_merge($this->flashErrors, (array)$this->session->getFlashdata('errors')); + $this->session->set('errors', $this->flashErrors); $this->session->setFlashdata('errors'); } + /** + * Get Errors Array + * + * Return array of errors + * + * @return array Array of messages, empty array if no errors + */ public function getErrorsArray() { return $this->errors; } + /** + * Print Errors + * + * Prints string of errors separated by delimiter + * + * @param string $divider Separator for errors + */ public function printErrors($divider = '
', $return = null) { $msg = ''; @@ -322,39 +496,79 @@ class Aauth { echo $msg; } + /** + * Clear Errors + * + * Removes errors from error list and clears all associated flashdata + */ public function clearErrors() { - $this->errors = array(); + $this->errors = []; $this->session->remove('errors'); } + /** + * Info + * + * Add message to info array and set flash data + * + * @param string $message Message to add to infos array + * @param boolean $flashdata if TRUE add $message to CI flashdata (deflault: FALSE) + */ public function info($message = '', $flashdata = null) { $this->infos[] = $message; if ($flashdata) { - $this->flash_infos[] = $message; - $this->session->set('infos', $this->flash_infos); + $this->flashInfos[] = $message; + $this->session->set('infos', $this->flashInfos); $this->session->setFlashdata('infos'); } } + /** + * Keep Infos + * + * Keeps the flashdata infos for one more page refresh. Optionally adds the default infos into the + * flashdata list. This should be called last in your controller, and with care as it could continue + * to revive all infos and not let them expire as intended. + * Benefitial by using Ajax Requests + * + * @see http://ellislab.com/codeigniter/user-guide/libraries/sessions.html + * + * @param boolean $include_non_flash TRUE if it should stow basic infos as flashdata (default = FALSE) + */ public function keepInfos($includeNonFlash = null) { if ($includeNonFlash) - $this->flash_infos = array_merge($this->flash_infos, $this->infos); + $this->flashInfos = array_merge($this->flashInfos, $this->infos); - $this->flash_infos = array_merge($this->flash_infos, (array)$this->session->getFlashdata('infos')); - $this->session->set('infos', $this->flash_infos); + $this->flashInfos = array_merge($this->flashInfos, (array)$this->session->getFlashdata('infos')); + $this->session->set('infos', $this->flashInfos); $this->session->setFlashdata('infos'); } + /** + * Get Info Array + * + * Return array of infos + * + * @return array Array of messages, empty array if no errors + */ public function getInfosArray() { return $this->infos; } + /** + * Print Info + * + * Print string of info separated by delimiter + * + * @param string $divider Separator for info + * + */ public function printInfos($divider = '
', $return = null) { $msg = ''; @@ -375,9 +589,14 @@ class Aauth { echo $msg; } + /** + * Clear Info List + * + * Removes info messages from info list and clears all associated flashdata + */ public function clearInfos() { - $this->infos = array(); + $this->infos = []; $this->session->remove('infos'); } }