616 lines
21 KiB
PHP
616 lines
21 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers;
|
|
|
|
use App\Models\LoginActivityModel;
|
|
use App\Models\UserModel;
|
|
use App\Models\UserRoleModel;
|
|
use CodeIgniter\Controller;
|
|
use CodeIgniter\Events\Events;
|
|
use App\Models\IpAttemptModel;
|
|
use App\Models\PasswordResetModel;
|
|
use CodeIgniter\I18n\Time;
|
|
use App\Models\RoleModel;
|
|
use App\Models\ConfigurationModel;
|
|
use App\Models\PreferencesModel;
|
|
|
|
|
|
require_once APPPATH . 'Helpers/pbkdf2_helper.php';
|
|
require_once APPPATH . 'Helpers/jwt_helper.php';
|
|
|
|
|
|
class AuthController extends Controller
|
|
{
|
|
protected $configModel;
|
|
protected $userModel;
|
|
protected $roleModel;
|
|
protected $schoolYear;
|
|
protected $semester;
|
|
protected $ipAttemptModel;
|
|
protected $loginActivityModel;
|
|
protected $preferencesModel;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->userModel = new UserModel();
|
|
$this->roleModel = new RoleModel();
|
|
$this->configModel = new ConfigurationModel();
|
|
$this->ipAttemptModel = new IpAttemptModel();
|
|
$this->loginActivityModel = new LoginActivityModel();
|
|
$this->preferencesModel = new PreferencesModel();
|
|
$this->schoolYear = $this->configModel->getConfig('school_year');
|
|
$this->semester = $this->configModel->getConfig('semester');
|
|
|
|
}
|
|
|
|
public function setModel($model)
|
|
{
|
|
$this->userModel = $model;
|
|
}
|
|
|
|
private function scheduleEvent($userId)
|
|
{
|
|
// Schedule the event to run after 2 minutes (120 seconds)
|
|
Events::trigger('delete_unverified_user', $userId);
|
|
}
|
|
|
|
public function loginMask()
|
|
{
|
|
// Serve the login view directly here
|
|
return view('user/login'); // Adjust the view path as needed
|
|
}
|
|
|
|
public function login()
|
|
{
|
|
log_message('info', 'Processing login form submission.');
|
|
|
|
// Step 1: Get email, password, and IP from the request
|
|
$email = $this->request->getPost('email');
|
|
$password = $this->request->getPost('password');
|
|
$ip = $this->request->getIPAddress();
|
|
|
|
log_message('info', 'Login attempt from IP: ' . $ip . ' for email: ' . $email);
|
|
|
|
// Step 2: Check if the IP is blocked (too many failed attempts)
|
|
if ($this->isIpBlocked($ip)) {
|
|
return redirect()
|
|
->back()
|
|
->with('error', 'Too many failed attempts from your IP. Please try again later.')
|
|
->withInput(); // ✅ preserve email input
|
|
}
|
|
|
|
// Step 3: Look up user by email
|
|
$user = $this->getUserByEmail($email);
|
|
|
|
if ($user) {
|
|
// Step 4: Check if user is suspended
|
|
if ($this->checkIfUserSuspended($user)) {
|
|
return redirect()
|
|
->back()
|
|
->with('error', 'Account suspended. Please check your email to reset your password.')
|
|
->withInput(); // ✅ preserve email input
|
|
}
|
|
|
|
// Step 5: Verify password
|
|
if ($this->verifyPassword($password, $user['password'])) {
|
|
// Step 6: Login successful — reset failed attempts and log
|
|
$this->resetFailedAttempts($user['id']);
|
|
$this->logLoginAttempt($user['id'], $user['email'], $ip, $this->request->getUserAgent());
|
|
|
|
// ✅ Step 7: Call loginUser() to set session and redirect
|
|
return $this->loginUser($user);
|
|
} else {
|
|
// Step 8: Password mismatch — log failed attempt
|
|
$this->handleFailedLogin($user['id'], $user['email'], $ip);
|
|
return redirect()
|
|
->back()
|
|
->with('error', 'The email and password combination you entered is invalid. Please try again.')
|
|
->withInput(); // ✅ preserve email input
|
|
}
|
|
} else {
|
|
// Step 9: No user found — log IP-level attempt
|
|
$this->logIpAttempt($ip);
|
|
return redirect()
|
|
->back()
|
|
->with('error', 'The email and password combination you entered is invalid. Please try again.')
|
|
->withInput(); // ✅ preserve email input
|
|
}
|
|
}
|
|
|
|
// JSON API: POST /api/login
|
|
public function apiLogin()
|
|
{
|
|
$requestData = $this->request->getJSON(true);
|
|
if (!$requestData) {
|
|
// fallback to form vars
|
|
$requestData = [
|
|
'email' => $this->request->getPost('email'),
|
|
'password' => $this->request->getPost('password'),
|
|
];
|
|
}
|
|
|
|
$email = $requestData['email'] ?? '';
|
|
$password = $requestData['password'] ?? '';
|
|
$ip = $this->request->getIPAddress();
|
|
|
|
if (!$email || !$password) {
|
|
return $this->response->setStatusCode(400)->setJSON([
|
|
'status' => false,
|
|
'message' => 'Email and password are required.'
|
|
]);
|
|
}
|
|
|
|
if ($this->isIpBlocked($ip)) {
|
|
return $this->response->setStatusCode(429)->setJSON([
|
|
'status' => false,
|
|
'message' => 'Too many failed attempts from your IP. Please try again later.'
|
|
]);
|
|
}
|
|
|
|
$user = $this->getUserByEmail($email);
|
|
if (!$user) {
|
|
$this->logIpAttempt($ip);
|
|
return $this->response->setStatusCode(401)->setJSON([
|
|
'status' => false,
|
|
'message' => 'Invalid email or password.'
|
|
]);
|
|
}
|
|
|
|
if ($this->checkIfUserSuspended($user)) {
|
|
return $this->response->setStatusCode(403)->setJSON([
|
|
'status' => false,
|
|
'message' => 'Account suspended. Please reset your password.'
|
|
]);
|
|
}
|
|
|
|
if (!$this->verifyPassword($password, $user['password'])) {
|
|
$this->handleFailedLogin($user['id'], $user['email'], $ip);
|
|
return $this->response->setStatusCode(401)->setJSON([
|
|
'status' => false,
|
|
'message' => 'Invalid email or password.'
|
|
]);
|
|
}
|
|
|
|
// Success: reset attempts + log
|
|
$this->resetFailedAttempts($user['id']);
|
|
$this->logLoginAttempt($user['id'], $user['email'], $ip, $this->request->getUserAgent());
|
|
|
|
// Fetch roles
|
|
$userRoleModel = new UserRoleModel();
|
|
$rolesRows = $userRoleModel->select('roles.name')
|
|
->join('roles', 'roles.id = user_roles.role_id')
|
|
->where('user_roles.user_id', $user['id'])
|
|
->get()
|
|
->getResultArray();
|
|
$roleNames = array_column($rolesRows, 'name');
|
|
|
|
// Build roles map (object with keys per example)
|
|
$rolesMap = [];
|
|
foreach ($roleNames as $r) {
|
|
$rolesMap[$r] = true;
|
|
}
|
|
|
|
// Build JWT token
|
|
$now = time();
|
|
$exp = $now + 60 * 60 * 24; // 24h default
|
|
$payload = [
|
|
'sub' => (int) $user['id'],
|
|
'name' => trim(($user['firstname'] ?? '') . ' ' . ($user['lastname'] ?? '')),
|
|
'roles' => $roleNames,
|
|
'iat' => $now,
|
|
'exp' => $exp,
|
|
];
|
|
|
|
$secret = env('JWT_SECRET', 'change-me-in-env');
|
|
$token = jwt_encode($payload, $secret, 'HS256');
|
|
|
|
return $this->response->setJSON([
|
|
'status' => true,
|
|
'token' => $token,
|
|
'user' => [
|
|
'id' => (int) $user['id'],
|
|
'name' => $payload['name'],
|
|
'roles' => (object) $rolesMap,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* API Registration endpoint
|
|
* POST /api/v1/register
|
|
*/
|
|
public function apiRegister()
|
|
{
|
|
$requestData = $this->request->getJSON(true);
|
|
if (!$requestData) {
|
|
// fallback to form vars
|
|
$requestData = $this->request->getPost();
|
|
}
|
|
|
|
// Basic validation
|
|
$rules = [
|
|
'firstname' => 'required|min_length[2]|max_length[30]',
|
|
'lastname' => 'required|min_length[2]|max_length[30]',
|
|
'email' => 'required|valid_email|is_unique[users.email]',
|
|
'password' => 'required|min_length[8]',
|
|
'cellphone' => 'required|min_length[10]|max_length[20]',
|
|
];
|
|
|
|
$validation = \Config\Services::validation();
|
|
$validation->setRules($rules);
|
|
|
|
if (!$validation->run($requestData)) {
|
|
return $this->response->setStatusCode(422)->setJSON([
|
|
'status' => false,
|
|
'message' => 'Validation failed',
|
|
'errors' => $validation->getErrors(),
|
|
]);
|
|
}
|
|
|
|
// Check if email already exists
|
|
$existingUser = $this->userModel->where('email', $requestData['email'])->first();
|
|
if ($existingUser) {
|
|
return $this->response->setStatusCode(422)->setJSON([
|
|
'status' => false,
|
|
'message' => 'Email already registered',
|
|
]);
|
|
}
|
|
|
|
// Hash password
|
|
require_once APPPATH . 'Helpers/pbkdf2_helper.php';
|
|
$hashedPassword = pbkdf2_hash($requestData['password']);
|
|
|
|
// Prepare user data
|
|
$userData = [
|
|
'firstname' => $requestData['firstname'],
|
|
'lastname' => $requestData['lastname'],
|
|
'email' => $requestData['email'],
|
|
'password' => $hashedPassword,
|
|
'cellphone' => $requestData['cellphone'],
|
|
'gender' => $requestData['gender'] ?? null,
|
|
'address_street' => $requestData['address_street'] ?? null,
|
|
'city' => $requestData['city'] ?? null,
|
|
'state' => $requestData['state'] ?? null,
|
|
'zip' => $requestData['zip'] ?? null,
|
|
'school_year' => $this->configModel->getConfig('school_year'),
|
|
'semester' => $this->configModel->getConfig('semester'),
|
|
'status' => 'active',
|
|
'is_verified' => 0, // Require email verification
|
|
];
|
|
|
|
try {
|
|
$userId = $this->userModel->insert($userData);
|
|
if (!$userId) {
|
|
return $this->response->setStatusCode(500)->setJSON([
|
|
'status' => false,
|
|
'message' => 'Failed to create user account',
|
|
'errors' => $this->userModel->errors(),
|
|
]);
|
|
}
|
|
|
|
// Assign parent role if specified
|
|
$roleId = null;
|
|
if (isset($requestData['role']) && $requestData['role'] === 'parent') {
|
|
$role = $this->roleModel->where('name', 'parent')->first();
|
|
if ($role) {
|
|
$roleId = $role['id'];
|
|
$userRoleModel = new \App\Models\UserRoleModel();
|
|
$userRoleModel->insert([
|
|
'user_id' => $userId,
|
|
'role_id' => $roleId,
|
|
]);
|
|
}
|
|
}
|
|
|
|
// Generate JWT token
|
|
$now = time();
|
|
$exp = $now + 60 * 60 * 24; // 24h
|
|
$payload = [
|
|
'sub' => (int) $userId,
|
|
'name' => trim($userData['firstname'] . ' ' . $userData['lastname']),
|
|
'roles' => $roleId ? ['parent'] : [],
|
|
'iat' => $now,
|
|
'exp' => $exp,
|
|
];
|
|
|
|
$secret = env('JWT_SECRET', 'change-me-in-env');
|
|
$token = jwt_encode($payload, $secret, 'HS256');
|
|
|
|
return $this->response->setStatusCode(201)->setJSON([
|
|
'status' => true,
|
|
'message' => 'Registration successful. Please verify your email.',
|
|
'token' => $token,
|
|
'user' => [
|
|
'id' => (int) $userId,
|
|
'name' => $payload['name'],
|
|
'email' => $userData['email'],
|
|
'roles' => $payload['roles'],
|
|
],
|
|
]);
|
|
} catch (\Exception $e) {
|
|
log_message('error', 'Registration error: ' . $e->getMessage());
|
|
return $this->response->setStatusCode(500)->setJSON([
|
|
'status' => false,
|
|
'message' => 'Registration failed. Please try again.',
|
|
]);
|
|
}
|
|
}
|
|
|
|
private function verifyPassword($password, $storedHash)
|
|
{
|
|
if (!function_exists('pbkdf2_verify')) {
|
|
die('pbkdf2_verify() is NOT loaded');
|
|
}
|
|
|
|
return pbkdf2_verify($password, $storedHash);
|
|
}
|
|
|
|
private function handleFailedLogin($userId, $email, $ip)
|
|
{
|
|
// Get user data
|
|
$user = $this->userModel->find($userId);
|
|
$failedAttempts = $user['failed_attempts'] + 1;
|
|
$data = ['failed_attempts' => $failedAttempts, 'last_failed_at' => utc_now()];
|
|
|
|
// Check if the failed attempts have reached 3
|
|
if ($failedAttempts >= 3) {
|
|
// Suspend the account
|
|
$data['is_suspended'] = true;
|
|
|
|
// Set a warning message in session to inform the user
|
|
session()->setFlashdata('warning_message', 'Your account has been suspended due to multiple failed login attempts. A reset password email has been sent to your registered email address.');
|
|
|
|
// Send the password reset email
|
|
$this->sendPasswordResetEmail($email);
|
|
}
|
|
|
|
// Update the user record
|
|
$this->userModel->update($userId, $data);
|
|
|
|
// Log the IP attempt (track failed login attempts)
|
|
$this->logIpAttempt($ip);
|
|
}
|
|
|
|
private function isIpBlocked($ip)
|
|
{
|
|
$attempt = $this->ipAttemptModel->where('ip_address', $ip)->first();
|
|
return $attempt && $attempt['blocked_until'] > utc_now();
|
|
}
|
|
|
|
private function logIpAttempt($ip)
|
|
{
|
|
$now = utc_now();
|
|
$attempt = $this->ipAttemptModel->where('ip_address', $ip)->first();
|
|
|
|
if ($attempt) {
|
|
$attempts = $attempt['attempts'] + 1;
|
|
$blockedUntil = $attempts >= 10 ? date('Y-m-d H:i:s', strtotime('+24 hours')) : null;
|
|
$this->ipAttemptModel->update($attempt['id'], [
|
|
'attempts' => $attempts,
|
|
'last_attempt_at' => $now,
|
|
'blocked_until' => $blockedUntil
|
|
]);
|
|
} else {
|
|
$this->ipAttemptModel->insert([
|
|
'ip_address' => $ip,
|
|
'attempts' => 1,
|
|
'last_attempt_at' => $now
|
|
]);
|
|
}
|
|
}
|
|
|
|
private function logLoginAttempt($userId, $email, $ipAddress, $userAgent)
|
|
{
|
|
$this->loginActivityModel->insert([
|
|
'user_id' => $userId,
|
|
'email' => $email,
|
|
'login_time' => utc_now(),
|
|
'ip_address' => $ipAddress,
|
|
'user_agent' => $userAgent
|
|
]);
|
|
}
|
|
|
|
private function resetFailedAttempts($userId)
|
|
{
|
|
$this->userModel->update($userId, ['failed_attempts' => 0, 'last_failed_at' => null]);
|
|
}
|
|
|
|
public function logout()
|
|
{
|
|
// Get user ID and email from session
|
|
$userId = session()->get('user_id');
|
|
$email = session()->get('user_email');
|
|
|
|
// Log logout
|
|
$this->logLogout($userId, $email);
|
|
|
|
log_message('info', 'Session destroyed.');
|
|
|
|
// Destroy the session
|
|
session()->destroy();
|
|
|
|
// Redirect to the main login page
|
|
return redirect()->to(base_url('/'));
|
|
}
|
|
|
|
private function logLogout($userId, $email)
|
|
{
|
|
$this->loginActivityModel->where('user_id', $userId)
|
|
->where('logout_time', null)
|
|
->set('logout_time', utc_now())
|
|
->set('email', $email)
|
|
->update();
|
|
}
|
|
|
|
private function getUserByEmail($email)
|
|
{
|
|
return $this->userModel->where('email', $email)->first();
|
|
}
|
|
|
|
private function checkIfUserSuspended($user)
|
|
{
|
|
return $user['is_suspended'];
|
|
}
|
|
|
|
// This function is used to send ResetPassword email to the userwith suspended account.
|
|
private function sendPasswordResetEmail($email)
|
|
{
|
|
// Load the UserController
|
|
$userController = new \App\Controllers\View\UserController();
|
|
// First, attempt to find the user by their email
|
|
$userModel = new UserModel();
|
|
$user = $userModel->where('email', $email)->first();
|
|
|
|
if (!$user) {
|
|
log_message('error', 'User with email ' . $email . ' not found for password reset.');
|
|
return false;
|
|
}
|
|
|
|
// Generate a secure token for the password reset
|
|
helper('text');
|
|
$token = bin2hex(random_bytes(48));
|
|
$tokenHash = hash('sha256', $token);
|
|
|
|
// Calculate the expiration time for the token (1 hour from now)
|
|
$expires_at = Time::now()->addHours(1);
|
|
|
|
// Store the token in the password_resets table
|
|
$passwordResetModel = new PasswordResetModel();
|
|
$passwordResetModel->insert([
|
|
'email' => $email,
|
|
'token' => $tokenHash,
|
|
'created_at' => Time::now(),
|
|
'expires_at' => $expires_at,
|
|
]);
|
|
|
|
// Now, send the reset email using the generated token
|
|
$userController->sendResetEmail($email, $token); // Calling the original helper function to send the email
|
|
|
|
// Log and return success
|
|
log_message('info', 'Password reset email sent to ' . $email);
|
|
return true;
|
|
}
|
|
|
|
private function loginUser($user)
|
|
{
|
|
$userRoleModel = new UserRoleModel();
|
|
$roles = $userRoleModel->select('roles.name')
|
|
->join('roles', 'roles.id = user_roles.role_id')
|
|
->where('user_roles.user_id', $user['id'])
|
|
->get()
|
|
->getResultArray();
|
|
|
|
if (empty($roles)) {
|
|
log_message('error', 'No roles found for user ID: ' . $user['id']);
|
|
return redirect()->back()->with('error', 'Role not assigned. Please contact support.');
|
|
}
|
|
|
|
$roleNames = array_column($roles, 'name');
|
|
|
|
session()->set([
|
|
'user_id' => $user['id'],
|
|
'user_email' => $user['email'],
|
|
'user_name' => $user['firstname'] . ' ' . $user['lastname'],
|
|
'user_type' => $user['user_type'],
|
|
'is_logged_in' => true,
|
|
'login_time' => time(),
|
|
'roles' => $roleNames,
|
|
'semester' => $this->semester,
|
|
'school_year' => $this->schoolYear,
|
|
]);
|
|
$this->applyStylePreferences((int) $user['id']);
|
|
log_message('debug', 'Session after login: ' . print_r(session()->get(), true));
|
|
//dd('Login successful. Roles:', $roleNames, session()->get());
|
|
|
|
if (count($roleNames) === 1) {
|
|
// One role → set and redirect directly
|
|
session()->set('role', $roleNames[0]);
|
|
return $this->redirectToDashboard([$roleNames[0]]);
|
|
} else {
|
|
// Multiple roles → redirect to role selection view
|
|
return redirect()->to('/select-role');
|
|
}
|
|
|
|
}
|
|
|
|
private function applyStylePreferences(int $userId): void
|
|
{
|
|
if (!$userId) {
|
|
return;
|
|
}
|
|
$prefs = $this->preferencesModel->where('user_id', $userId)->first();
|
|
if (!$prefs) {
|
|
return;
|
|
}
|
|
$styleCfg = config('Style');
|
|
|
|
$styleColor = (string) ($prefs['style_color'] ?? '');
|
|
if ($styleColor !== '' && isset(($styleCfg->stylePalettes ?? [])[$styleColor])) {
|
|
session()->set('style_color', $styleColor);
|
|
}
|
|
|
|
$menuColor = (string) ($prefs['menu_color'] ?? '');
|
|
if ($menuColor === 'custom') {
|
|
$bg = (string) ($prefs['menu_custom_bg'] ?? '#0f172a');
|
|
$tx = (string) ($prefs['menu_custom_text'] ?? '#ffffff');
|
|
$mode = (string) ($prefs['menu_custom_mode'] ?? 'dark');
|
|
session()->set('menu_color', 'custom');
|
|
session()->set('menu_custom_bg', $bg);
|
|
session()->set('menu_custom_text', $tx);
|
|
session()->set('menu_custom_mode', $mode);
|
|
} elseif ($menuColor !== '' && isset(($styleCfg->menuPalettes ?? [])[$menuColor])) {
|
|
session()->set('menu_color', $menuColor);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private function redirectToDashboard(array $roles)
|
|
{
|
|
if (empty($roles)) {
|
|
log_message('error', 'Empty roles array passed to redirectToDashboard.');
|
|
return redirect()->to('/landing_page/guest_dashboard');
|
|
}
|
|
|
|
$roleModel = new RoleModel();
|
|
|
|
// Resolve all candidate roles (by name or slug), ordered by priority ASC
|
|
$rows = $roleModel->findByNamesOrSlugs($roles);
|
|
|
|
if (!empty($rows)) {
|
|
$route = $rows[0]['dashboard_route'] ?? '/landing_page/guest_dashboard';
|
|
log_message('debug', 'Redirecting user to: ' . $route);
|
|
return redirect()->to($route);
|
|
}
|
|
|
|
log_message('warning', 'No matching role found. Redirecting to guest dashboard.');
|
|
return redirect()->to('/landing_page/guest_dashboard');
|
|
}
|
|
|
|
|
|
public function setRole()
|
|
{
|
|
$selectedRole = $this->request->getPost('selected_role');
|
|
$availableRoles = session()->get('roles');
|
|
|
|
if (!$selectedRole || !in_array($selectedRole, $availableRoles)) {
|
|
return redirect()->to('/select-role')->with('error', 'Invalid role selected.');
|
|
}
|
|
|
|
session()->set('role', $selectedRole);
|
|
return $this->redirectToDashboard([$selectedRole]);
|
|
}
|
|
|
|
public function selectRole()
|
|
{
|
|
$roles = session()->get('roles');
|
|
|
|
if (!$roles || !is_array($roles)) {
|
|
return redirect()->to('/login')->with('error', 'No roles available.');
|
|
}
|
|
|
|
return view('auth/select_role', ['roles' => $roles]);
|
|
}
|
|
|
|
}
|