Files
2026-03-24 01:02:36 -04:00

1001 lines
36 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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
{
private const ACTIVATION_TTL_HOURS = 48;
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();
}
private function denyAccess(string $message)
{
if ($this->request->isAJAX() || $this->request->getHeaderLine('Accept') === 'application/json') {
return service('response')
->setStatusCode(403)
->setJSON(['status' => 'error', 'message' => $message]);
}
session()->setFlashdata('error', $message);
return redirect()->to('/access_denied');
}
private function requirePermission(string $permission)
{
if (!session()->get('is_logged_in')) {
return redirect()->to('/login');
}
$userId = (int) session()->get('user_id');
if ($userId <= 0 || !has_permission($userId, $permission)) {
return $this->denyAccess("You don't have permission to use this feature.");
}
return null;
}
private function hashToken(string $token): string
{
return hash('sha256', $token);
}
// 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()
{
if ($resp = $this->requirePermission('read_user')) {
return $resp;
}
helper('url');
return view('user/user_list', [
'userListEndpoint' => site_url('api/users'),
]);
}
// Method to show the list of users
public function index()
{
if ($resp = $this->requirePermission('read_user')) {
return $resp;
}
// 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()
{
if ($resp = $this->requirePermission('read_user')) {
return $resp;
}
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()
{
if ($resp = $this->requirePermission('edit_user')) {
return $resp;
}
// 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)
{
if ($resp = $this->requirePermission('edit_user')) {
return $resp;
}
$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)
{
if ($resp = $this->requirePermission('edit_user')) {
return $resp;
}
$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 or unverified email ---
if (!$user || (int) $user['is_verified'] === 0) {
session()->setFlashdata('success', 'If this email is registered, you will receive a reset link.');
log_message('info', "Password reset requested for {$email} (user missing or unverified).");
return redirect()->back();
}
// --- Verified user: continue with reset ---
$token = bin2hex(random_bytes(48));
$tokenHash = $this->hashToken($token);
$expires_at = Time::now()->addHours(1);
$this->passwordResetModel->insert([
'email' => $email,
'token' => $tokenHash,
'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
$tokenHash = $this->hashToken($token);
$resetEntry = $this->passwordResetModel
->groupStart()
->where('token', $tokenHash)
->orWhere('token', $token)
->groupEnd()
->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()
{
if (strtolower($this->request->getMethod()) !== 'post') {
return redirect()->to('/')->with('error', 'Invalid request.');
}
$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
$tokenHash = $this->hashToken($token);
$resetEntry = $this->passwordResetModel
->groupStart()
->where('token', $tokenHash)
->orWhere('token', $token)
->groupEnd()
->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
->groupStart()
->where('token', $tokenHash)
->orWhere('token', $token)
->groupEnd()
->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.');
$tokenHash = $this->hashToken($token);
$user = $this->userModel
->groupStart()
->where('token', $tokenHash)
->orWhere('token', $token)
->groupEnd()
->where('created_at >=', Time::now()->subHours(self::ACTIVATION_TTL_HOURS)->toDateTimeString())
->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;
$tokenHash = $this->hashToken($token);
$user = $this->userModel
->groupStart()
->where('token', $tokenHash)
->orWhere('token', $token)
->groupEnd()
->where('created_at >=', Time::now()->subHours(self::ACTIVATION_TTL_HOURS)->toDateTimeString())
->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()
{
if (strtolower($this->request->getMethod()) !== 'post') {
return redirect()->to('/')->with('error', 'Invalid request.');
}
$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');
$tokenHash = $this->hashToken($token);
$user = $this->userModel
->where('id', $userId)
->groupStart()
->where('token', $tokenHash)
->orWhere('token', $token)
->groupEnd()
->where('created_at >=', Time::now()->subHours(self::ACTIVATION_TTL_HOURS)->toDateTimeString())
->first();
log_message('debug', "Attempting to set password for user $userId");
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);
$userId = (int) session()->get('user_id');
log_message('info', 'User ID: ' . $userId);
if ($userId <= 0) {
return $this->denyAccess("You don't have permission to use this feature.");
}
$roleRow = $this->db->table('user_roles ur')
->join('roles r', 'r.id = ur.role_id', 'inner')
->select('r.name, r.slug, r.dashboard_route')
->where('ur.user_id', $userId)
->where('r.is_active', 1)
->groupStart()
->where('LOWER(r.name)', strtolower($roleKey))
->orWhere('LOWER(r.slug)', strtolower($roleKey))
->groupEnd()
->get()
->getRowArray();
if (empty($roleRow)) {
log_message('error', 'Invalid or unassigned role selected: ' . $roleKey);
return redirect()->back()->with('error', 'Invalid role selected.');
}
$route = $roleRow['dashboard_route'] ?? null;
if ($route === null) {
log_message('error', 'No dashboard route configured for role: ' . $roleKey);
return redirect()->back()->with('error', 'Invalid role selected.');
}
// Persist the *exact* role name or slug—choose your convention.
// Store the canonical name to avoid arbitrary role strings.
$this->userModel->update($userId, ['role' => $roleRow['name']]);
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)
{
if ($resp = $this->requirePermission('edit_user')) {
return $resp;
}
// 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()
{
if ($resp = $this->requirePermission('view_login_activity')) {
return $resp;
}
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()
{
if ($resp = $this->requirePermission('view_login_activity')) {
return $resp;
}
$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 ($resp = $this->requirePermission('edit_user')) {
return $resp;
}
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'),
];
// 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,
],
];
}
}