Implements and troubleshoots new messages system

main
Yasen Pramatarov 2025-01-06 11:13:28 +02:00
parent 4bfae911db
commit b314cdd14d
15 changed files with 343 additions and 233 deletions

View File

@ -7,106 +7,136 @@ class Messages {
const TYPE_INFO = 'info'; const TYPE_INFO = 'info';
const TYPE_WARNING = 'warning'; const TYPE_WARNING = 'warning';
// Message categories // Default message configurations
const SECURITY = [ const NOTICE = [
'WHITELIST_ADD_SUCCESS' => [ 'DEFAULT' => [
'message' => 'IP address successfully added to whitelist.',
'type' => self::TYPE_SUCCESS,
'dismissible' => true
],
'WHITELIST_ADD_ERROR' => [
'message' => 'Failed to add IP to whitelist. Please check the IP format.',
'type' => self::TYPE_ERROR,
'dismissible' => true
],
'WHITELIST_REMOVE_SUCCESS' => [
'message' => 'IP address successfully removed from whitelist.',
'type' => self::TYPE_SUCCESS,
'dismissible' => true
],
'WHITELIST_REMOVE_ERROR' => [
'message' => 'Failed to remove IP from whitelist.',
'type' => self::TYPE_ERROR,
'dismissible' => true
],
'BLACKLIST_ADD_SUCCESS' => [
'message' => 'IP address successfully added to blacklist.',
'type' => self::TYPE_SUCCESS,
'dismissible' => true
],
'BLACKLIST_ADD_ERROR' => [
'message' => 'Failed to add IP to blacklist. Please check the IP format.',
'type' => self::TYPE_ERROR,
'dismissible' => true
],
'BLACKLIST_REMOVE_SUCCESS' => [
'message' => 'IP address successfully removed from blacklist.',
'type' => self::TYPE_SUCCESS,
'dismissible' => true
],
'BLACKLIST_REMOVE_ERROR' => [
'message' => 'Failed to remove IP from blacklist.',
'type' => self::TYPE_ERROR,
'dismissible' => true
],
'IP_REQUIRED' => [
'message' => 'IP address is required.',
'type' => self::TYPE_ERROR,
'dismissible' => true
],
'PERMISSION_DENIED' => [
'message' => 'You do not have permission to perform this action.',
'type' => self::TYPE_ERROR,
'dismissible' => false
],
'RATE_LIMIT_INFO' => [
'message' => 'Rate limiting is active. This helps protect against brute force attacks.',
'type' => self::TYPE_INFO, 'type' => self::TYPE_INFO,
'dismissible' => true
]
];
const ERROR = [
'DEFAULT' => [
'type' => self::TYPE_ERROR,
'dismissible' => false 'dismissible' => false
] ]
]; ];
const LOGIN = [ const LOGIN = [
'LOGIN_FAILED' => [ 'LOGIN_SUCCESS' => [
'message' => 'Invalid username or password.', 'type' => self::TYPE_SUCCESS,
'type' => self::TYPE_ERROR,
'dismissible' => true 'dismissible' => true
], ],
'LOGIN_BLOCKED' => [ 'LOGIN_FAILED' => [
'message' => 'Too many failed attempts. Please try again later.',
'type' => self::TYPE_ERROR, 'type' => self::TYPE_ERROR,
'dismissible' => false 'dismissible' => false
], ],
'LOGOUT_SUCCESS' => [
'type' => self::TYPE_SUCCESS,
'dismissible' => true
],
'IP_BLACKLISTED' => [ 'IP_BLACKLISTED' => [
'message' => 'Access denied. Your IP address is blacklisted.', 'type' => self::TYPE_ERROR,
'dismissible' => false
],
'IP_NOT_WHITELISTED' => [
'type' => self::TYPE_ERROR,
'dismissible' => false
],
'TOO_MANY_ATTEMPTS' => [
'type' => self::TYPE_ERROR, 'type' => self::TYPE_ERROR,
'dismissible' => false 'dismissible' => false
] ]
]; ];
const SECURITY = [
'WHITELIST_ADD_SUCCESS' => [
'type' => self::TYPE_SUCCESS,
'dismissible' => true
],
'WHITELIST_ADD_ERROR' => [
'type' => self::TYPE_ERROR,
'dismissible' => true
],
'WHITELIST_REMOVE_SUCCESS' => [
'type' => self::TYPE_SUCCESS,
'dismissible' => true
],
'WHITELIST_REMOVE_ERROR' => [
'type' => self::TYPE_ERROR,
'dismissible' => true
],
'BLACKLIST_ADD_SUCCESS' => [
'type' => self::TYPE_SUCCESS,
'dismissible' => true
],
'BLACKLIST_ADD_ERROR' => [
'type' => self::TYPE_ERROR,
'dismissible' => true
],
'BLACKLIST_REMOVE_SUCCESS' => [
'type' => self::TYPE_SUCCESS,
'dismissible' => true
],
'BLACKLIST_REMOVE_ERROR' => [
'type' => self::TYPE_ERROR,
'dismissible' => true
],
'RATE_LIMIT_INFO' => [
'type' => self::TYPE_INFO,
'dismissible' => false
],
'PERMISSION_DENIED' => [
'type' => self::TYPE_ERROR,
'dismissible' => false
],
'IP_REQUIRED' => [
'type' => self::TYPE_ERROR,
'dismissible' => false
]
];
private static $strings = null;
/**
* Get message strings
*/
private static function getStrings() {
if (self::$strings === null) {
self::$strings = require __DIR__ . '/../includes/messages-strings.php';
}
return self::$strings;
}
/** /**
* Get message configuration by key * Get message configuration by key
*/ */
public static function get($category, $key) { public static function get($category, $key) {
$messages = constant("self::$category"); $config = constant("self::$category")[$key] ?? null;
return $messages[$key] ?? null; if (!$config) return null;
$strings = self::getStrings();
$message = $strings[$category][$key] ?? '';
return array_merge($config, ['message' => $message]);
} }
/** /**
* Render message HTML * Render message HTML
*/ */
public static function render($category, $key, $customMessage = null) { public static function render($category, $key, $customMessage = null, $dismissible = null) {
$config = self::get($category, $key); $config = self::get($category, $key);
if (!$config) return ''; if (!$config) return '';
$message = $customMessage ?? $config['message']; $message = $customMessage ?? $config['message'];
$dismissible = $config['dismissible'] ? ' alert-dismissible fade show' : ''; $isDismissible = $dismissible ?? $config['dismissible'];
$dismissButton = $config['dismissible'] ? '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>' : ''; $dismissClass = $isDismissible ? ' alert-dismissible fade show' : '';
$dismissButton = $isDismissible ? '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>' : '';
return sprintf( return sprintf(
'<div class="alert alert-%s%s" role="alert">%s%s</div>', '<div class="alert alert-%s%s" role="alert">%s%s</div>',
$config['type'], $config['type'],
$dismissible, $dismissClass,
htmlspecialchars($message), htmlspecialchars($message),
$dismissButton $dismissButton
); );
@ -115,14 +145,15 @@ class Messages {
/** /**
* Store message in session for display after redirect * Store message in session for display after redirect
*/ */
public static function flash($category, $key, $customMessage = null) { public static function flash($category, $key, $customMessage = null, $dismissible = null) {
if (!isset($_SESSION['flash_messages'])) { if (!isset($_SESSION['flash_messages'])) {
$_SESSION['flash_messages'] = []; $_SESSION['flash_messages'] = [];
} }
$_SESSION['flash_messages'][] = [ $_SESSION['flash_messages'][] = [
'category' => $category, 'category' => $category,
'key' => $key, 'key' => $key,
'custom_message' => $customMessage 'custom_message' => $customMessage,
'dismissible' => $dismissible
]; ];
} }

View File

@ -3,8 +3,8 @@
class RateLimiter { class RateLimiter {
public $db; public $db;
private $log; private $log;
public $maxAttempts = 5; // Maximum login attempts public $maxAttempts = 5; // Maximum login attempts
public $decayMinutes = 15; // Time window in minutes public $decayMinutes = 15; // Time window in minutes
public $autoBlacklistThreshold = 10; // Attempts before auto-blacklist public $autoBlacklistThreshold = 10; // Attempts before auto-blacklist
public $autoBlacklistDuration = 24; // Hours to blacklist for public $autoBlacklistDuration = 24; // Hours to blacklist for
public $ratelimitTable = 'login_attempts'; public $ratelimitTable = 'login_attempts';
@ -22,7 +22,7 @@ class RateLimiter {
// Login attempts table // Login attempts table
$sql = "CREATE TABLE IF NOT EXISTS {$this->ratelimitTable} ( $sql = "CREATE TABLE IF NOT EXISTS {$this->ratelimitTable} (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL, ip_address TEXT NOT NULL UNIQUE,
username TEXT NOT NULL, username TEXT NOT NULL,
attempted_at TEXT DEFAULT (DATETIME('now')) attempted_at TEXT DEFAULT (DATETIME('now'))
)"; )";
@ -45,7 +45,7 @@ class RateLimiter {
ip_address TEXT NOT NULL UNIQUE, ip_address TEXT NOT NULL UNIQUE,
is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)), is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)),
reason TEXT, reason TEXT,
expiry_time TEXT NULL, expiry_time TEXT NULL,
created_at TEXT DEFAULT (DATETIME('now')), created_at TEXT DEFAULT (DATETIME('now')),
created_by TEXT created_by TEXT
)"; )";
@ -53,11 +53,11 @@ class RateLimiter {
// Default IPs to whitelist (local interface and private networks IPs) // Default IPs to whitelist (local interface and private networks IPs)
$defaultIps = [ $defaultIps = [
['127.0.0.1', 0, 'localhost IPv4'], ['127.0.0.1', false, 'localhost IPv4'],
['::1', 0, 'localhost IPv6'], ['::1', false, 'localhost IPv6'],
['10.0.0.0/8', 1, 'Private network (Class A)'], ['10.0.0.0/8', true, 'Private network (Class A)'],
['172.16.0.0/12', 1, 'Private network (Class B)'], ['172.16.0.0/12', true, 'Private network (Class B)'],
['192.168.0.0/16', 1, 'Private network (Class C)'] ['192.168.0.0/16', true, 'Private network (Class C)']
]; ];
// Insert default whitelisted IPs if they don't exist // Insert default whitelisted IPs if they don't exist
@ -70,11 +70,11 @@ class RateLimiter {
// Insert known malicious networks // Insert known malicious networks
$defaultBlacklist = [ $defaultBlacklist = [
['0.0.0.0/8', 1, 'Reserved address space - RFC 1122'], ['0.0.0.0/8', true, 'Reserved address space - RFC 1122'],
['100.64.0.0/10', 1, 'Carrier-grade NAT space - RFC 6598'], ['100.64.0.0/10', true, 'Carrier-grade NAT space - RFC 6598'],
['192.0.2.0/24', 1, 'TEST-NET-1 Documentation space - RFC 5737'], ['192.0.2.0/24', true, 'TEST-NET-1 Documentation space - RFC 5737'],
['198.51.100.0/24', 1, 'TEST-NET-2 Documentation space - RFC 5737'], ['198.51.100.0/24', true, 'TEST-NET-2 Documentation space - RFC 5737'],
['203.0.113.0/24', 1, 'TEST-NET-3 Documentation space - RFC 5737'] ['203.0.113.0/24', true, 'TEST-NET-3 Documentation space - RFC 5737']
]; ];
$stmt = $this->db->prepare("INSERT OR IGNORE INTO {$this->blacklistTable} $stmt = $this->db->prepare("INSERT OR IGNORE INTO {$this->blacklistTable}
@ -87,27 +87,21 @@ class RateLimiter {
} }
private function isIpWhitelisted($ip) { /**
// Check exact IP match and CIDR ranges * Get number of recent login attempts for an IP
$stmt = $this->db->prepare("SELECT ip_address, is_network FROM {$this->whitelistTable}"); */
$stmt->execute(); public function getRecentAttempts($ip) {
$stmt = $this->db->prepare("SELECT COUNT(*) as attempts FROM {$this->ratelimitTable}
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { WHERE ip_address = ? AND attempted_at > datetime('now', '-' || :minutes || ' minutes')");
if ($row['is_network']) { $stmt->execute([$ip, $this->decayMinutes]);
if ($this->ipInRange($ip, $row['ip_address'])) { $result = $stmt->fetch(PDO::FETCH_ASSOC);
return true; return intval($result['attempts']);
}
} else {
if ($ip === $row['ip_address']) {
return true;
}
}
}
return false;
} }
private function isIpBlacklisted($ip) { /**
* Check if an IP is blacklisted
*/
public function isIpBlacklisted($ip) {
// First check if IP is explicitly blacklisted or in a blacklisted range // First check if IP is explicitly blacklisted or in a blacklisted range
$stmt = $this->db->prepare("SELECT ip_address, is_network, expiry_time FROM {$this->blacklistTable}"); $stmt = $this->db->prepare("SELECT ip_address, is_network, expiry_time FROM {$this->blacklistTable}");
$stmt->execute(); $stmt->execute();
@ -132,6 +126,29 @@ class RateLimiter {
return false; return false;
} }
/**
* Check if an IP is whitelisted
*/
public function isIpWhitelisted($ip) {
// Check exact IP match and CIDR ranges
$stmt = $this->db->prepare("SELECT ip_address, is_network FROM {$this->whitelistTable}");
$stmt->execute();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
if ($row['is_network']) {
if ($this->ipInRange($ip, $row['ip_address'])) {
return true;
}
} else {
if ($ip === $row['ip_address']) {
return true;
}
}
}
return false;
}
private function ipInRange($ip, $cidr) { private function ipInRange($ip, $cidr) {
list($subnet, $bits) = explode('/', $cidr); list($subnet, $bits) = explode('/', $cidr);
@ -151,6 +168,7 @@ class RateLimiter {
$message = "Cannot whitelist {$ip} - IP is currently blacklisted"; $message = "Cannot whitelist {$ip} - IP is currently blacklisted";
if ($userId) { if ($userId) {
$this->log->insertLog($userId, "IP Whitelist: {$message}", 'system'); $this->log->insertLog($userId, "IP Whitelist: {$message}", 'system');
Messages::flash('ERROR', 'DEFAULT', $message);
} }
return false; return false;
} }
@ -177,6 +195,7 @@ class RateLimiter {
} catch (Exception $e) { } catch (Exception $e) {
if ($userId) { if ($userId) {
$this->log->insertLog($userId, "IP Whitelist: Failed to add {$ip}: " . $e->getMessage(), 'system'); $this->log->insertLog($userId, "IP Whitelist: Failed to add {$ip}: " . $e->getMessage(), 'system');
Messages::flash('ERROR', 'DEFAULT', "IP Whitelist: Failed to add {$ip}: " . $e->getMessage());
} }
return false; return false;
} }
@ -211,6 +230,7 @@ class RateLimiter {
} catch (Exception $e) { } catch (Exception $e) {
if ($userId) { if ($userId) {
$this->log->insertLog($userId, "IP Whitelist: Failed to remove {$ip}: " . $e->getMessage(), 'system'); $this->log->insertLog($userId, "IP Whitelist: Failed to remove {$ip}: " . $e->getMessage(), 'system');
Messages::flash('ERROR', 'DEFAULT', "IP Whitelist: Failed to remove {$ip}: " . $e->getMessage());
} }
return false; return false;
} }
@ -223,6 +243,7 @@ class RateLimiter {
$message = "Cannot blacklist {$ip} - IP is currently whitelisted"; $message = "Cannot blacklist {$ip} - IP is currently whitelisted";
if ($userId) { if ($userId) {
$this->log->insertLog($userId, "IP Blacklist: {$message}", 'system'); $this->log->insertLog($userId, "IP Blacklist: {$message}", 'system');
Messages::flash('ERROR', 'DEFAULT', $message);
} }
return false; return false;
} }
@ -251,6 +272,7 @@ class RateLimiter {
} catch (Exception $e) { } catch (Exception $e) {
if ($userId) { if ($userId) {
$this->log->insertLog($userId, "IP Blacklist: Failed to add {$ip}: " . $e->getMessage(), 'system'); $this->log->insertLog($userId, "IP Blacklist: Failed to add {$ip}: " . $e->getMessage(), 'system');
Messages::flash('ERROR', 'DEFAULT', "IP Blacklist: Failed to add {$ip}: " . $e->getMessage());
} }
return false; return false;
} }
@ -283,6 +305,7 @@ class RateLimiter {
} catch (Exception $e) { } catch (Exception $e) {
if ($userId) { if ($userId) {
$this->log->insertLog($userId, "IP Blacklist: Failed to remove {$ip}: " . $e->getMessage(), 'system'); $this->log->insertLog($userId, "IP Blacklist: Failed to remove {$ip}: " . $e->getMessage(), 'system');
Messages::flash('ERROR', 'DEFAULT', "IP Blacklist: Failed to remove {$ip}: " . $e->getMessage());
} }
return false; return false;
} }
@ -317,6 +340,7 @@ class RateLimiter {
return true; return true;
} catch (Exception $e) { } catch (Exception $e) {
$this->log->insertLog(0, "Failed to cleanup expired entries: " . $e->getMessage(), 'system'); $this->log->insertLog(0, "Failed to cleanup expired entries: " . $e->getMessage(), 'system');
Messages::flash('ERROR', 'DEFAULT', "Failed to cleanup expired entries: " . $e->getMessage());
return false; return false;
} }
} }
@ -390,9 +414,9 @@ class RateLimiter {
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute([ $stmt->execute([
':ip' => $ipAddress, ':ip' => $ipAddress,
':username' => $username, ':username' => $username,
':minutes' => $this->decayMinutes ':minutes' => $this->decayMinutes
]); ]);
$result = $stmt->fetch(PDO::FETCH_ASSOC); $result = $stmt->fetch(PDO::FETCH_ASSOC);
@ -405,7 +429,7 @@ class RateLimiter {
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute([ $stmt->execute([
':minutes' => $this->decayMinutes ':minutes' => $this->decayMinutes
]); ]);
} }
@ -418,9 +442,9 @@ class RateLimiter {
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute([ $stmt->execute([
':ip' => $ipAddress, ':ip' => $ipAddress,
':username' => $username, ':username' => $username,
':minutes' => $this->decayMinutes ':minutes' => $this->decayMinutes
]); ]);
$result = $stmt->fetch(PDO::FETCH_ASSOC); $result = $stmt->fetch(PDO::FETCH_ASSOC);

View File

@ -0,0 +1,9 @@
<?php
if (isset($messages) && is_array($messages)) {
foreach ($messages as $msg) {
echo Messages::render($msg['category'], $msg['key'], $msg['custom_message'] ?? null);
}
}
?>

View File

@ -0,0 +1,36 @@
<?php
// Message strings for translation
return [
'LOGIN' => [
'LOGIN_SUCCESS' => 'Login successful.',
'LOGIN_FAILED' => 'Login failed. Please check your credentials.',
'LOGOUT_SUCCESS' => 'Logout successful. You can log in again.',
'IP_BLACKLISTED' => 'Access denied. Your IP address is blacklisted.',
'IP_NOT_WHITELISTED' => 'Access denied. Your IP address is not whitelisted.',
'TOO_MANY_ATTEMPTS' => 'Too many login attempts. Please try again later.',
],
'SECURITY' => [
'WHITELIST_ADD_SUCCESS' => 'IP address successfully added to whitelist.',
'WHITELIST_ADD_ERROR' => 'Failed to add IP to whitelist. Please check the IP format.',
'WHITELIST_REMOVE_SUCCESS' => 'IP address successfully removed from whitelist.',
'WHITELIST_REMOVE_ERROR' => 'Failed to remove IP from whitelist.',
'BLACKLIST_ADD_SUCCESS' => 'IP address successfully added to blacklist.',
'BLACKLIST_ADD_ERROR' => 'Failed to add IP to blacklist. Please check the IP format.',
'BLACKLIST_REMOVE_SUCCESS' => 'IP address successfully removed from blacklist.',
'BLACKLIST_REMOVE_ERROR' => 'Failed to remove IP from blacklist.',
'RATE_LIMIT_INFO' => 'Rate limiting is active. This helps protect against brute force attacks.',
'PERMISSION_DENIED' => 'Permission denied. You do not have the required rights.',
'IP_REQUIRED' => 'IP address is required.',
],
'REGISTER' => [
'SUCCESS' => 'Registration successful. You can log in now.',
'FAILED' => 'Registration failed: %s',
'DISABLED' => 'Registration is disabled.',
],
'SYSTEM' => [
'DB_ERROR' => 'Error connecting to the database: %s',
'DB_CONNECT_ERROR' => 'Error connecting to DB: %s',
'DB_UNKNOWN_TYPE' => 'Error: unknown database type "%s"',
],
];

View File

@ -1,17 +1,15 @@
<?php <?php
// Start session if not already started
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Include Messages class // Get any flash messages from previous request
require_once __DIR__ . '/../classes/messages.php';
// Initialize messages array
$messages = [];
// Get any flash messages from previous requests
$flash_messages = Messages::getFlash(); $flash_messages = Messages::getFlash();
if (!empty($flash_messages)) { if (!empty($flash_messages)) {
$messages = array_merge($messages, $flash_messages); $messages = array_merge($messages, array_map(function($flash) {
return [
'category' => $flash['category'],
'key' => $flash['key'],
'custom_message' => $flash['custom_message']
];
}, $flash_messages));
} }
?>

View File

@ -21,6 +21,15 @@ if (isset($_REQUEST['until_time'])) {
$until_time = htmlspecialchars($_REQUEST['until_time']); $until_time = htmlspecialchars($_REQUEST['until_time']);
} }
// sanitize session vars
if (isset($_SESSION)) {
foreach ($_SESSION as $key => $value) {
if (is_string($value)) {
$_SESSION[$key] = htmlspecialchars($value);
}
}
}
// hosts // hosts
if (isset($_POST['address'])) { if (isset($_POST['address'])) {
$address = htmlspecialchars($_POST['address']); $address = htmlspecialchars($_POST['address']);

View File

@ -9,6 +9,10 @@
* 3. The most recent 10 conferences. * 3. The most recent 10 conferences.
*/ */
// Get any new messages
include '../app/includes/messages.php';
include '../app/includes/messages-show.php';
require '../app/classes/conference.php'; require '../app/classes/conference.php';
require '../app/classes/participant.php'; require '../app/classes/participant.php';

View File

@ -21,11 +21,28 @@ try {
// connect to database // connect to database
$dbWeb = connectDB($config); $dbWeb = connectDB($config);
// Initialize RateLimiter
require_once '../app/classes/ratelimiter.php';
$rateLimiter = new RateLimiter($dbWeb);
if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) { if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {
try { try {
$username = $_POST['username']; $username = $_POST['username'];
$password = $_POST['password']; $password = $_POST['password'];
// Check if IP is blacklisted
if ($rateLimiter->isIpBlacklisted($user_IP)) {
throw new Exception(Messages::get('LOGIN', 'IP_BLACKLISTED')['message']);
}
// Check rate limiting (but skip if IP is whitelisted)
if (!$rateLimiter->isIpWhitelisted($user_IP)) {
$attempts = $rateLimiter->getRecentAttempts($user_IP);
if ($attempts >= $rateLimiter->maxAttempts) {
throw new Exception(Messages::get('LOGIN', 'LOGIN_BLOCKED')['message']);
}
}
// login successful // login successful
if ( $userObject->login($username, $password) ) { if ( $userObject->login($username, $password) ) {
// if remember_me is checked, max out the session // if remember_me is checked, max out the session
@ -52,32 +69,40 @@ try {
'samesite' => 'Strict' 'samesite' => 'Strict'
]); ]);
// redirect to index // Log successful login
$_SESSION['notice'] = "Login successful";
$user_id = $userObject->getUserId($username)[0]['id']; $user_id = $userObject->getUserId($username)[0]['id'];
$logObject->insertLog($user_id, "Login: User \"$username\" logged in. IP: $user_IP", 'user'); $logObject->insertLog($user_id, "Login: User \"$username\" logged in. IP: $user_IP", 'user');
// Set success message and redirect
Messages::flash('LOGIN', 'LOGIN_SUCCESS');
header('Location: ' . htmlspecialchars($app_root)); header('Location: ' . htmlspecialchars($app_root));
exit(); exit();
} else {
throw new Exception(Messages::get('LOGIN', 'LOGIN_FAILED')['message']);
} }
} catch (Exception $e) { } catch (Exception $e) {
// Log the failed attempt // Log the failed attempt
$error = $e->getMessage(); Messages::flash('ERROR', 'DEFAULT', $e->getMessage());
if (isset($username)) { if (isset($username)) {
$user_id = $userObject->getUserId($username)[0]['id'] ?? 0; $user_id = $userObject->getUserId($username)[0]['id'] ?? 0;
$logObject->insertLog($user_id, "Login: Failed login attempt for user \"$username\". IP: $user_IP. Reason: {$error}", 'user'); $logObject->insertLog($user_id, "Login: Failed login attempt for user \"$username\". IP: $user_IP. Reason: {$e->getMessage()}", 'user');
} }
include '../app/templates/block-message.php';
} }
} }
} catch (Exception $e) { } catch (Exception $e) {
$error = getError('There was an unexpected error. Please try again.', $e->getMessage()); Messages::flash('ERROR', 'DEFAULT', 'There was an unexpected error. Please try again.');
} }
// Show configured login message if any
if (!empty($config['login_message'])) { if (!empty($config['login_message'])) {
$notice = $config['login_message']; echo Messages::render('NOTICE', 'DEFAULT', $config['login_message'], false);
include '../app/templates/block-message.php';
} }
// Get any new messages
include '../app/includes/messages.php';
include '../app/includes/messages-show.php';
// Load the template
include '../app/templates/form-login.php'; include '../app/templates/form-login.php';
?> ?>

