875 lines
33 KiB
PHP
875 lines
33 KiB
PHP
<?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. 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,
|
||
],
|
||
];
|
||
}
|
||
}
|