db = \Config\Database::connect(); // Check if the database connection is established if (!$this->db->connect()) { log_message('error', 'Database connection failed.'); throw new \Exception('Database connection failed.'); } else { log_message('info', 'Database connection successful.'); } helper('auth'); $this->userModel = new UserModel(); $this->roleModel = new RoleModel(); $this->userRoleModel = new UserRoleModel(); $this->passwordResetModel = new PasswordResetModel(); $this->loginActivityModel = new LoginActivityModel(); $this->resetRequestModel = new PasswordResetRequestModel(); } // Method to show the home page public function home() { return view('/index'); } // Method to show the about page public function about() { return view('/about'); } // Method to show the classes page public function classes() { return view('/classes'); } // Method to show the contact page public function contact() { return view('/contact'); } public function userList() { helper('url'); return view('user/user_list', [ 'userListEndpoint' => site_url('api/users'), ]); } // Method to show the list of users public function index() { // Fetch users along with their assigned roles $builder = $this->db->table('users'); $builder->select('users.id, users.firstname, users.lastname, users.email, user_roles.role_id, roles.name as role, users.status, users.updated_at'); $builder->join('user_roles', 'users.id = user_roles.user_id', 'left'); $builder->join('roles', 'user_roles.role_id = roles.id', 'left'); $sort = $this->request->getGet('sort'); $order = $this->request->getGet('order') === 'desc' ? 'desc' : 'asc'; // Define allowed fields for sorting $allowedFields = ['id', 'firstname', 'lastname', 'email', 'role']; if (in_array($sort, $allowedFields)) { $sort_column = $sort === 'role' ? 'roles.name' : 'users.' . $sort; $builder->orderBy($sort_column, $order); } else { $builder->orderBy('users.id', 'asc'); } $query = $builder->get(); $users = $query->getResultArray(); // Fetch available roles $roleBuilder = $this->db->table('roles'); $roleQuery = $roleBuilder->get(); $roles = $roleQuery->getResultArray(); return view('user/user_list', [ 'users' => $users, 'roles' => $roles, 'sort' => $sort, 'order' => $order, ]); } public function userListData() { return $this->response->setJSON([ 'users' => $this->buildUsersWithRoles(), ]); } private function buildUsersWithRoles(): array { // Fetch all users $users = $this->userModel->findAll(); if (!is_array($users)) { return []; } // Aggregate guardian types (primary/secondary) by user_id $gmap = []; try { $rows = $this->db->table('family_guardians') ->select('user_id, MAX(is_primary) AS max_primary, COUNT(*) AS cnt', false) ->groupBy('user_id') ->get()->getResultArray(); foreach ($rows as $r) { $uid = (int)($r['user_id'] ?? 0); if ($uid <= 0) continue; $maxp = (int)($r['max_primary'] ?? 0); $cnt = (int)($r['cnt'] ?? 0); if ($cnt > 0) { $gmap[$uid] = $maxp > 0 ? 'Primary' : 'Secondary'; } } } catch (\Throwable $e) { // ignore if table missing; leave map empty } // Aggregate "second parent presence" from legacy parents table $secondByUserId = []; $secondByEmail = []; try { $parentFields = $this->db->getFieldNames('parents'); $hasSecondUid = in_array('secondparent_user_id', $parentFields, true); $hasSecondEmail = in_array('secondparent_email', $parentFields, true); if ($hasSecondUid) { // Map: user_id present in parents.secondparent_user_id $uidRows = $this->db->table('parents') ->select('secondparent_user_id AS uid, COUNT(*) AS cnt', false) ->where('secondparent_user_id !=', 0) ->groupBy('secondparent_user_id') ->get()->getResultArray(); foreach ($uidRows as $r) { $u = (int)($r['uid'] ?? 0); if ($u > 0) { $secondByUserId[$u] = true; } } } if ($hasSecondEmail) { // Map: email present in parents.secondparent_email $emailRows = $this->db->table('parents') ->select('LOWER(TRIM(secondparent_email)) AS em, COUNT(*) AS cnt', false) ->where('secondparent_email !=', '') ->groupBy('secondparent_email') ->get()->getResultArray(); foreach ($emailRows as $r) { $em = trim(strtolower((string)($r['em'] ?? ''))); if ($em !== '') { $secondByEmail[$em] = true; } } } } catch (\Throwable $e) { // ignore if legacy table or fields are missing } // Prepare an array to store user data along with their roles $usersWithRoles = []; // Loop through each user and get their roles foreach ($users as $user) { if (!is_array($user)) { continue; } // Get the user's roles from the user_roles table $userRoles = $this->userRoleModel->where('user_id', $user['id'])->findAll() ?? []; // Initialize arrays to hold role names/ids for this user $roleNames = []; $roleIds = []; // Fetch role names from the roles table foreach ($userRoles as $userRole) { if (!is_array($userRole) || empty($userRole['role_id'])) { continue; } $role = $this->roleModel->find($userRole['role_id']); if ($role) { $roleIds[] = (int) $role['id']; $roleNames[] = $role['name']; } } // Parent type if user is a guardian in any family $parentType = $gmap[(int)($user['id'] ?? 0)] ?? ''; // Second-from-parents flag: by secondparent_user_id or secondparent_email $isSecond = false; $uid = (int)($user['id'] ?? 0); if ($uid > 0 && !empty($secondByUserId[$uid])) { $isSecond = true; } else { $em = trim(strtolower((string)($user['email'] ?? ''))); if ($em !== '' && !empty($secondByEmail[$em])) $isSecond = true; } // Add the user data along with role names to the result array $usersWithRoles[] = [ 'user' => $user, 'roles' => $roleNames, 'role_ids' => $roleIds, 'parent_type' => $parentType, // 'Primary', 'Secondary', or '' 'second_from_parents' => $isSecond ? 'Yes' : '', ]; } return $usersWithRoles; } // Method to store a new user public function store() { // Validate input data $validation = \Config\Services::validation(); $validation->setRules([ 'username' => 'required', 'password' => 'required|min_length[6]', 'lastname' => 'required', 'firstname' => 'required', 'cellphone' => 'required', 'email' => 'required|valid_email', 'address_street' => 'required', 'city' => 'required', 'state' => 'required', 'zip' => 'required', 'accept_school_policy' => 'required', 'role_id' => 'required|integer' ]); if (!$this->validate($validation->getRules())) { $errors = $validation->getErrors(); return redirect()->back()->withInput()->with('errors', $errors); } // Prepare user data for insertion $userData = [ 'username' => $this->request->getPost('username'), 'password' => pbkdf2_hash($this->request->getPost('password')), 'lastname' => ucfirst($this->request->getPost('lastname')), 'firstname' => ucfirst($this->request->getPost('firstname')), 'cellphone' => $this->request->getPost('cellphone'), 'email' => strtolower($this->request->getPost('email')), 'address_street' => $this->request->getPost('address_street'), 'city' => ucfirst($this->request->getPost('city')), 'state' => $this->request->getPost('state'), 'zip' => $this->request->getPost('zip'), 'accept_school_policy' => $this->request->getPost('accept_school_policy'), ]; // Insert user data into the database if ($this->userModel->save($userData) === false) { return redirect()->back()->withInput()->with('errors', $this->userModel->errors()); } // Retrieve the newly created user ID $userId = $this->userModel->getInsertID(); // Assign the role to the user in the user_roles table $roleData = [ 'user_id' => $userId, 'role_id' => $this->request->getPost('role_id'), 'created_at' => utc_now(), ]; if ($this->userRoleModel->save($roleData) === false) { return redirect()->back()->withInput()->with('errors', $this->userRoleModel->errors()); } return redirect()->to('/user'); } // Method to show the form for editing an existing user public function edit($id) { $data['user'] = $this->userModel->find($id); $data['roles'] = $this->roleModel->findAll(); $userRoles = $this->userRoleModel->where('user_id', $id)->findAll(); $data['assignedRoles'] = array_column($userRoles, 'role_id'); return view('user/edit', $data); } // Method to delete an existing user public function delete($id) { $this->userModel->delete($id); // Delete the user's roles from the user_roles table $this->userRoleModel->where('user_id', $id)->delete(); return redirect()->to('/user'); } //This function is necessary to display the initial forgot password form where the user enters their email address. public function forgotPassword() { return view('/user/forgot_password'); } //This function is crucial for processing the forgot password request, generating a reset token, storing it in the database, and sending an email with the reset link. public function processForgotPassword() { log_message('debug', 'Entered processForgotPassword'); $ip = $this->request->getIPAddress(); $recentAttempts = $this->resetRequestModel ->where('ip_address', $ip) ->where('requested_at >=', Time::now()->subHours(24)->toDateTimeString()) ->countAllResults(); if ($recentAttempts >= 3) { log_message('warning', "IP {$ip} blocked due to too many reset attempts."); session()->setFlashdata('show_block_modal', true); return redirect()->back(); } // Log this attempt $this->resetRequestModel->insert([ 'ip_address' => $ip, 'requested_at' => Time::now(), ]); $email = strtolower($this->request->getPost('email')); $user = $this->userModel->where('email', $email)->first(); // --- Handle unknown email --- if (!$user) { session()->setFlashdata('error', 'If this email is registered, you will receive a reset link.'); log_message('info', "Password reset requested for non-existing user {$email}"); return redirect()->back(); } // --- Handle unverified accounts --- if ((int) $user['is_verified'] === 0) { session()->setFlashdata('error', 'Please check your email and complete the account activation process before resetting your password.'); log_message('info', "Password reset blocked for unverified user {$email}"); return redirect()->back(); } // --- Verified user: continue with reset --- $token = bin2hex(random_bytes(48)); $expires_at = Time::now()->addHours(1); $this->passwordResetModel->insert([ 'email' => $email, 'token' => $token, 'created_at' => Time::now(), 'expires_at' => $expires_at, ]); $this->sendResetEmail($email, $token); session()->setFlashdata('success', 'A password reset link has been sent to your email.'); log_message('info', "Password reset email sent to {$email}"); return redirect()->back(); } public function blocked() { session()->setFlashdata('show_block_modal', true); return redirect()->back(); // Show modal on the previous page } //This function is needed to display a confirmation page after the reset email has been sent, informing the user to check their email. public function passwordResetConfirmation() { $email = session()->getFlashdata('reset_email'); return view('user/password_reset_confirmation', ['email' => $email]); } //This is a private helper function that handles sending the reset email. It’s separated out to keep the processForgotPassword function clean and focused. public function sendResetEmail($email, $token) { $resetLink = site_url('/reset_password?token=' . $token); // Render email template with data $message = view('emails/reset_password', ['resetLink' => $resetLink]); $emailController = new EmailController(); $subject = 'Password Reset Request'; if ($emailController->sendEmail($email, $subject, $message)) { log_message('info', 'Password reset email successfully sent to ' . $email); } else { log_message('error', 'Failed to send password reset email to ' . $email); } } //This function is necessary to display the form where the user can enter a new password, after clicking on the reset link in their email. public function resetPassword() { // Retrieve the token from the query string $token = $this->request->getGet('token'); if (!$token) { return redirect()->to('/')->with('error', 'Invalid password reset link.'); } // You may want to validate the token here $resetEntry = $this->passwordResetModel->where('token', $token) ->where('expires_at >=', Time::now()) ->first(); if (!$resetEntry) { return redirect()->to('/')->with('error', 'Invalid or expired reset link.'); } // Load the reset password view with the token return view('user/reset_password', ['token' => $token]); } //This function processes the new password submission, validating the token, updating the user's password, and cleaning up the reset entry. public function processResetPassword() { $token = $this->request->getPost('token'); $newPassword = $this->request->getPost('password'); $passConfirm = $this->request->getPost('pass_confirm'); // Validate the input if (!$this->validate([ 'password' => [ 'label' => 'Password', 'rules' => 'required|min_length[8]|regex_match[/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@\-=\+*#$%&!?])[A-Za-z\d@\-=\+*#$%&!?]{8,}$/]', 'errors' => [ 'required' => 'Password is required.', 'min_length' => 'Password must be at least 8 characters.', 'regex_match' => 'Password must contain at least one lowercase letter, one uppercase letter, one number, and one special character (@, -, =, +, *, #, $, %, &, !, ?).' ] ], 'pass_confirm' => [ 'label' => 'Confirm Password', 'rules' => 'required|matches[password]', 'errors' => [ 'required' => 'Password confirmation is required.', 'matches' => 'Passwords do not match.' ] ] ])) { return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); } // Find the password reset entry $resetEntry = $this->passwordResetModel->where('token', $token) ->where('expires_at >=', Time::now()) ->first(); if (!$resetEntry) { return redirect()->to('/')->with('error', 'Invalid or expired reset link.'); } // Find the user by email $user = $this->userModel->where('email', $resetEntry['email'])->first(); if (!$user) { return redirect()->to('/')->with('error', 'User not found.'); } // 🛠 Hash the password using PBKDF2 $hashedPassword = pbkdf2_hash($newPassword); // Update the password in the users table and set the account status to 'Active' // Reset failed login attempts, suspension status, and last failed login time $this->userModel->update($user['id'], [ 'failed_attempts' => 0, // Reset failed attempts 'is_suspended' => 0, // Unsuspend the account 'last_failed_at' => null, // Reset the last failed login timestamp 'password' => $hashedPassword, 'status' => 'Active' ]); // Delete the used token from the password reset table $this->passwordResetModel->where('token', $token)->delete(); // Retrieve the user's IP address from the request $ipAddress = $this->request->getIPAddress(); // Delete the corresponding entry from the ip_attempts table, only if it exists $ipAttemptModel = new IpAttemptModel(); $existingAttempt = $ipAttemptModel->where('ip_address', $ipAddress)->first(); if ($existingAttempt) { // IP address entry exists, so delete it $ipAttemptModel->where('ip_address', $ipAddress)->delete(); } // Redirect to a success page with a success message return view('user/password_set_success'); } public function passwordSetSuccess() { return view('user/password_set_success'); } public function confirm($token) { log_message('info', 'Processing email confirmation with token: ' . $token); $user = $this->userModel->where('token', $token)->first(); if (!$user || $user['is_verified'] == 1) { return redirect()->to('/invalid_token'); } // Mark the user as verified and generate an account ID $account_id = 'ACC' . str_pad($user['id'], 8, '0', STR_PAD_LEFT); // Example: ACC00000001 $this->userModel->update($user['id'], ['is_verified' => 1, 'token' => null, 'account_id' => $account_id]); log_message('info', 'User verified and account ID generated: ' . $account_id); // Redirect to the set password page return redirect()->to('/set_password/' . $user['id']); } public function setPassword($token) { //echo "Reached setPassword with token: " . esc($token); //echo "Token received: " . $token; $user = $this->userModel->where('token', $token)->first(); if (!$user || $user['is_verified'] == 1) { return redirect()->to('/invalid_token'); } return view('user/set_password', [ 'userId' => $user['id'], 'token' => $token ]); } public function savePassword() { $validation = \Config\Services::validation(); $validation->setRules([ 'password' => [ 'label' => 'Password', 'rules' => 'required|min_length[8]|regex_match[/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@\-=\+*#$%&!?])[A-Za-z\d@\-=\+*#$%&!?]{8,}$/]', 'errors' => [ 'required' => 'Password is required.', 'min_length' => 'Password must be at least 8 characters long.', 'regex_match' => 'Password must contain at least one lowercase letter, one uppercase letter, one number, and one special character (@, -, =, +, *, #, $, %, &, !, ?).' ] ], 'password_confirm' => [ 'label' => 'Confirm Password', 'rules' => 'required|matches[password]', 'errors' => [ 'required' => 'Password confirmation is required.', 'matches' => 'Passwords do not match.' ] ], 'user_id' => 'required|integer', 'token' => 'required' ]); if (!$this->validate($validation->getRules())) { return redirect()->back()->withInput()->with('errors', $validation->getErrors()); } $userId = $this->request->getPost('user_id'); $token = $this->request->getPost('token'); $password = $this->request->getPost('password'); $user = $this->userModel->where('id', $userId)->where('token', $token)->first(); log_message('debug', "Attempting to set password for user $userId with token $token"); if (!$user || $user['is_verified'] == 1) { return redirect()->to('/invalid_token'); } $hashedPassword = pbkdf2_hash($password); // Or use password_hash() // Generate account_id only if it's not already set $account_id = $user['account_id'] ?? null; if (!$account_id) { $account_id = 'ACC' . str_pad($userId, 8, '0', STR_PAD_LEFT); } $this->userModel->update($userId, [ 'password' => $hashedPassword, 'status' => 'Active', 'is_verified' => 1, 'token' => null, 'account_id' => $account_id, ]); // Optional: trigger event $userData = $this->userModel->getUserInfoById($userId); if (!is_array($userData)) { $userData = (array) $userData; } $userData['id'] = $userId; \CodeIgniter\Events\Events::trigger('user_registered', $userData); return redirect()->to('/user/password_set_success'); } public function invalidToken() { return view('errors/invalid_token'); } public function selectRole() { log_message('info', 'Processing setRole form submission.'); if (!session()->get('is_logged_in')) { log_message('error', 'User is not logged in.'); return redirect()->to('/user/login'); } $roleKey = (string) $this->request->getPost('role'); log_message('info', 'Role selected: ' . $roleKey); $roleModel = new RoleModel(); $route = $roleModel->getRouteByNameOrSlug($roleKey); if ($route === null) { log_message('error', 'Invalid or inactive role selected: ' . $roleKey); return redirect()->back()->with('error', 'Invalid role selected.'); } $userId = (int) session()->get('user_id'); log_message('info', 'User ID: ' . $userId); // Persist the *exact* role name or slug—choose your convention. // If you want to store the canonical name, fetch the row and use $row['name']. $this->userModel->update($userId, ['role' => $roleKey]); log_message('info', 'Role updated in database.'); log_message('info', 'Redirecting to role dashboard: ' . $route); return redirect()->to($route); } public function welcomeBack() { if (!session()->get('is_logged_in')) { return redirect()->to('/user/login'); } return view('user/welcome_back'); } public function delete_role($roleId) { // Fetch the role to be deleted $role = $this->roleModel->find($roleId); if (!$role) { return redirect()->back()->with('error', 'Role not found.'); } // Fetch the "Guest" role $guestRole = $this->roleModel->where('name', 'Guest')->first(); if (!$guestRole) { return redirect()->back()->with('error', 'Guest role not found.'); } // Update users with the deleted role to have the "Guest" role $this->userModel->where('role_id', $roleId)->set(['role_id' => $guestRole['id'], 'role' => 'Guest', 'status' => 'Inactive'])->update(); // Verify that users have been updated $updatedUsers = $this->userModel->where('role_id', $guestRole['id'])->findAll(); log_message('debug', 'Updated users: ' . print_r($updatedUsers, true)); // Delete the role if ($this->roleModel->delete($roleId)) { return redirect()->to('/administrator/user_roles')->with('success', 'Role deleted successfully. Users with this role have been assigned the Guest role.'); } else { return redirect()->back()->with('error', 'Failed to delete role.'); } } public function loginActivity() { helper('url'); $perPage = (int) ($this->request->getGet('per_page') ?? 25); return view('user/login_activity', [ 'loginActivityEndpoint' => site_url('api/login-activity'), 'defaultPerPage' => $perPage > 0 ? $perPage : 25, ]); } public function loginActivityData() { $perPage = (int) ($this->request->getGet('per_page') ?? 25); $page = (int) ($this->request->getGet('page') ?? 1); return $this->response->setJSON($this->buildLoginActivityPayload($perPage, $page)); } // Method to update an existing user public function updateUser() { if (strtolower($this->request->getMethod()) !== 'post') { return redirect()->to(site_url('user/user_list'))->with('error', 'Invalid request.'); } $id = (int) $this->request->getPost('id'); if (!$id) { return redirect()->to(site_url('user/user_list'))->with('error', 'User ID is missing.'); } $user = $this->userModel->find($id); if (!$user) { return redirect()->to(site_url('user/user_list'))->with('error', 'User not found.'); } // Helpers $toBool = fn($k) => $this->request->getPost($k) ? 1 : 0; $toDT = function ($key) { $v = trim((string)$this->request->getPost($key)); if ($v === '') return null; // Expecting HTML datetime-local "YYYY-MM-DDTHH:MM" $ts = strtotime($v); return $ts ? date('Y-m-d H:i:s', $ts) : null; }; $data = [ 'id' => $id, 'account_id' => trim((string)$this->request->getPost('account_id')), 'lastname' => trim((string)$this->request->getPost('lastname')), 'firstname' => trim((string)$this->request->getPost('firstname')), 'gender' => trim((string)$this->request->getPost('gender')), 'cellphone' => trim((string)$this->request->getPost('cellphone')), 'email' => trim((string)$this->request->getPost('email')), 'address_street' => trim((string)$this->request->getPost('address_street')), 'apt' => trim((string)$this->request->getPost('apt')), 'city' => trim((string)$this->request->getPost('city')), 'state' => trim((string)$this->request->getPost('state')), 'zip' => trim((string)$this->request->getPost('zip')), 'accept_school_policy' => $toBool('accept_school_policy'), 'user_type' => trim((string)$this->request->getPost('user_type')), 'rfid_tag' => trim((string)$this->request->getPost('rfid_tag')), 'school_id' => trim((string)$this->request->getPost('school_id')), 'failed_attempts' => (string)$this->request->getPost('failed_attempts') === '' ? null : (int)$this->request->getPost('failed_attempts'), 'last_failed_at' => $toDT('last_failed_at'), 'semester' => trim((string)$this->request->getPost('semester')), 'school_year' => trim((string)$this->request->getPost('school_year')), 'status' => trim((string)$this->request->getPost('status')), 'is_suspended' => $toBool('is_suspended'), 'is_verified' => $toBool('is_verified'), 'token' => trim((string)$this->request->getPost('token')), 'updated_at' => $toDT('updated_at'), 'created_at' => $toDT('created_at'), ]; // Validation $rules = [ 'firstname' => 'required|min_length[2]|max_length[100]', 'lastname' => 'required|min_length[2]|max_length[100]', 'email' => "required|valid_email|is_unique[users.email,id,{$id}]", 'state' => 'permit_empty|max_length[2]', 'zip' => 'permit_empty|max_length[20]', 'cellphone' => 'permit_empty|max_length[50]', 'failed_attempts' => 'permit_empty|integer', 'gender' => 'permit_empty|in_list[male,female,MALE,FEMALE]', // adjust as needed ]; if (!$this->validate($rules)) { return redirect()->back() ->with('error', implode(' ', $this->validator->getErrors())) ->withInput(); } if (!$this->userModel->save($data)) { return redirect()->back()->with('error', 'Failed to update user.')->withInput(); } return redirect()->to(site_url('user/user_list'))->with('success', 'User updated successfully.'); } private function buildLoginActivityPayload(int $perPage, int $page): array { $perPage = max(1, min($perPage, 200)); $page = max(1, $page); $totalActivities = (int) $this->loginActivityModel->countAll(); $activities = $this->loginActivityModel ->orderBy('login_time', 'DESC') ->paginate($perPage, 'loginActivity', $page) ?? []; $pager = $this->loginActivityModel->pager; $currentPage = $pager ? (int) $pager->getCurrentPage('loginActivity') : $page; $pageCount = $pager ? (int) $pager->getPageCount('loginActivity') : (int) max(1, ceil($totalActivities / $perPage)); $normalized = array_map(static function ($activity) { if (!is_array($activity)) { return []; } return [ 'user_id' => $activity['user_id'] ?? null, 'email' => $activity['email'] ?? null, 'login_time' => $activity['login_time'] ?? null, 'logout_time' => $activity['logout_time'] ?? null, 'ip_address' => $activity['ip_address'] ?? null, 'user_agent' => $activity['user_agent'] ?? null, ]; }, $activities); return [ 'activities' => $normalized, 'pagination' => [ 'total' => $totalActivities, 'perPage' => $perPage, 'currentPage' => $currentPage, 'pageCount' => $pageCount, 'hasNext' => $currentPage < $pageCount, 'hasPrevious' => $currentPage > 1, ], ]; } }