View File

@ -8,12 +8,9 @@
* and redirects to the login page on success or displays an error message on failure. * and redirects to the login page on success or displays an error message on failure.
*/ */
// check if the registration is allowed // registration is allowed, go on
if ($config['registration_enabled'] === true) { if ($config['registration_enabled'] === true) {
// clear any previous error messages
unset($error);
try { try {
// connect to database // connect to database
@ -28,27 +25,30 @@ if ($config['registration_enabled'] === true) {
// redirect to login // redirect to login
if ($result === true) { if ($result === true) {
$_SESSION['notice'] = "Registration successful.<br />You can log in now."; Messages::flash('NOTICE', 'DEFAULT', "Registration successful.<br />You can log in now.");
header('Location: ' . htmlspecialchars($app_root)); header('Location: ' . htmlspecialchars($app_root));
exit(); exit();
// registration fail, redirect to login // registration fail, redirect to login
} else { } else {
$_SESSION['error'] = "Registration failed. $result"; Messages::flash('ERROR', 'DEFAULT', "Registration failed. $result");
header('Location: ' . htmlspecialchars($app_root)); header('Location: ' . htmlspecialchars($app_root));
exit(); exit();
} }
} }
} catch (Exception $e) { } catch (Exception $e) {
$error = $e->getMessage(); Messages::flash('ERROR', 'DEFAULT', $e->getMessage());
} }
include '../app/templates/block-message.php'; // Get any new messages
include '../app/includes/messages.php';
include '../app/includes/messages-show.php';
// Load the template
include '../app/templates/form-register.php'; include '../app/templates/form-register.php';
// registration disabled // registration disabled
} else { } else {
$notice = 'Registration is disabled'; echo Messages::render('NOTICE', 'DEFAULT', 'Registration is disabled', false);
include '../app/templates/block-message.php';
} }
?> ?>

