mirror of
https://github.com/itflow-org/itflow
synced 2026-09-22 22:51:17 +00:00
Fix Login flow where user agent and client exists and agent has MFA but will not let them continue, also update some wording and button colors. Also dont show email password fields again after success and login as agent and client is shown.
This commit is contained in:
381
login.php
381
login.php
@@ -2,13 +2,10 @@
|
|||||||
|
|
||||||
// Unified login (Agent + Client) using one email & password
|
// Unified login (Agent + Client) using one email & password
|
||||||
|
|
||||||
// Enforce a Content Security Policy for security against cross-site scripting
|
|
||||||
header("Content-Security-Policy: default-src 'self'");
|
header("Content-Security-Policy: default-src 'self'");
|
||||||
|
|
||||||
// Check if the config.php file exists
|
|
||||||
if (!file_exists('config.php')) {
|
if (!file_exists('config.php')) {
|
||||||
// Redirect to the setup page if config.php doesn't exist
|
header("Location: /setup");
|
||||||
header("Location: /setup"); // Must use header as functions aren't included yet
|
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,12 +13,9 @@ require_once "config.php";
|
|||||||
require_once "functions.php";
|
require_once "functions.php";
|
||||||
require_once "plugins/totp/totp.php";
|
require_once "plugins/totp/totp.php";
|
||||||
|
|
||||||
// Sessions & cookies
|
|
||||||
if (session_status() === PHP_SESSION_NONE) {
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
// HTTP-Only cookies
|
|
||||||
ini_set("session.cookie_httponly", true);
|
ini_set("session.cookie_httponly", true);
|
||||||
|
|
||||||
// Tell client to only send cookie(s) over HTTPS
|
|
||||||
if ($config_https_only || !isset($config_https_only)) {
|
if ($config_https_only || !isset($config_https_only)) {
|
||||||
ini_set("session.cookie_secure", true);
|
ini_set("session.cookie_secure", true);
|
||||||
}
|
}
|
||||||
@@ -29,28 +23,25 @@ if (session_status() === PHP_SESSION_NONE) {
|
|||||||
session_start();
|
session_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if setup mode is enabled or the variable is missing
|
|
||||||
if (!isset($config_enable_setup) || $config_enable_setup == 1) {
|
if (!isset($config_enable_setup) || $config_enable_setup == 1) {
|
||||||
// Redirect to the setup page
|
|
||||||
header("Location: /setup");
|
header("Location: /setup");
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the application is configured for HTTPS-only access
|
if (
|
||||||
if ($config_https_only && (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') && (!isset($_SERVER['HTTP_X_FORWARDED_PROTO']) || $_SERVER['HTTP_X_FORWARDED_PROTO'] !== 'https')) {
|
$config_https_only
|
||||||
|
&& (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on')
|
||||||
|
&& (!isset($_SERVER['HTTP_X_FORWARDED_PROTO']) || $_SERVER['HTTP_X_FORWARDED_PROTO'] !== 'https')
|
||||||
|
) {
|
||||||
echo "Login is restricted as ITFlow defaults to HTTPS-only for enhanced security. To login using HTTP, modify the config.php file by setting config_https_only to false. However, this is strongly discouraged, especially when accessing from potentially unsafe networks like the internet.";
|
echo "Login is restricted as ITFlow defaults to HTTPS-only for enhanced security. To login using HTTP, modify the config.php file by setting config_https_only to false. However, this is strongly discouraged, especially when accessing from potentially unsafe networks like the internet.";
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set Timezone after session_start
|
|
||||||
require_once "includes/inc_set_timezone.php";
|
require_once "includes/inc_set_timezone.php";
|
||||||
|
|
||||||
// IP & User Agent for logging
|
|
||||||
$session_ip = sanitizeInput(getIP());
|
$session_ip = sanitizeInput(getIP());
|
||||||
$session_user_agent = sanitizeInput($_SERVER['HTTP_USER_AGENT'] ?? '');
|
$session_user_agent = sanitizeInput($_SERVER['HTTP_USER_AGENT'] ?? '');
|
||||||
|
|
||||||
// Block brute force password attacks - check recent failed login attempts for this IP
|
|
||||||
// Block access if more than 15 failed login attempts have happened in the last 10 minutes
|
|
||||||
$row = mysqli_fetch_assoc(mysqli_query(
|
$row = mysqli_fetch_assoc(mysqli_query(
|
||||||
$mysqli,
|
$mysqli,
|
||||||
"SELECT COUNT(log_id) AS failed_login_count
|
"SELECT COUNT(log_id) AS failed_login_count
|
||||||
@@ -63,15 +54,12 @@ $row = mysqli_fetch_assoc(mysqli_query(
|
|||||||
$failed_login_count = intval($row['failed_login_count']);
|
$failed_login_count = intval($row['failed_login_count']);
|
||||||
|
|
||||||
if ($failed_login_count >= 15) {
|
if ($failed_login_count >= 15) {
|
||||||
|
|
||||||
logAction("Login", "Blocked", "$session_ip was blocked access to login due to IP lockout");
|
logAction("Login", "Blocked", "$session_ip was blocked access to login due to IP lockout");
|
||||||
|
|
||||||
// Inform user & quit processing page
|
|
||||||
header("HTTP/1.1 429 Too Many Requests");
|
header("HTTP/1.1 429 Too Many Requests");
|
||||||
exit("<h2>$config_app_name</h2>Your IP address has been blocked due to repeated failed login attempts. Please try again later. <br><br>This action has been logged.");
|
exit("<h2>$config_app_name</h2>Your IP address has been blocked due to repeated failed login attempts. Please try again later. <br><br>This action has been logged.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query Settings for company
|
// Settings
|
||||||
$sql_settings = mysqli_query($mysqli, "
|
$sql_settings = mysqli_query($mysqli, "
|
||||||
SELECT settings.*, companies.company_name, companies.company_logo
|
SELECT settings.*, companies.company_name, companies.company_logo
|
||||||
FROM settings
|
FROM settings
|
||||||
@@ -80,13 +68,11 @@ $sql_settings = mysqli_query($mysqli, "
|
|||||||
");
|
");
|
||||||
$row = mysqli_fetch_array($sql_settings);
|
$row = mysqli_fetch_array($sql_settings);
|
||||||
|
|
||||||
// Company info
|
|
||||||
$company_name = $row['company_name'];
|
$company_name = $row['company_name'];
|
||||||
$company_logo = $row['company_logo'];
|
$company_logo = $row['company_logo'];
|
||||||
$config_start_page = nullable_htmlentities($row['config_start_page']);
|
$config_start_page = nullable_htmlentities($row['config_start_page']);
|
||||||
$config_login_message = nullable_htmlentities($row['config_login_message']);
|
$config_login_message = nullable_htmlentities($row['config_login_message']);
|
||||||
|
|
||||||
// Mail
|
|
||||||
$config_smtp_host = $row['config_smtp_host'];
|
$config_smtp_host = $row['config_smtp_host'];
|
||||||
$config_smtp_port = intval($row['config_smtp_port']);
|
$config_smtp_port = intval($row['config_smtp_port']);
|
||||||
$config_smtp_encryption = $row['config_smtp_encryption'];
|
$config_smtp_encryption = $row['config_smtp_encryption'];
|
||||||
@@ -95,49 +81,96 @@ $config_smtp_password = $row['config_smtp_password'];
|
|||||||
$config_mail_from_email = sanitizeInput($row['config_mail_from_email']);
|
$config_mail_from_email = sanitizeInput($row['config_mail_from_email']);
|
||||||
$config_mail_from_name = sanitizeInput($row['config_mail_from_name']);
|
$config_mail_from_name = sanitizeInput($row['config_mail_from_name']);
|
||||||
|
|
||||||
// Client Portal Enabled
|
|
||||||
$config_client_portal_enable = intval($row['config_client_portal_enable']);
|
$config_client_portal_enable = intval($row['config_client_portal_enable']);
|
||||||
$config_login_remember_me_expire = intval($row['config_login_remember_me_expire']);
|
$config_login_remember_me_expire = intval($row['config_login_remember_me_expire']);
|
||||||
|
|
||||||
// Login key (if setup)
|
|
||||||
$config_login_key_required = $row['config_login_key_required'];
|
$config_login_key_required = $row['config_login_key_required'];
|
||||||
$config_login_key_secret = $row['config_login_key_secret'];
|
$config_login_key_secret = $row['config_login_key_secret'];
|
||||||
|
|
||||||
// Azure / Entra for client
|
|
||||||
$azure_client_id = $row['config_azure_client_id'] ?? null;
|
$azure_client_id = $row['config_azure_client_id'] ?? null;
|
||||||
|
|
||||||
$response = null;
|
$response = null;
|
||||||
$token_field = null;
|
$token_field = null;
|
||||||
$show_role_choice = false;
|
$show_role_choice = false;
|
||||||
|
|
||||||
$email = '';
|
$email = '';
|
||||||
$password = '';
|
$password = ''; // only ever used in the initial POST request
|
||||||
|
|
||||||
// Handle POST login request (normal login or role choice)
|
// Helpers
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_POST['role_choice']))) {
|
function pendingExpired($sess, $ttl_seconds = 120) {
|
||||||
|
return !$sess || empty($sess['created']) || (time() - intval($sess['created']) > $ttl_seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST handling
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_POST['role_choice']) || isset($_POST['mfa_login']))) {
|
||||||
|
|
||||||
|
$role_choice = $_POST['role_choice'] ?? null;
|
||||||
|
|
||||||
|
$is_login_step = isset($_POST['login']);
|
||||||
|
$is_role_step = isset($_POST['role_choice']) && !$is_login_step && !isset($_POST['mfa_login']);
|
||||||
|
$is_mfa_step = isset($_POST['mfa_login']);
|
||||||
|
|
||||||
|
// -----------------------------------
|
||||||
|
// STEP 2: ROLE CHOICE (no email/pass)
|
||||||
|
// -----------------------------------
|
||||||
|
if ($is_role_step) {
|
||||||
|
|
||||||
|
$posted_token = $_POST['pending_login_token'] ?? '';
|
||||||
|
$sess = $_SESSION['pending_dual_login'] ?? null;
|
||||||
|
|
||||||
|
if (pendingExpired($sess) || empty($posted_token) || empty($sess['token']) || !hash_equals($sess['token'], $posted_token)) {
|
||||||
|
unset($_SESSION['pending_dual_login']);
|
||||||
|
header("HTTP/1.1 401 Unauthorized");
|
||||||
|
$response = "
|
||||||
|
<div class='alert alert-danger'>
|
||||||
|
Your login session expired. Please sign in again.
|
||||||
|
</div>";
|
||||||
|
} else {
|
||||||
|
$email = sanitizeInput($sess['email'] ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------
|
||||||
|
// STEP 3: MFA SUBMIT (no email/pass)
|
||||||
|
// -----------------------------------
|
||||||
|
if ($is_mfa_step && empty($response)) {
|
||||||
|
|
||||||
|
$posted_token = $_POST['pending_mfa_token'] ?? '';
|
||||||
|
$sess = $_SESSION['pending_mfa_login'] ?? null;
|
||||||
|
|
||||||
|
if (pendingExpired($sess) || empty($posted_token) || empty($sess['token']) || !hash_equals($sess['token'], $posted_token)) {
|
||||||
|
unset($_SESSION['pending_mfa_login']);
|
||||||
|
header("HTTP/1.1 401 Unauthorized");
|
||||||
|
$response = "
|
||||||
|
<div class='alert alert-danger'>
|
||||||
|
Your MFA session expired. Please sign in again.
|
||||||
|
</div>";
|
||||||
|
} else {
|
||||||
|
$email = sanitizeInput($sess['email'] ?? '');
|
||||||
|
$role_choice = 'agent';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------
|
||||||
|
// STEP 1: INITIAL CREDENTIALS
|
||||||
|
// -----------------------------------
|
||||||
|
if ($is_login_step && empty($response)) {
|
||||||
$email = sanitizeInput($_POST['email'] ?? '');
|
$email = sanitizeInput($_POST['email'] ?? '');
|
||||||
$password = $_POST['password'] ?? '';
|
$password = $_POST['password'] ?? '';
|
||||||
$role_choice = $_POST['role_choice'] ?? null; // 'agent' or 'client'
|
|
||||||
|
|
||||||
// Basic validation
|
|
||||||
if (empty($email) || empty($password) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
if (empty($email) || empty($password) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
header("HTTP/1.1 401 Unauthorized");
|
header("HTTP/1.1 401 Unauthorized");
|
||||||
$response = "
|
$response = "
|
||||||
<div class='alert alert-danger'>
|
<div class='alert alert-danger'>
|
||||||
Incorrect username or password.
|
Incorrect username or password.
|
||||||
<button class='close' data-dismiss='alert'>×</button>
|
|
||||||
</div>";
|
</div>";
|
||||||
} else {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
// Continue only if no response error
|
||||||
* Unified lookup:
|
if (empty($response)) {
|
||||||
* - user_type = 1 → Agent
|
|
||||||
* - user_type = 2 → Client (must not be archived, client not archived)
|
// Query all possible matches for that email
|
||||||
* We fetch all possible matches for this email, then verify password per row.
|
|
||||||
* If both an agent and a client match with the same password:
|
|
||||||
* - First, show choice buttons (Agent / Client).
|
|
||||||
* - When user clicks a choice, we honor role_choice.
|
|
||||||
*/
|
|
||||||
$sql = mysqli_query($mysqli, "
|
$sql = mysqli_query($mysqli, "
|
||||||
SELECT users.*,
|
SELECT users.*,
|
||||||
user_settings.*,
|
user_settings.*,
|
||||||
@@ -159,36 +192,63 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_
|
|||||||
$agentRow = null;
|
$agentRow = null;
|
||||||
$clientRow = null;
|
$clientRow = null;
|
||||||
|
|
||||||
|
// Step 1: verify password. Step 2/3: use stored allowed ids.
|
||||||
|
$allowed_agent_id = null;
|
||||||
|
$allowed_client_id = null;
|
||||||
|
|
||||||
|
if ($is_role_step) {
|
||||||
|
$sess = $_SESSION['pending_dual_login'] ?? null;
|
||||||
|
$allowed_agent_id = isset($sess['agent_user_id']) ? intval($sess['agent_user_id']) : null;
|
||||||
|
$allowed_client_id = isset($sess['client_user_id']) ? intval($sess['client_user_id']) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($is_mfa_step) {
|
||||||
|
$sess = $_SESSION['pending_mfa_login'] ?? null;
|
||||||
|
$allowed_agent_id = isset($sess['agent_user_id']) ? intval($sess['agent_user_id']) : null;
|
||||||
|
}
|
||||||
|
|
||||||
while ($r = mysqli_fetch_assoc($sql)) {
|
while ($r = mysqli_fetch_assoc($sql)) {
|
||||||
|
|
||||||
|
$ut = intval($r['user_type']);
|
||||||
|
|
||||||
|
if ($is_login_step) {
|
||||||
|
// Only Step 1 checks password
|
||||||
if (!password_verify($password, $r['user_password'])) {
|
if (!password_verify($password, $r['user_password'])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (intval($r['user_type']) === 1 && $agentRow === null) {
|
} else {
|
||||||
|
// Step 2/3: restrict to ids we previously verified
|
||||||
|
if ($ut === 1 && $allowed_agent_id !== null && intval($r['user_id']) !== $allowed_agent_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($ut === 2 && $allowed_client_id !== null && intval($r['user_id']) !== $allowed_client_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ut === 1 && $agentRow === null) {
|
||||||
$agentRow = $r;
|
$agentRow = $r;
|
||||||
}
|
}
|
||||||
if (intval($r['user_type']) === 2 && $clientRow === null) {
|
if ($ut === 2 && $clientRow === null) {
|
||||||
$clientRow = $r;
|
$clientRow = $r;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$selectedRow = null;
|
|
||||||
$selectedType = null; // 1 = agent, 2 = client
|
|
||||||
|
|
||||||
if ($agentRow === null && $clientRow === null) {
|
if ($agentRow === null && $clientRow === null) {
|
||||||
|
|
||||||
// No matching user/password combo
|
|
||||||
header("HTTP/1.1 401 Unauthorized");
|
header("HTTP/1.1 401 Unauthorized");
|
||||||
logAction("Login", "Failed", "Failed login attempt using $email");
|
logAction("Login", "Failed", "Failed login attempt using $email");
|
||||||
|
|
||||||
$response = "
|
$response = "
|
||||||
<div class='alert alert-danger'>
|
<div class='alert alert-danger'>
|
||||||
Incorrect username or password.
|
Incorrect username or password.
|
||||||
<button class='close' data-dismiss='alert'>×</button>
|
|
||||||
</div>";
|
</div>";
|
||||||
|
} else {
|
||||||
|
|
||||||
} elseif ($agentRow !== null && $clientRow !== null) {
|
$selectedRow = null;
|
||||||
|
$selectedType = null; // 1 agent, 2 client
|
||||||
|
|
||||||
|
// Dual role
|
||||||
|
if ($agentRow !== null && $clientRow !== null) {
|
||||||
|
|
||||||
// Both agent and client accounts share same email + password
|
|
||||||
if ($role_choice === 'agent') {
|
if ($role_choice === 'agent') {
|
||||||
$selectedRow = $agentRow;
|
$selectedRow = $agentRow;
|
||||||
$selectedType = 1;
|
$selectedType = 1;
|
||||||
@@ -196,18 +256,41 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_
|
|||||||
$selectedRow = $clientRow;
|
$selectedRow = $clientRow;
|
||||||
$selectedType = 2;
|
$selectedType = 2;
|
||||||
} else {
|
} else {
|
||||||
// First time we realise this is a dual-role account: ask user to pick
|
// Show role choice screen
|
||||||
$show_role_choice = true;
|
$show_role_choice = true;
|
||||||
|
|
||||||
|
// If this is the first time (Step 1), we need to stash allowed ids and (optional) decrypted agent encryption key
|
||||||
|
// WITHOUT storing password.
|
||||||
|
if ($is_login_step) {
|
||||||
|
|
||||||
|
$pending_token = bin2hex(random_bytes(32));
|
||||||
|
|
||||||
|
// If agent has user-specific encryption ciphertext, decrypt it NOW while password is present.
|
||||||
|
$agent_master_key = null;
|
||||||
|
$agent_cipher = $agentRow['user_specific_encryption_ciphertext'] ?? null;
|
||||||
|
if (!empty($agent_cipher)) {
|
||||||
|
$agent_master_key = decryptUserSpecificKey($agent_cipher, $password);
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['pending_dual_login'] = [
|
||||||
|
'email' => $email,
|
||||||
|
'agent_user_id' => intval($agentRow['user_id']),
|
||||||
|
'client_user_id' => intval($clientRow['user_id']),
|
||||||
|
'agent_master_key' => $agent_master_key, // may be null
|
||||||
|
'token' => $pending_token,
|
||||||
|
'created' => time()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
$response = "
|
$response = "
|
||||||
<div class='alert alert-info'>
|
<div class='alert alert-light'>
|
||||||
This login can be used as either an Agent account or a Client Portal account.
|
This login can be used as either an Agent account or a Client Portal account.
|
||||||
Please choose how you want to continue.
|
Please choose how you want to continue.
|
||||||
<button class='close' data-dismiss='alert'>×</button>
|
|
||||||
</div>";
|
</div>";
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Only one valid row (agent OR client)
|
// Single role
|
||||||
if ($agentRow !== null) {
|
if ($agentRow !== null) {
|
||||||
$selectedRow = $agentRow;
|
$selectedRow = $agentRow;
|
||||||
$selectedType = 1;
|
$selectedType = 1;
|
||||||
@@ -217,19 +300,20 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have a specific user selected, proceed with actual login
|
// Proceed if selected
|
||||||
if ($selectedRow !== null && $selectedType !== null) {
|
if ($selectedRow !== null && $selectedType !== null) {
|
||||||
|
|
||||||
|
// Clear dual pending once we actually proceed
|
||||||
|
unset($_SESSION['pending_dual_login']);
|
||||||
|
|
||||||
$user_id = intval($selectedRow['user_id']);
|
$user_id = intval($selectedRow['user_id']);
|
||||||
$user_email = sanitizeInput($selectedRow['user_email']);
|
$user_email = sanitizeInput($selectedRow['user_email']);
|
||||||
$session_user_id = $user_id; // to pass the user_id to logAction function
|
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// AGENT LOGIN FLOW
|
// AGENT FLOW
|
||||||
// =========================
|
// =========================
|
||||||
if ($selectedType === 1) {
|
if ($selectedType === 1) {
|
||||||
// Login key verification
|
|
||||||
// If no/incorrect 'key' is supplied, send to client portal instead
|
|
||||||
if ($config_login_key_required) {
|
if ($config_login_key_required) {
|
||||||
if (!isset($_GET['key']) || $_GET['key'] !== $config_login_key_secret) {
|
if (!isset($_GET['key']) || $_GET['key'] !== $config_login_key_secret) {
|
||||||
redirect();
|
redirect();
|
||||||
@@ -239,9 +323,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_
|
|||||||
$user_name = sanitizeInput($selectedRow['user_name']);
|
$user_name = sanitizeInput($selectedRow['user_name']);
|
||||||
$token = sanitizeInput($selectedRow['user_token']);
|
$token = sanitizeInput($selectedRow['user_token']);
|
||||||
$force_mfa = intval($selectedRow['user_config_force_mfa']);
|
$force_mfa = intval($selectedRow['user_config_force_mfa']);
|
||||||
$user_role_id = intval($selectedRow['user_role_id']);
|
|
||||||
$user_encryption_ciphertext = $selectedRow['user_specific_encryption_ciphertext'];
|
$user_encryption_ciphertext = $selectedRow['user_specific_encryption_ciphertext'];
|
||||||
$user_extension_key = $selectedRow['user_extension_key'];
|
|
||||||
|
|
||||||
$current_code = 0;
|
$current_code = 0;
|
||||||
if (isset($_POST['current_code'])) {
|
if (isset($_POST['current_code'])) {
|
||||||
@@ -252,11 +334,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_
|
|||||||
$extended_log = '';
|
$extended_log = '';
|
||||||
|
|
||||||
if (empty($token)) {
|
if (empty($token)) {
|
||||||
// MFA is not configured
|
$mfa_is_complete = true; // no MFA configured
|
||||||
$mfa_is_complete = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate MFA via a remember-me cookie
|
// remember-me cookie allows bypass
|
||||||
if (isset($_COOKIE['rememberme'])) {
|
if (isset($_COOKIE['rememberme'])) {
|
||||||
$remember_tokens = mysqli_query($mysqli, "
|
$remember_tokens = mysqli_query($mysqli, "
|
||||||
SELECT remember_token_token
|
SELECT remember_token_token
|
||||||
@@ -280,30 +361,24 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($mfa_is_complete) {
|
if ($mfa_is_complete) {
|
||||||
// FULL AGENT LOGIN SUCCESS
|
|
||||||
|
|
||||||
// Create a remember me token, if requested
|
// Clear pending MFA if exists
|
||||||
|
unset($_SESSION['pending_mfa_login']);
|
||||||
|
|
||||||
|
// Remember me token creation
|
||||||
if (isset($_POST['remember_me'])) {
|
if (isset($_POST['remember_me'])) {
|
||||||
$newRememberToken = bin2hex(random_bytes(64));
|
$newRememberToken = bin2hex(random_bytes(64));
|
||||||
setcookie(
|
setcookie('rememberme', $newRememberToken, time() + 86400 * $config_login_remember_me_expire, "/", null, true, true);
|
||||||
'rememberme',
|
|
||||||
$newRememberToken,
|
|
||||||
time() + 86400 * $config_login_remember_me_expire,
|
|
||||||
"/",
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
mysqli_query($mysqli, "
|
mysqli_query($mysqli, "
|
||||||
INSERT INTO remember_tokens
|
INSERT INTO remember_tokens
|
||||||
SET remember_token_user_id = $user_id,
|
SET remember_token_user_id = $user_id,
|
||||||
remember_token_token = '$newRememberToken'
|
remember_token_token = '$newRememberToken'
|
||||||
");
|
");
|
||||||
|
|
||||||
$extended_log .= ", generated a new remember-me token";
|
$extended_log .= ", generated a new remember-me token";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check this login isn't suspicious
|
// Suspicious login checks / email notify (kept from your code)
|
||||||
$sql_ip_prev_logins = mysqli_fetch_assoc(mysqli_query($mysqli, "
|
$sql_ip_prev_logins = mysqli_fetch_assoc(mysqli_query($mysqli, "
|
||||||
SELECT COUNT(log_id) AS ip_previous_logins
|
SELECT COUNT(log_id) AS ip_previous_logins
|
||||||
FROM logs
|
FROM logs
|
||||||
@@ -324,60 +399,91 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_
|
|||||||
"));
|
"));
|
||||||
$ua_prev_logins = sanitizeInput($sql_ua_prev_logins['ua_previous_logins']);
|
$ua_prev_logins = sanitizeInput($sql_ua_prev_logins['ua_previous_logins']);
|
||||||
|
|
||||||
// Notify if both the user agent and IP are different
|
|
||||||
if (!empty($config_smtp_host) && $ip_previous_logins == 0 && $ua_prev_logins == 0) {
|
if (!empty($config_smtp_host) && $ip_previous_logins == 0 && $ua_prev_logins == 0) {
|
||||||
$subject = "$config_app_name new login for $user_name";
|
$subject = "$config_app_name new login for $user_name";
|
||||||
$body = "Hi $user_name, <br><br>A recent successful login to your $config_app_name account was considered a little unusual. If this was you, you can safely ignore this email!<br><br>IP Address: $session_ip<br> User Agent: $session_user_agent <br><br>If you did not perform this login, your credentials may be compromised. <br><br>Thanks, <br>ITFlow";
|
$body = "Hi $user_name, <br><br>A recent successful login to your $config_app_name account was considered a little unusual. If this was you, you can safely ignore this email!<br><br>IP Address: $session_ip<br> User Agent: $session_user_agent <br><br>If you did not perform this login, your credentials may be compromised. <br><br>Thanks, <br>ITFlow";
|
||||||
|
|
||||||
$data = [
|
$data = [[
|
||||||
[
|
|
||||||
'from' => $config_mail_from_email,
|
'from' => $config_mail_from_email,
|
||||||
'from_name' => $config_mail_from_name,
|
'from_name' => $config_mail_from_name,
|
||||||
'recipient' => $user_email,
|
'recipient' => $user_email,
|
||||||
'recipient_name' => $user_name,
|
'recipient_name' => $user_name,
|
||||||
'subject' => $subject,
|
'subject' => $subject,
|
||||||
'body' => $body
|
'body' => $body
|
||||||
]
|
]];
|
||||||
];
|
|
||||||
addToMailQueue($data);
|
addToMailQueue($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
logAction("Login", "Success", "$user_name successfully logged in $extended_log", 0, $user_id);
|
logAction("Login", "Success", "$user_name successfully logged in $extended_log", 0, $user_id);
|
||||||
|
|
||||||
// Session info
|
|
||||||
$_SESSION['user_id'] = $user_id;
|
$_SESSION['user_id'] = $user_id;
|
||||||
$_SESSION['csrf_token'] = randomString(32);
|
$_SESSION['csrf_token'] = randomString(32);
|
||||||
$_SESSION['logged'] = true;
|
$_SESSION['logged'] = true;
|
||||||
|
|
||||||
// Forcing MFA
|
|
||||||
if ($force_mfa == 1 && $token == NULL) {
|
if ($force_mfa == 1 && $token == NULL) {
|
||||||
$config_start_page = "user/mfa_enforcement.php";
|
$config_start_page = "user/mfa_enforcement.php";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup encryption session key
|
// ✅ Setup encryption session key WITHOUT PASSWORD IN SESSION
|
||||||
|
// If we are coming from MFA step, master key is in pending_mfa_login.
|
||||||
|
// If we are coming from login step with no MFA, decrypt now.
|
||||||
|
$site_encryption_master_key = null;
|
||||||
|
|
||||||
|
if ($is_mfa_step) {
|
||||||
|
$sess = $_SESSION['pending_mfa_login'] ?? null;
|
||||||
|
if ($sess && isset($sess['agent_master_key'])) {
|
||||||
|
$site_encryption_master_key = $sess['agent_master_key'];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No MFA step: password exists in this request (Step 1)
|
||||||
if (!empty($user_encryption_ciphertext)) {
|
if (!empty($user_encryption_ciphertext)) {
|
||||||
$site_encryption_master_key = decryptUserSpecificKey($user_encryption_ciphertext, $password);
|
$site_encryption_master_key = decryptUserSpecificKey($user_encryption_ciphertext, $password);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($site_encryption_master_key)) {
|
||||||
generateUserSessionKey($site_encryption_master_key);
|
generateUserSessionKey($site_encryption_master_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect to last visited or config home
|
// Redirect
|
||||||
if (isset($_GET['last_visited']) && (str_starts_with(base64_decode($_GET['last_visited']), '/agent') || str_starts_with(base64_decode($_GET['last_visited']), '/admin'))) {
|
if (isset($_GET['last_visited']) && (str_starts_with(base64_decode($_GET['last_visited']), '/agent') || str_starts_with(base64_decode($_GET['last_visited']), '/admin'))) {
|
||||||
|
|
||||||
redirect($_SERVER["REQUEST_SCHEME"] . "://" . $config_base_url . base64_decode($_GET['last_visited']));
|
redirect($_SERVER["REQUEST_SCHEME"] . "://" . $config_base_url . base64_decode($_GET['last_visited']));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
redirect("agent/$config_start_page");
|
redirect("agent/$config_start_page");
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// MFA is configured and needs to be confirmed, or was unsuccessful
|
// MFA required — store *only what we need*, not password
|
||||||
|
$pending_mfa_token = bin2hex(random_bytes(32));
|
||||||
|
|
||||||
|
// If we arrived here from role-choice step, the agent master key may be in pending_dual_login
|
||||||
|
// If we arrived from initial login step, decrypt now (password in memory) and store master key.
|
||||||
|
$agent_master_key = null;
|
||||||
|
|
||||||
|
if ($is_role_step) {
|
||||||
|
$sess = $_SESSION['pending_dual_login'] ?? null;
|
||||||
|
if ($sess && isset($sess['agent_master_key'])) {
|
||||||
|
$agent_master_key = $sess['agent_master_key'];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!empty($user_encryption_ciphertext)) {
|
||||||
|
$agent_master_key = decryptUserSpecificKey($user_encryption_ciphertext, $password);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['pending_mfa_login'] = [
|
||||||
|
'email' => $user_email,
|
||||||
|
'agent_user_id' => $user_id,
|
||||||
|
'agent_master_key'=> $agent_master_key, // may be null
|
||||||
|
'token' => $pending_mfa_token,
|
||||||
|
'created' => time()
|
||||||
|
];
|
||||||
|
|
||||||
// HTML code for the token input field
|
|
||||||
$token_field = "
|
$token_field = "
|
||||||
<div class='input-group mb-3'>
|
<div class='input-group mb-3'>
|
||||||
<input type='text' inputmode='numeric' pattern='[0-9]*' maxlength='6'
|
<input type='text' inputmode='numeric' pattern='[0-9]*' maxlength='6'
|
||||||
class='form-control' placeholder='Enter your 2FA code'
|
class='form-control' placeholder='Verify your 2FA code'
|
||||||
name='current_code' required autofocus>
|
name='current_code' required autofocus>
|
||||||
<div class='input-group-append'>
|
<div class='input-group-append'>
|
||||||
<div class='input-group-text'>
|
<div class='input-group-text'>
|
||||||
@@ -387,47 +493,40 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_
|
|||||||
</div>";
|
</div>";
|
||||||
|
|
||||||
if ($current_code !== 0) {
|
if ($current_code !== 0) {
|
||||||
// Logging
|
|
||||||
logAction("Login", "MFA Failed", "$user_email failed MFA", 0, $user_id);
|
logAction("Login", "MFA Failed", "$user_email failed MFA", 0, $user_id);
|
||||||
|
|
||||||
// Email the tech to advise their credentials may be compromised
|
|
||||||
if (!empty($config_smtp_host)) {
|
if (!empty($config_smtp_host)) {
|
||||||
$subject = "Important: $config_app_name failed 2FA login attempt for $user_name";
|
$subject = "Important: $config_app_name failed 2FA login attempt for $user_name";
|
||||||
$body = "Hi $user_name, <br><br>A recent login to your $config_app_name account was unsuccessful due to an incorrect 2FA code. If you did not attempt this login, your credentials may be compromised. <br><br>Thanks, <br>ITFlow";
|
$body = "Hi $user_name, <br><br>A recent login to your $config_app_name account was unsuccessful due to an incorrect 2FA code. If you did not attempt this login, your credentials may be compromised. <br><br>Thanks, <br>ITFlow";
|
||||||
$data = [
|
$data = [[
|
||||||
[
|
|
||||||
'from' => $config_mail_from_email,
|
'from' => $config_mail_from_email,
|
||||||
'from_name' => $config_mail_from_name,
|
'from_name' => $config_mail_from_name,
|
||||||
'recipient' => $user_email,
|
'recipient' => $user_email,
|
||||||
'recipient_name' => $user_name,
|
'recipient_name' => $user_name,
|
||||||
'subject' => $subject,
|
'subject' => $subject,
|
||||||
'body' => $body
|
'body' => $body
|
||||||
]
|
]];
|
||||||
];
|
|
||||||
addToMailQueue($data);
|
addToMailQueue($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
$response = "
|
$response = "
|
||||||
<div class='alert alert-warning'>
|
<div class='alert alert-danger'>
|
||||||
Please Enter 2FA Code!
|
Please enter a valid 2FA code.
|
||||||
<button class='close' data-dismiss='alert'>×</button>
|
|
||||||
</div>";
|
</div>";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// CLIENT LOGIN FLOW
|
// CLIENT FLOW
|
||||||
// =========================
|
// =========================
|
||||||
} elseif ($selectedType === 2) {
|
} elseif ($selectedType === 2) {
|
||||||
|
|
||||||
if ($config_client_portal_enable != 1) {
|
if ($config_client_portal_enable != 1) {
|
||||||
// Client portal disabled
|
|
||||||
header("HTTP/1.1 401 Unauthorized");
|
header("HTTP/1.1 401 Unauthorized");
|
||||||
logAction("Client Login", "Failed", "Client portal disabled; login attempt using $email");
|
logAction("Client Login", "Failed", "Client portal disabled; login attempt using $email");
|
||||||
$response = "
|
$response = "
|
||||||
<div class='alert alert-danger'>
|
<div class='alert alert-danger'>
|
||||||
Incorrect username or password.
|
Incorrect username or password.
|
||||||
<button class='close' data-dismiss='alert'>×</button>
|
|
||||||
</div>";
|
</div>";
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
@@ -451,22 +550,25 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// Not allowed or invalid
|
|
||||||
logAction("Client Login", "Failed", "Failed client portal login attempt using $email (invalid auth method or missing contact/client)", $client_id ?? 0, $user_id);
|
logAction("Client Login", "Failed", "Failed client portal login attempt using $email (invalid auth method or missing contact/client)", $client_id ?? 0, $user_id);
|
||||||
|
|
||||||
header("HTTP/1.1 401 Unauthorized");
|
header("HTTP/1.1 401 Unauthorized");
|
||||||
$response = "
|
$response = "
|
||||||
<div class='alert alert-danger'>
|
<div class='alert alert-danger'>
|
||||||
Incorrect username or password.
|
Incorrect username or password.
|
||||||
<button class='close' data-dismiss='alert'>×</button>
|
|
||||||
</div>";
|
</div>";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Form state
|
||||||
|
$show_mfa_form = (isset($token_field) && !empty($token_field));
|
||||||
|
$show_login_form = (!$show_role_choice && !$show_mfa_form);
|
||||||
|
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@@ -474,21 +576,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<title><?php echo nullable_htmlentities($company_name); ?> | Login</title>
|
<title><?php echo nullable_htmlentities($company_name); ?> | Login</title>
|
||||||
<!-- Tell the browser to be responsive to screen width -->
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="robots" content="noindex">
|
<meta name="robots" content="noindex">
|
||||||
|
|
||||||
<!-- Font Awesome -->
|
|
||||||
<link rel="stylesheet" href="plugins/fontawesome-free/css/all.min.css">
|
<link rel="stylesheet" href="plugins/fontawesome-free/css/all.min.css">
|
||||||
|
|
||||||
<!-- Favicon -->
|
|
||||||
<?php if(file_exists('uploads/favicon.ico')) { ?>
|
<?php if(file_exists('uploads/favicon.ico')) { ?>
|
||||||
<link rel="icon" type="image/x-icon" href="/uploads/favicon.ico">
|
<link rel="icon" type="image/x-icon" href="/uploads/favicon.ico">
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
|
|
||||||
<!-- Theme style -->
|
|
||||||
<link rel="stylesheet" href="plugins/adminlte/css/adminlte.min.css">
|
<link rel="stylesheet" href="plugins/adminlte/css/adminlte.min.css">
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body class="hold-transition login-page">
|
<body class="hold-transition login-page">
|
||||||
|
|
||||||
@@ -514,12 +611,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_
|
|||||||
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
|
|
||||||
<div class="input-group mb-3" <?php if (isset($token_field) && $token_field) { echo "hidden"; } ?>>
|
<?php if ($show_login_form): ?>
|
||||||
|
<!-- STEP 1: Email + Password -->
|
||||||
|
<div class="input-group mb-3">
|
||||||
<input type="text" class="form-control"
|
<input type="text" class="form-control"
|
||||||
placeholder="<?php if ($config_login_key_required) { if (!isset($_GET['key']) || $_GET['key'] !== $config_login_key_secret) { echo "Client "; } } echo "Email"; ?>"
|
placeholder="<?php if ($config_login_key_required) { if (!isset($_GET['key']) || $_GET['key'] !== $config_login_key_secret) { echo "Client "; } } echo "Email"; ?>"
|
||||||
name="email"
|
name="email"
|
||||||
value="<?php echo htmlspecialchars($email ?? '', ENT_QUOTES); ?>"
|
value="<?php echo htmlspecialchars($email ?? '', ENT_QUOTES); ?>"
|
||||||
required <?php if (!isset($token_field) || !$token_field) { echo "autofocus"; } ?>
|
required autofocus
|
||||||
>
|
>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<div class="input-group-text">
|
<div class="input-group-text">
|
||||||
@@ -528,10 +627,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group mb-3" <?php if (isset($token_field) && $token_field) { echo "hidden"; } ?>>
|
<div class="input-group mb-3">
|
||||||
<input type="password" class="form-control" placeholder="Password" name="password"
|
<input type="password" class="form-control" placeholder="Password" name="password" required>
|
||||||
value="<?php echo isset($token_field) && $token_field ? htmlspecialchars($password ?? '', ENT_QUOTES) : ''; ?>"
|
|
||||||
required>
|
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<div class="input-group-text">
|
<div class="input-group-text">
|
||||||
<span class="fas fa-lock"></span>
|
<span class="fas fa-lock"></span>
|
||||||
@@ -539,32 +636,39 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php
|
<button type="submit" class="btn btn-primary btn-block mb-3" name="login">Sign In</button>
|
||||||
// If agent needs MFA, show 2FA field + remember me
|
<?php endif; ?>
|
||||||
if (isset($token_field) && $token_field) {
|
|
||||||
|
<?php if ($show_role_choice): ?>
|
||||||
|
<!-- STEP 2: Role choice only -->
|
||||||
|
<input type="hidden" name="pending_login_token"
|
||||||
|
value="<?php echo htmlspecialchars($_SESSION['pending_dual_login']['token'] ?? '', ENT_QUOTES); ?>">
|
||||||
|
|
||||||
|
<div class="mb-2 text-center">
|
||||||
|
<button type="submit" class="btn btn-dark btn-block mb-2" name="role_choice" value="agent">
|
||||||
|
Log in as Agent
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-light btn-block" name="role_choice" value="client">
|
||||||
|
Log in as Client
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($show_mfa_form): ?>
|
||||||
|
<!-- STEP 3: MFA only -->
|
||||||
|
<?php echo $token_field; ?>
|
||||||
|
|
||||||
|
<input type="hidden" name="pending_mfa_token"
|
||||||
|
value="<?php echo htmlspecialchars($_SESSION['pending_mfa_login']['token'] ?? '', ENT_QUOTES); ?>">
|
||||||
|
|
||||||
echo $token_field;
|
|
||||||
?>
|
|
||||||
<div class="form-group mb-3">
|
<div class="form-group mb-3">
|
||||||
<div class="custom-control custom-checkbox">
|
<div class="custom-control custom-checkbox">
|
||||||
<input type="checkbox" class="custom-control-input" id="remember_me" name="remember_me">
|
<input type="checkbox" class="custom-control-input" id="remember_me" name="remember_me">
|
||||||
<label class="custom-control-label" for="remember_me">Remember Me</label>
|
<label class="custom-control-label" for="remember_me">Remember Me</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php } ?>
|
|
||||||
|
|
||||||
<?php if ($show_role_choice): ?>
|
<button type="submit" class="btn btn-dark btn-block mb-3" name="mfa_login">Verify & Sign In</button>
|
||||||
<!-- When both agent & client accounts exist with same email/password -->
|
|
||||||
<div class="mb-2 text-center">
|
|
||||||
<button type="submit" class="btn btn-primary btn-block mb-2" name="role_choice" value="agent">
|
|
||||||
Log in as Agent
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="btn btn-success btn-block" name="role_choice" value="client">
|
|
||||||
Log in as Client
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
|
||||||
<button type="submit" class="btn btn-primary btn-block mb-3" name="login">Sign In</button>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
@@ -593,16 +697,9 @@ if (!$config_whitelabel_enabled) {
|
|||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!-- jQuery -->
|
|
||||||
<script src="plugins/jquery/jquery.min.js"></script>
|
<script src="plugins/jquery/jquery.min.js"></script>
|
||||||
|
|
||||||
<!-- Bootstrap 4 -->
|
|
||||||
<script src="plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
|
<script src="plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|
||||||
<!-- AdminLTE App -->
|
|
||||||
<script src="plugins/adminlte/js/adminlte.min.js"></script>
|
<script src="plugins/adminlte/js/adminlte.min.js"></script>
|
||||||
|
|
||||||
<!-- Prevents resubmit on refresh or back -->
|
|
||||||
<script src="js/login_prevent_resubmit.js"></script>
|
<script src="js/login_prevent_resubmit.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user