recreate project

This commit is contained in:
root
2026-02-10 22:11:06 -05:00
commit 663c0cdbda
10149 changed files with 1379710 additions and 0 deletions
+874
View File
@@ -0,0 +1,874 @@
<?php
namespace App\Controllers\View;
use CodeIgniter\Events\Events;
use App\Controllers\BaseController;
use App\Models\UserModel; // Assuming you have a UserModel
use App\Models\PasswordResetModel; // Model for the password_resets table
use CodeIgniter\I18n\Time;
use App\Models\RoleModel;
use App\Models\UserRoleModel;
use App\Models\PasswordResetRequestModel;
use App\Models\RolePermissionModel;
use App\Models\IpAttemptModel;
use CodeIgniter\Controller;
use App\Controllers\View\EmailController;
use App\Models\LoginActivityModel; // Make sure this import is present
require_once APPPATH . 'Helpers/pbkdf2_helper.php';
class UserController extends BaseController
{
protected $userModel;
protected $roleModel;
protected $userRoleModel;
protected $db;
protected $passwordResetModel;
protected $loginActivityModel;
protected $resetRequestModel;
public function __construct()
{
// Load the database service
$this->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. Its 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,
],
];
}
}