View File

@ -9,15 +9,18 @@ if (!($userObject->hasRight($user_id, 'superuser') ||
exit; exit;
} }
// Include Messages class if (!isset($currentUser)) {
require_once '../app/classes/messages.php'; include '../app/templates/error-unauthorized.php';
exit;
// Initialize variables for feedback messages }
$messages = [];
// Get current section // Get current section
$section = isset($_POST['section']) ? $_POST['section'] : (isset($_GET['section']) ? $_GET['section'] : 'whitelist'); $section = isset($_POST['section']) ? $_POST['section'] : (isset($_GET['section']) ? $_GET['section'] : 'whitelist');
// Initialize RateLimiter
require_once '../app/classes/ratelimiter.php';
$rateLimiter = new RateLimiter($dbWeb);
// Handle form submissions // Handle form submissions
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) { if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action']; $action = $_POST['action'];
@ -81,6 +84,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
} }
} catch (Exception $e) { } catch (Exception $e) {
$messages[] = ['category' => 'SECURITY', 'key' => 'CUSTOM_ERROR', 'custom_message' => $e->getMessage()]; $messages[] = ['category' => 'SECURITY', 'key' => 'CUSTOM_ERROR', 'custom_message' => $e->getMessage()];
Messages::flash('SECURITY', 'CUSTOM_ERROR', 'custom_message');
} }
if (empty($messages)) { if (empty($messages)) {
@ -90,16 +94,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
} }
} }
// Get flash messages from previous request
$flash_messages = Messages::getFlash();
$messages = array_merge($messages, array_map(function($flash) {
return [
'category' => $flash['category'],
'key' => $flash['key'],
'custom_message' => $flash['custom_message']
];
}, $flash_messages));
// Always show rate limit info message for rate limiting section // Always show rate limit info message for rate limiting section
if ($section === 'ratelimit') { if ($section === 'ratelimit') {
$messages[] = ['category' => 'SECURITY', 'key' => 'RATE_LIMIT_INFO']; $messages[] = ['category' => 'SECURITY', 'key' => 'RATE_LIMIT_INFO'];
@ -109,7 +103,11 @@ if ($section === 'ratelimit') {
$whitelisted = $rateLimiter->getWhitelistedIps(); $whitelisted = $rateLimiter->getWhitelistedIps();
$blacklisted = $rateLimiter->getBlacklistedIps(); $blacklisted = $rateLimiter->getBlacklistedIps();
// Include template // Get any new messages
include '../app/includes/messages.php';
include '../app/includes/messages-show.php';
// Load the template
include '../app/templates/security.php'; include '../app/templates/security.php';
?> ?>

View File

@ -1,18 +0,0 @@
<?php
// Display and clean up session messages
foreach (['error', 'notice'] as $type) {
if (isset($_SESSION[$type])) {
renderMessage($_SESSION[$type], $type, true);
}
}
// Display standalone messages
if (isset($error)) {
renderMessage($error, 'error', true);
}
if (isset($notice)) {
renderMessage($notice, 'notice', true);
}
?>

View File

@ -18,6 +18,28 @@ $(document).ready(function(){
$('[data-toggle="tooltip"]').tooltip(); $('[data-toggle="tooltip"]').tooltip();
}); });
</script> </script>
<script>
// dismissible messages
document.addEventListener('DOMContentLoaded', function() {
// Initialize Bootstrap alerts
var alerts = document.querySelectorAll('.alert');
alerts.forEach(function(alert) {
var closeButton = alert.querySelector('.btn-close');
if (closeButton) {
closeButton.addEventListener('click', function() {
alert.classList.remove('show');
setTimeout(function() {
alert.remove();
}, 150);
});
}
});
});
</script>
</body> </body>
</html> </html>

View File

@ -37,3 +37,13 @@
</head> </head>
<body> <body>
<div class="container-fluid">
<div class="row">
<div class="col">
<?php if (isset($messages) && is_array($messages)): ?>
<?php foreach ($messages as $msg): ?>
<?= Messages::render($msg['category'], $msg['key'], $msg['custom_message'] ?? null) ?>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>

View File

@ -3,9 +3,6 @@
<div class="row mb-4"> <div class="row mb-4">
<div class="col"> <div class="col">
<h2>Security Settings</h2> <h2>Security Settings</h2>
<?php foreach ($messages as $msg): ?>
<?= Messages::render($msg['category'], $msg['key'], $msg['custom_message'] ?? null) ?>
<?php endforeach; ?>
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
<?php if ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit whitelist')) { ?> <?php if ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit whitelist')) { ?>
<li class="nav-item"> <li class="nav-item">
@ -172,7 +169,7 @@
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h3>Rate Limiting Settings</h3> <h3>Rate Limiting Settings</h3>
Restricts brute force or flooding attempts at login page. Rate limiting settings control how many failed login attempts are allowed before blocking an IP address.
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="alert alert-info"> <div class="alert alert-info">
@ -224,21 +221,3 @@
<?php } ?> <?php } ?>
</div> </div>
<!-- /Security Settings --> <!-- /Security Settings -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize Bootstrap alerts
var alerts = document.querySelectorAll('.alert');
alerts.forEach(function(alert) {
var closeButton = alert.querySelector('.btn-close');
if (closeButton) {
closeButton.addEventListener('click', function() {
alert.classList.remove('show');
setTimeout(function() {
alert.remove();
}, 150);
});
}
});
});
</script>

View File

@ -18,14 +18,14 @@ ob_start();
// sanitize all input vars that may end up in URLs or forms // sanitize all input vars that may end up in URLs or forms
require '../app/includes/sanitize.php'; require '../app/includes/sanitize.php';
require '../app/includes/errors.php'; // Initialize message system
// Include Messages class
require_once '../app/classes/messages.php'; require_once '../app/classes/messages.php';
// Initialize variables for feedback messages
$messages = []; $messages = [];
include '../app/includes/messages.php';
require '../app/includes/errors.php';
// error reporting, comment out in production // error reporting, comment out in production
ini_set('display_errors', 1); ini_set('display_errors', 1);
ini_set('display_startup_errors', 1); ini_set('display_startup_errors', 1);
@ -104,12 +104,19 @@ if ( !isset($_COOKIE['username']) && ($page !== 'login' && $page !== 'register')
// connect to db of Jilo Web // connect to db of Jilo Web
require '../app/classes/database.php'; require '../app/classes/database.php';
require '../app/includes/database.php'; require '../app/includes/database.php';
$response = connectDB($config); try {
if ($response['db'] === null) { $response = connectDB($config);
$error .= $response['error']; if (!$response['db']) {
// include '../app/templates/block-message.php'; throw new Exception('Could not connect to database: ' . $response['error']);
} else { }
$dbWeb = $response['db']; $dbWeb = $response['db'];
} catch (Exception $e) {
Messages::flash('ERROR', 'DEFAULT', getError('Error connecting to the database.', $e->getMessage()));
include '../app/templates/page-header.php';
include '../app/includes/messages.php';
include '../app/includes/messages-show.php';
include '../app/templates/page-footer.php';
exit();
} }
// start logging // start logging
@ -146,38 +153,17 @@ if ($page == 'logout') {
session_destroy(); session_destroy();
setcookie('username', "", time() - 100, $config['folder'], $config['domain'], isset($_SERVER['HTTPS']), true); setcookie('username', "", time() - 100, $config['folder'], $config['domain'], isset($_SERVER['HTTPS']), true);
$notice = "You were logged out.<br />You can log in again."; // Log successful logout
$user_id = $userObject->getUserId($currentUser)[0]['id']; $user_id = $userObject->getUserId($currentUser)[0]['id'];
$logObject->insertLog($user_id, "Logout: User \"$currentUser\" logged out. IP: $user_IP", 'user'); $logObject->insertLog($user_id, "Logout: User \"$currentUser\" logged out. IP: $user_IP", 'user');
// Set success message
Messages::flash('LOGIN', 'LOGOUT_SUCCESS');
include '../app/templates/page-header.php'; include '../app/templates/page-header.php';
include '../app/templates/page-menu.php'; include '../app/templates/page-menu.php';
include '../app/templates/block-message.php';
include '../app/pages/login.php'; include '../app/pages/login.php';
} elseif ($page === 'security') {
// Security settings require login
if (!isset($currentUser)) {
include '../app/templates/error-unauthorized.php';
exit;
}
// Get user details and rights
$user_id = $userObject->getUserId($currentUser)[0]['id'];
$userDetails = $userObject->getUserDetails($user_id);
$userRights = $userObject->getUserRights($user_id);
$userTimezone = isset($userDetails[0]['timezone']) ? $userDetails[0]['timezone'] : 'UTC';
// Initialize RateLimiter
require_once '../app/classes/ratelimiter.php';
$rateLimiter = new RateLimiter($dbWeb);
include '../app/templates/page-header.php';
include '../app/templates/page-menu.php';
include '../app/templates/page-sidebar.php';
include '../app/pages/security.php';
include '../app/templates/page-footer.php';
} else { } else {
// if user is logged in, we need user details and rights // if user is logged in, we need user details and rights
@ -202,9 +188,7 @@ if ($page == 'logout') {
$server_endpoint = '/health'; $server_endpoint = '/health';
$server_status = $serverObject->getServerStatus($server_host, $server_port, $server_endpoint); $server_status = $serverObject->getServerStatus($server_host, $server_port, $server_endpoint);
if (!$server_status) { if (!$server_status) {
$error = 'The Jilo Server is not running. Some data may be old and incorrect.'; echo Messages::render('ERROR', 'DEFAULT', 'The Jilo Server is not running. Some data may be old and incorrect.', false);
Messages::get('SECURITY', 'RATE_LIMIT_INFO');
Messages::render('SECURITY', 'RATE_LIMIT_INFO');
} }
} }
@ -214,7 +198,6 @@ if ($page == 'logout') {
if (isset($currentUser)) { if (isset($currentUser)) {
include '../app/templates/page-sidebar.php'; include '../app/templates/page-sidebar.php';
} }
include '../app/templates/block-message.php';
if (in_array($page, $allowed_urls)) { if (in_array($page, $allowed_urls)) {
// all normal pages // all normal pages
include "../app/pages/{$page}.php"; include "../app/pages/{$page}.php";