Compare commits

..

36 Commits
v0.4 ... HEAD

Author SHA1 Message Date
Yasen Pramatarov 6542df9074 Fixes the tests 2025-04-17 10:59:40 +03:00
Yasen Pramatarov 40c646291e Removes old registration core code 2025-04-17 10:45:29 +03:00
Yasen Pramatarov 4877354e8d Fixes register plugin 2025-04-17 10:41:40 +03:00
Yasen Pramatarov 61d23cd8c2 Lets plugins add themselves to the public pages 2025-04-17 10:36:45 +03:00
Yasen Pramatarov 8dfd54eb9f Replaces hardcoded register link with a plugin hook 2025-04-17 10:31:35 +03:00
Yasen Pramatarov af8d86321f Removes hardcoded "register" page 2025-04-17 10:30:34 +03:00
Yasen Pramatarov 26817c1bb6 Adds registration plugin 2025-04-17 10:29:31 +03:00
Yasen Pramatarov 6443eb9b00 Makes plugin system plugin-name agnostic 2025-04-17 10:20:37 +03:00
Yasen Pramatarov 14eefb99e9 Adds "manage plugins" right 2025-04-17 09:46:19 +03:00
Yasen Pramatarov 3915ca6633 Prepares for plugins. Autodiscovery and hooks. 2025-04-16 20:23:27 +03:00
Yasen Pramatarov 5246c47ee6 Makes csrf_token a global constant and moves it to includes 2025-04-16 13:11:51 +03:00
Yasen Pramatarov 221a6e8139 Removes system settings entries from sidebar menu 2025-04-15 22:43:23 +03:00
Yasen Pramatarov b098096930 Uses $userId instead of session var 2025-04-15 22:40:29 +03:00
Yasen Pramatarov 47779baa5e Adds top right system menu 2025-04-15 22:37:49 +03:00
Yasen Pramatarov eebdbc409c Adds top right help menu 2025-04-15 22:29:55 +03:00
Yasen Pramatarov 95530ed5f0 Adds CSRF to profile edit pages 2025-04-15 18:10:17 +03:00
Yasen Pramatarov 0a7f3737c5 Explicitly adds/removes rights, makes possible to remove all rights 2025-04-15 18:05:09 +03:00
Yasen Pramatarov 9cb7812144 Bugfixes 2025-04-15 17:57:13 +03:00
Yasen Pramatarov 4625321079 Removes length check for old password 2025-04-14 19:39:51 +03:00
Yasen Pramatarov 1c2c1a76fa Fixes bugs in login ratelimiting 2025-04-14 19:36:07 +03:00
Yasen Pramatarov 8d64bf7c6e Ratelimits only failed login attempts 2025-04-14 19:12:26 +03:00
Yasen Pramatarov 45181c11c5 Fixes db connection issues 2025-04-14 18:07:15 +03:00
Yasen Pramatarov e96480807c Updates SQL schemas 2025-04-14 18:06:44 +03:00
Yasen Pramatarov 9e94639657 Makes password at least 8 chars 2025-04-14 15:48:54 +03:00
Yasen Pramatarov 649a94c560 Fixes to show session expiration only once 2025-04-14 15:31:19 +03:00
Yasen Pramatarov 8655258ac3 Standartizes $userId as user ID variable in whole app 2025-04-14 10:39:58 +03:00
Yasen Pramatarov 67ba6b38c7 Session expiration bug fix 2025-04-14 10:06:13 +03:00
Yasen Pramatarov 16854f0f77 Fixes tests and adds Session unit test 2025-04-13 20:51:52 +03:00
Yasen Pramatarov 582b5492fe Removes unneded login reirects 2025-04-13 20:05:10 +03:00
Yasen Pramatarov 101f4c539a Validates pagination vars 2025-04-13 19:49:47 +03:00
Yasen Pramatarov 522cded113 Implements the session class 2025-04-13 19:46:48 +03:00
Yasen Pramatarov f77e15bf44 Implements the new session class 2025-04-13 19:34:13 +03:00
Yasen Pramatarov dbdbe1bf49 Switches to session class in templates 2025-04-13 19:12:28 +03:00
Yasen Pramatarov d3f0c90272 Removes code duplicating with session class 2025-04-13 19:11:52 +03:00
Yasen Pramatarov 566b16190e Adds session timeout message 2025-04-13 19:06:48 +03:00
Yasen Pramatarov 5281102e36 Adds a special 'session' class for all session things. 2025-04-13 15:18:53 +03:00
44 changed files with 1157 additions and 614 deletions

View File

@ -16,7 +16,7 @@ class Config {
* @return array Returns an array with 'success' and 'updated' keys on success, or 'success' and 'error' keys on failure. * @return array Returns an array with 'success' and 'updated' keys on success, or 'success' and 'error' keys on failure.
*/ */
public function editConfigFile($updatedConfig, $config_file) { public function editConfigFile($updatedConfig, $config_file) {
global $logObject, $user_id; global $logObject, $userId;
$allLogs = []; $allLogs = [];
$updated = []; $updated = [];
@ -140,7 +140,7 @@ class Config {
} }
if (!empty($allLogs)) { if (!empty($allLogs)) {
$logObject->insertLog($user_id, implode("\n", $allLogs), 'system'); $logObject->insertLog($userId, implode("\n", $allLogs), 'system');
} }
return [ return [
@ -148,7 +148,7 @@ class Config {
'updated' => $updated 'updated' => $updated
]; ];
} catch (Exception $e) { } catch (Exception $e) {
$logObject->insertLog($user_id, "Config update error: " . $e->getMessage(), 'system'); $logObject->insertLog($userId, "Config update error: " . $e->getMessage(), 'system');
return [ return [
'success' => false, 'success' => false,
'error' => $e->getMessage() 'error' => $e->getMessage()

View File

@ -35,6 +35,10 @@ class Feedback {
'type' => self::TYPE_SUCCESS, 'type' => self::TYPE_SUCCESS,
'dismissible' => true 'dismissible' => true
], ],
'SESSION_TIMEOUT' => [
'type' => self::TYPE_ERROR,
'dismissible' => true
],
'IP_BLACKLISTED' => [ 'IP_BLACKLISTED' => [
'type' => self::TYPE_ERROR, 'type' => self::TYPE_ERROR,
'dismissible' => false 'dismissible' => false

View File

@ -18,19 +18,23 @@ class Log {
* @param object $database The database object to initialize the connection. * @param object $database The database object to initialize the connection.
*/ */
public function __construct($database) { public function __construct($database) {
$this->db = $database->getConnection(); if ($database instanceof PDO) {
$this->db = $database;
} else {
$this->db = $database->getConnection();
}
} }
/** /**
* Insert a log event into the database. * Insert a log event into the database.
* *
* @param int $user_id The ID of the user associated with the log event. * @param int $userId The ID of the user associated with the log event.
* @param string $message The log message to insert. * @param string $message The log message to insert.
* @param string $scope The scope of the log event (e.g., 'user', 'system'). Default is 'user'. * @param string $scope The scope of the log event (e.g., 'user', 'system'). Default is 'user'.
* *
* @return bool|string True on success, or an error message on failure. * @return bool|string True on success, or an error message on failure.
*/ */
public function insertLog($user_id, $message, $scope='user') { public function insertLog($userId, $message, $scope='user') {
try { try {
$sql = 'INSERT INTO logs $sql = 'INSERT INTO logs
(user_id, scope, message) (user_id, scope, message)
@ -39,7 +43,7 @@ class Log {
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->execute([ $query->execute([
':user_id' => $user_id, ':user_id' => $userId,
':scope' => $scope, ':scope' => $scope,
':message' => $message, ':message' => $message,
]); ]);
@ -54,7 +58,7 @@ class Log {
/** /**
* Retrieve log entries from the database. * Retrieve log entries from the database.
* *
* @param int $user_id The ID of the user whose logs are being retrieved. * @param int $userId The ID of the user whose logs are being retrieved.
* @param string $scope The scope of the logs ('user' or 'system'). * @param string $scope The scope of the logs ('user' or 'system').
* @param int $offset The offset for pagination. Default is 0. * @param int $offset The offset for pagination. Default is 0.
* @param int $items_per_page The number of log entries to retrieve per page. Default is no limit. * @param int $items_per_page The number of log entries to retrieve per page. Default is no limit.
@ -62,7 +66,7 @@ class Log {
* *
* @return array An array of log entries. * @return array An array of log entries.
*/ */
public function readLog($user_id, $scope, $offset=0, $items_per_page='', $filters=[]) { public function readLog($userId, $scope, $offset=0, $items_per_page='', $filters=[]) {
$params = []; $params = [];
$where_clauses = []; $where_clauses = [];
@ -74,7 +78,7 @@ class Log {
// Add scope condition // Add scope condition
if ($scope === 'user') { if ($scope === 'user') {
$where_clauses[] = 'l.user_id = :user_id'; $where_clauses[] = 'l.user_id = :user_id';
$params[':user_id'] = $user_id; $params[':user_id'] = $userId;
} }
// Add time range filters if specified // Add time range filters if specified

View File

@ -23,7 +23,11 @@ class RateLimiter {
]; ];
public function __construct($database) { public function __construct($database) {
$this->db = $database->getConnection(); if ($database instanceof PDO) {
$this->db = $database;
} else {
$this->db = $database->getConnection();
}
$this->log = new Log($database); $this->log = new Log($database);
$this->createTablesIfNotExist(); $this->createTablesIfNotExist();
} }
@ -423,7 +427,12 @@ class RateLimiter {
return $result['total_attempts'] < $this->autoBlacklistThreshold; return $result['total_attempts'] < $this->autoBlacklistThreshold;
} }
public function attempt($username, $ipAddress) { public function attempt($username, $ipAddress, $failed = true) {
// Only record failed attempts
if (!$failed) {
return true;
}
// Record this attempt // Record this attempt
$sql = "INSERT INTO {$this->authRatelimitTable} (ip_address, username) VALUES (:ip, :username)"; $sql = "INSERT INTO {$this->authRatelimitTable} (ip_address, username) VALUES (:ip, :username)";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);

View File

@ -0,0 +1,179 @@
<?php
/**
* Session Class
*
* Core session management functionality for the application
*/
class Session {
private static $sessionOptions = [
'cookie_httponly' => 1,
'cookie_secure' => 1,
'cookie_samesite' => 'Strict',
'gc_maxlifetime' => 7200 // 2 hours
];
/**
* Start or resume a session with secure options
*/
public static function startSession() {
session_name('jilo');
if (session_status() !== PHP_SESSION_ACTIVE && !headers_sent()) {
session_start(self::$sessionOptions);
}
}
/**
* Destroy current session and clean up
*/
public static function destroySession() {
if (session_status() === PHP_SESSION_ACTIVE) {
session_unset();
session_destroy();
}
}
/**
* Get current username if set
*/
public static function getUsername() {
return isset($_SESSION['username']) ? htmlspecialchars($_SESSION['username']) : null;
}
/**
* Get current user ID if set
*/
public static function getUserId() {
return isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : null;
}
/**
* Check if current session is valid
*/
public static function isValidSession() {
// Check required session variables
if (!isset($_SESSION['user_id']) || !isset($_SESSION['username'])) {
return false;
}
// Check session timeout
$session_timeout = isset($_SESSION['REMEMBER_ME']) ? (30 * 24 * 60 * 60) : 7200; // 30 days or 2 hours
if (isset($_SESSION['LAST_ACTIVITY']) && (time() - $_SESSION['LAST_ACTIVITY'] > $session_timeout)) {
return false;
}
// Update last activity time
$_SESSION['LAST_ACTIVITY'] = time();
// Regenerate session ID periodically (every 30 minutes)
if (!isset($_SESSION['CREATED'])) {
$_SESSION['CREATED'] = time();
} else if (time() - $_SESSION['CREATED'] > 1800) {
// Regenerate session ID and update creation time
if (!headers_sent() && session_status() === PHP_SESSION_ACTIVE) {
$oldData = $_SESSION;
session_regenerate_id(true);
$_SESSION = $oldData;
$_SESSION['CREATED'] = time();
}
}
return true;
}
/**
* Set remember me option for extended session
*/
public static function setRememberMe($value = true) {
$_SESSION['REMEMBER_ME'] = $value;
}
/**
* Clear session data and cookies
*/
public static function cleanup($config) {
self::destroySession();
// Clear cookies if headers not sent
if (!headers_sent()) {
setcookie('username', '', [
'expires' => time() - 3600,
'path' => $config['folder'],
'domain' => $config['domain'],
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Strict'
]);
}
// Start fresh session
self::startSession();
// Reset session timeout flag
unset($_SESSION['session_timeout_shown']);
}
/**
* Create a new authenticated session for a user
*/
public static function createAuthSession($userId, $username, $rememberMe, $config) {
// Set cookie lifetime based on remember me
$cookieLifetime = $rememberMe ? time() + (30 * 24 * 60 * 60) : 0;
// Regenerate session ID to prevent session fixation
session_regenerate_id(true);
// Set cookie with secure options
setcookie('username', $username, [
'expires' => $cookieLifetime,
'path' => $config['folder'],
'domain' => $config['domain'],
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Strict'
]);
// Set session variables
$_SESSION['user_id'] = $userId;
$_SESSION['username'] = $username;
$_SESSION['LAST_ACTIVITY'] = time();
if ($rememberMe) {
self::setRememberMe(true);
}
}
/**
* Store 2FA pending information in session
*/
public static function store2FAPending($userId, $username, $rememberMe = false) {
$_SESSION['2fa_pending_user_id'] = $userId;
$_SESSION['2fa_pending_username'] = $username;
if ($rememberMe) {
$_SESSION['2fa_pending_remember'] = true;
}
}
/**
* Clear 2FA pending information from session
*/
public static function clear2FAPending() {
unset($_SESSION['2fa_pending_user_id']);
unset($_SESSION['2fa_pending_username']);
unset($_SESSION['2fa_pending_remember']);
}
/**
* Get 2FA pending information
*/
public static function get2FAPending() {
if (!isset($_SESSION['2fa_pending_user_id']) || !isset($_SESSION['2fa_pending_username'])) {
return null;
}
return [
'user_id' => $_SESSION['2fa_pending_user_id'],
'username' => $_SESSION['2fa_pending_username'],
'remember_me' => isset($_SESSION['2fa_pending_remember'])
];
}
}

View File

@ -33,69 +33,12 @@ class User {
} }
/**
* Registers a new user.
*
* @param string $username The username of the new user.
* @param string $password The password for the new user.
*
* @return bool|string True if registration is successful, error message otherwise.
*/
public function register($username, $password) {
try {
// we have two inserts, start a transaction
$this->db->beginTransaction();
// hash the password, don't store it plain
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
// insert into users table
$sql = 'INSERT
INTO users (username, password)
VALUES (:username, :password)';
$query = $this->db->prepare($sql);
$query->bindValue(':username', $username);
$query->bindValue(':password', $hashedPassword);
// execute the first query
if (!$query->execute()) {
// rollback on error
$this->db->rollBack();
return false;
}
// insert the last user id into users_meta table
$sql2 = 'INSERT
INTO users_meta (user_id)
VALUES (:user_id)';
$query2 = $this->db->prepare($sql2);
$query2->bindValue(':user_id', $this->db->lastInsertId());
// execute the second query
if (!$query2->execute()) {
// rollback on error
$this->db->rollBack();
return false;
}
// if all is OK, commit the transaction
$this->db->commit();
return true;
} catch (Exception $e) {
// rollback on any error
$this->db->rollBack();
return $e->getMessage();
}
}
/** /**
* Logs in a user by verifying credentials. * Logs in a user by verifying credentials.
* *
* @param string $username The username of the user. * @param string $username The username of the user.
* @param string $password The password of the user. * @param string $password The password of the user.
* @param string $twoFactorCode Optional. The 2FA code if 2FA is enabled. * @param string $twoFactorCode Optional. The 2FA code if 2FA is enabled.
* *
* @return array Login result with status and any necessary data * @return array Login result with status and any necessary data
*/ */
@ -104,9 +47,6 @@ class User {
require_once __DIR__ . '/../helpers/logs.php'; require_once __DIR__ . '/../helpers/logs.php';
$ipAddress = getUserIP(); $ipAddress = getUserIP();
// Record attempt
$this->rateLimiter->attempt($username, $ipAddress);
// Check rate limiting first // Check rate limiting first
if (!$this->rateLimiter->isAllowed($username, $ipAddress)) { if (!$this->rateLimiter->isAllowed($username, $ipAddress)) {
$remainingTime = $this->rateLimiter->getDecayMinutes(); $remainingTime = $this->rateLimiter->getDecayMinutes();
@ -180,11 +120,11 @@ class User {
/** /**
* Fetches user details by user ID. * Fetches user details by user ID.
* *
* @param int $user_id The user ID. * @param int $userId The user ID.
* *
* @return array|null User details or null if not found. * @return array|null User details or null if not found.
*/ */
public function getUserDetails($user_id) { public function getUserDetails($userId) {
$sql = 'SELECT $sql = 'SELECT
um.*, um.*,
u.username u.username
@ -197,7 +137,7 @@ class User {
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->execute([ $query->execute([
':user_id' => $user_id, ':user_id' => $userId,
]); ]);
return $query->fetchAll(PDO::FETCH_ASSOC); return $query->fetchAll(PDO::FETCH_ASSOC);
@ -208,19 +148,19 @@ class User {
/** /**
* Grants a user a specific right. * Grants a user a specific right.
* *
* @param int $user_id The user ID. * @param int $userId The user ID.
* @param int $right_id The right ID to grant. * @param int $right_id The right ID to grant.
* *
* @return void * @return void
*/ */
public function addUserRight($user_id, $right_id) { public function addUserRight($userId, $right_id) {
$sql = 'INSERT INTO users_rights $sql = 'INSERT INTO users_rights
(user_id, right_id) (user_id, right_id)
VALUES VALUES
(:user_id, :right_id)'; (:user_id, :right_id)';
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->execute([ $query->execute([
':user_id' => $user_id, ':user_id' => $userId,
':right_id' => $right_id, ':right_id' => $right_id,
]); ]);
} }
@ -229,12 +169,12 @@ class User {
/** /**
* Revokes a specific right from a user. * Revokes a specific right from a user.
* *
* @param int $user_id The user ID. * @param int $userId The user ID.
* @param int $right_id The right ID to revoke. * @param int $right_id The right ID to revoke.
* *
* @return void * @return void
*/ */
public function removeUserRight($user_id, $right_id) { public function removeUserRight($userId, $right_id) {
$sql = 'DELETE FROM users_rights $sql = 'DELETE FROM users_rights
WHERE WHERE
user_id = :user_id user_id = :user_id
@ -242,7 +182,7 @@ class User {
right_id = :right_id'; right_id = :right_id';
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->execute([ $query->execute([
':user_id' => $user_id, ':user_id' => $userId,
':right_id' => $right_id, ':right_id' => $right_id,
]); ]);
} }
@ -270,11 +210,11 @@ class User {
/** /**
* Retrieves the rights assigned to a specific user. * Retrieves the rights assigned to a specific user.
* *
* @param int $user_id The user ID. * @param int $userId The user ID.
* *
* @return array List of user rights. * @return array List of user rights.
*/ */
public function getUserRights($user_id) { public function getUserRights($userId) {
$sql = 'SELECT $sql = 'SELECT
u.id AS user_id, u.id AS user_id,
r.id AS right_id, r.id AS right_id,
@ -290,7 +230,7 @@ class User {
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->execute([ $query->execute([
':user_id' => $user_id, ':user_id' => $userId,
]); ]);
$result = $query->fetchAll(PDO::FETCH_ASSOC); $result = $query->fetchAll(PDO::FETCH_ASSOC);
@ -299,7 +239,7 @@ class User {
$specialEntries = []; $specialEntries = [];
// user 1 is always superuser // user 1 is always superuser
if ($user_id == 1) { if ($userId == 1) {
$specialEntries = [ $specialEntries = [
[ [
'user_id' => 1, 'user_id' => 1,
@ -309,7 +249,7 @@ class User {
]; ];
// user 2 is always demo // user 2 is always demo
} elseif ($user_id == 2) { } elseif ($userId == 2) {
$specialEntries = [ $specialEntries = [
[ [
'user_id' => 2, 'user_id' => 2,
@ -333,17 +273,17 @@ class User {
/** /**
* Check if the user has a specific right. * Check if the user has a specific right.
* *
* @param int $user_id The user ID. * @param int $userId The user ID.
* @param string $right_name The human-readable name of the user right. * @param string $right_name The human-readable name of the user right.
* *
* @return bool True if the user has the right, false otherwise. * @return bool True if the user has the right, false otherwise.
*/ */
function hasRight($user_id, $right_name) { function hasRight($userId, $right_name) {
$userRights = $this->getUserRights($user_id); $userRights = $this->getUserRights($userId);
$userHasRight = false; $userHasRight = false;
// superuser always has all the rights // superuser always has all the rights
if ($user_id === 1) { if ($userId === 1) {
$userHasRight = true; $userHasRight = true;
} }
@ -362,8 +302,8 @@ class User {
/** /**
* Updates a user's metadata in the database. * Updates a user's metadata in the database.
* *
* @param int $user_id The ID of the user to update. * @param int $userId The ID of the user to update.
* @param array $updatedUser An associative array containing updated user data: * @param array $updatedUser An associative array containing updated user data:
* - 'name' (string): The updated name of the user. * - 'name' (string): The updated name of the user.
* - 'email' (string): The updated email of the user. * - 'email' (string): The updated email of the user.
* - 'timezone' (string): The updated timezone of the user. * - 'timezone' (string): The updated timezone of the user.
@ -371,7 +311,7 @@ class User {
* *
* @return bool|string Returns true if the update is successful, or an error message if an exception occurs. * @return bool|string Returns true if the update is successful, or an error message if an exception occurs.
*/ */
public function editUser($user_id, $updatedUser) { public function editUser($userId, $updatedUser) {
try { try {
$sql = 'UPDATE users_meta SET $sql = 'UPDATE users_meta SET
name = :name, name = :name,
@ -381,7 +321,7 @@ class User {
WHERE user_id = :user_id'; WHERE user_id = :user_id';
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->execute([ $query->execute([
':user_id' => $user_id, ':user_id' => $userId,
':name' => $updatedUser['name'], ':name' => $updatedUser['name'],
':email' => $updatedUser['email'], ':email' => $updatedUser['email'],
':timezone' => $updatedUser['timezone'], ':timezone' => $updatedUser['timezone'],
@ -400,12 +340,12 @@ class User {
/** /**
* Removes a user's avatar from the database and deletes the associated file. * Removes a user's avatar from the database and deletes the associated file.
* *
* @param int $user_id The ID of the user whose avatar is being removed. * @param int $userId The ID of the user whose avatar is being removed.
* @param string $old_avatar Optional. The file path of the current avatar to delete. Default is an empty string. * @param string $old_avatar Optional. The file path of the current avatar to delete. Default is an empty string.
* *
* @return bool|string Returns true if the avatar is successfully removed, or an error message if an exception occurs. * @return bool|string Returns true if the avatar is successfully removed, or an error message if an exception occurs.
*/ */
public function removeAvatar($user_id, $old_avatar = '') { public function removeAvatar($userId, $old_avatar = '') {
try { try {
// remove from database // remove from database
$sql = 'UPDATE users_meta SET $sql = 'UPDATE users_meta SET
@ -413,7 +353,7 @@ class User {
WHERE user_id = :user_id'; WHERE user_id = :user_id';
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->execute([ $query->execute([
':user_id' => $user_id, ':user_id' => $userId,
]); ]);
// delete the old avatar file // delete the old avatar file
@ -433,14 +373,14 @@ class User {
/** /**
* Updates a user's avatar by uploading a new file and saving its path in the database. * Updates a user's avatar by uploading a new file and saving its path in the database.
* *
* @param int $user_id The ID of the user whose avatar is being updated. * @param int $userId The ID of the user whose avatar is being updated.
* @param array $avatar_file The uploaded avatar file from the $_FILES array. * @param array $avatar_file The uploaded avatar file from the $_FILES array.
* Should include 'tmp_name', 'name', 'error', etc. * Should include 'tmp_name', 'name', 'error', etc.
* @param string $avatars_path The directory path where avatar files should be saved. * @param string $avatars_path The directory path where avatar files should be saved.
* *
* @return bool|string Returns true if the avatar is successfully updated, or an error message if an exception occurs. * @return bool|string Returns true if the avatar is successfully updated, or an error message if an exception occurs.
*/ */
public function changeAvatar($user_id, $avatar_file, $avatars_path) { public function changeAvatar($userId, $avatar_file, $avatars_path) {
try { try {
// check if the file was uploaded // check if the file was uploaded
if (isset($avatar_file) && $avatar_file['error'] === UPLOAD_ERR_OK) { if (isset($avatar_file) && $avatar_file['error'] === UPLOAD_ERR_OK) {
@ -463,7 +403,7 @@ class User {
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->execute([ $query->execute([
':avatar' => $newFileName, ':avatar' => $newFileName,
':user_id' => $user_id ':user_id' => $userId
]); ]);
// all went OK // all went OK
$_SESSION['notice'] .= 'Avatar updated successfully. '; $_SESSION['notice'] .= 'Avatar updated successfully. ';
@ -505,9 +445,9 @@ class User {
/** /**
* Enable two-factor authentication for a user * Enable two-factor authentication for a user
* *
* @param int $userId User ID * @param int $userId User ID
* @param string $secret Secret key to use * @param string $secret Secret key to use
* @param string $code Verification code to validate * @param string $code Verification code to validate
* @return bool True if enabled successfully * @return bool True if enabled successfully
*/ */
public function enableTwoFactor($userId, $secret = null, $code = null) { public function enableTwoFactor($userId, $secret = null, $code = null) {
@ -527,8 +467,8 @@ class User {
/** /**
* Verify a two-factor authentication code * Verify a two-factor authentication code
* *
* @param int $userId User ID * @param int $userId User ID
* @param string $code The verification code * @param string $code The verification code
* @return bool True if verified * @return bool True if verified
*/ */
public function verifyTwoFactor($userId, $code) { public function verifyTwoFactor($userId, $code) {
@ -548,9 +488,9 @@ class User {
/** /**
* Change a user's password * Change a user's password
* *
* @param int $userId User ID * @param int $userId User ID
* @param string $currentPassword Current password for verification * @param string $currentPassword Current password for verification
* @param string $newPassword New password to set * @param string $newPassword New password to set
* @return bool True if password was changed successfully * @return bool True if password was changed successfully
*/ */
public function changePassword($userId, $currentPassword, $newPassword) { public function changePassword($userId, $currentPassword, $newPassword) {

View File

@ -4,94 +4,33 @@
* Session Middleware * Session Middleware
* *
* Validates session status and handles session timeout. * Validates session status and handles session timeout.
* This middleware should be included in all protected pages. * If session is invalid, redirects to login page.
*/ */
function applySessionMiddleware($config, $app_root) { function applySessionMiddleware($config, $app_root, $isTest = false) {
$isTest = defined('PHPUNIT_RUNNING'); // Start session if not already started
if (session_status() !== PHP_SESSION_ACTIVE) {
Session::startSession();
}
// Access $_SESSION directly in test mode // Check session validity
if (!$isTest) { if (!Session::isValidSession()) {
// Start session if not already started // Only show session timeout message if there was an active session
if (session_status() !== PHP_SESSION_ACTIVE && !headers_sent()) { // and we haven't shown it yet
session_start([ if (isset($_SESSION['LAST_ACTIVITY']) && !isset($_SESSION['session_timeout_shown'])) {
'cookie_httponly' => 1, Feedback::flash('LOGIN', 'SESSION_TIMEOUT');
'cookie_secure' => 1, $_SESSION['session_timeout_shown'] = true;
'cookie_samesite' => 'Strict',
'gc_maxlifetime' => 7200 // 2 hours
]);
} }
}
// Check if user is logged in with all required session variables // Session invalid, clean up and redirect
if (!isset($_SESSION['user_id']) || !isset($_SESSION['username'])) { Session::cleanup($config);
cleanupSession($config, $app_root, $isTest);
return false;
}
// Check session timeout if (!$isTest) {
$session_timeout = isset($_SESSION['REMEMBER_ME']) ? (30 * 24 * 60 * 60) : 7200; // 30 days or 2 hours header('Location: ' . $app_root . '?page=login');
if (isset($_SESSION['LAST_ACTIVITY']) && (time() - $_SESSION['LAST_ACTIVITY'] > $session_timeout)) { exit();
// Session has expired
cleanupSession($config, $app_root, $isTest);
return false;
}
// Update last activity time
$_SESSION['LAST_ACTIVITY'] = time();
// Regenerate session ID periodically (every 30 minutes)
if (!isset($_SESSION['CREATED'])) {
$_SESSION['CREATED'] = time();
} else if (time() - $_SESSION['CREATED'] > 1800) {
// Regenerate session ID and update creation time
if (!$isTest && !headers_sent() && session_status() === PHP_SESSION_ACTIVE) {
$oldData = $_SESSION;
session_regenerate_id(true);
$_SESSION = $oldData;
$_SESSION['CREATED'] = time();
} }
return false;
} }
return true; return true;
} }
/**
* Helper function to clean up session data and redirect
*/
function cleanupSession($config, $app_root, $isTest) {
// Always clear session data
$_SESSION = array();
if (!$isTest) {
if (session_status() === PHP_SESSION_ACTIVE) {
session_unset();
session_destroy();
// Start a new session to prevent errors
if (!headers_sent()) {
session_start([
'cookie_httponly' => 1,
'cookie_secure' => 1,
'cookie_samesite' => 'Strict',
'gc_maxlifetime' => 7200
]);
}
}
// Clear cookies
if (!headers_sent()) {
setcookie('username', '', [
'expires' => time() - 3600,
'path' => $config['folder'],
'domain' => $config['domain'],
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Strict'
]);
}
header('Location: ' . $app_root . '?page=login&timeout=1');
exit();
}
}

View File

@ -11,6 +11,7 @@ return [
'LOGIN_SUCCESS' => 'Login successful.', 'LOGIN_SUCCESS' => 'Login successful.',
'LOGIN_FAILED' => 'Login failed. Please check your credentials.', 'LOGIN_FAILED' => 'Login failed. Please check your credentials.',
'LOGOUT_SUCCESS' => 'Logout successful. You can log in again.', 'LOGOUT_SUCCESS' => 'Logout successful. You can log in again.',
'SESSION_TIMEOUT' => 'Your session has expired. Please log in again.',
'IP_BLACKLISTED' => 'Access denied. Your IP address is blacklisted.', 'IP_BLACKLISTED' => 'Access denied. Your IP address is blacklisted.',
'IP_NOT_WHITELISTED' => 'Access denied. Your IP address is not whitelisted.', 'IP_NOT_WHITELISTED' => 'Access denied. Your IP address is not whitelisted.',
'TOO_MANY_ATTEMPTS' => 'Too many login attempts. Please try again later.', 'TOO_MANY_ATTEMPTS' => 'Too many login attempts. Please try again later.',

View File

@ -50,7 +50,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Apply rate limiting for adding new contacts // Apply rate limiting for adding new contacts
require '../app/includes/rate_limit_middleware.php'; require '../app/includes/rate_limit_middleware.php';
checkRateLimit($dbWeb, 'contact', $user_id); checkRateLimit($dbWeb, 'contact', $userId);
// Validate agent ID for POST operations // Validate agent ID for POST operations
if ($agentId === false || $agentId === null) { if ($agentId === false || $agentId === null) {

View File

@ -51,8 +51,8 @@ if (!$isWritable) {
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Check if user has permission to edit config // Check if user has permission to edit config
if (!$userObject->hasRight($user_id, 'edit config file')) { if (!$userObject->hasRight($userId, 'edit config file')) {
$logObject->insertLog($user_id, "Unauthorized: User \"$currentUser\" tried to edit config file. IP: $user_IP", 'system'); $logObject->insertLog($userId, "Unauthorized: User \"$currentUser\" tried to edit config file. IP: $user_IP", 'system');
if ($isAjax) { if ($isAjax) {
ApiResponse::error('Forbidden: You do not have permission to edit the config file', null, 403); ApiResponse::error('Forbidden: You do not have permission to edit the config file', null, 403);
exit; exit;
@ -64,7 +64,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Apply rate limiting // Apply rate limiting
require '../app/includes/rate_limit_middleware.php'; require '../app/includes/rate_limit_middleware.php';
checkRateLimit($dbWeb, 'config', $user_id); checkRateLimit($dbWeb, 'config', $userId);
// Ensure no output before this point // Ensure no output before this point
ob_clean(); ob_clean();
@ -74,7 +74,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Get raw input // Get raw input
$jsonData = file_get_contents('php://input'); $jsonData = file_get_contents('php://input');
if ($jsonData === false) { if ($jsonData === false) {
$logObject->insertLog($user_id, "Failed to read request data for config update", 'system'); $logObject->insertLog($userId, "Failed to read request data for config update", 'system');
ApiResponse::error('Failed to read request data'); ApiResponse::error('Failed to read request data');
exit; exit;
} }
@ -115,10 +115,10 @@ if (!$isAjax) {
* Handles GET requests to display templates. * Handles GET requests to display templates.
*/ */
if ($userObject->hasRight($user_id, 'view config file')) { if ($userObject->hasRight($userId, 'view config file')) {
include '../app/templates/config.php'; include '../app/templates/config.php';
} else { } else {
$logObject->insertLog($user_id, "Unauthorized: User \"$currentUser\" tried to access \"config\" page. IP: $user_IP", 'system'); $logObject->insertLog($userId, "Unauthorized: User \"$currentUser\" tried to access \"config\" page. IP: $user_IP", 'system');
include '../app/templates/error-unauthorized.php'; include '../app/templates/error-unauthorized.php';
} }
} }

View File

@ -14,17 +14,10 @@
* - `password`: Change password * - `password`: Change password
*/ */
// Check if user is logged in
if (!isset($_SESSION['user_id'])) {
header("Location: $app_root?page=login");
exit();
}
$user_id = $_SESSION['user_id'];
// Initialize user object // Initialize user object
$userObject = new User($dbWeb); $userObject = new User($dbWeb);
// Get action and item from request
$action = $_REQUEST['action'] ?? ''; $action = $_REQUEST['action'] ?? '';
$item = $_REQUEST['item'] ?? ''; $item = $_REQUEST['item'] ?? '';
@ -40,7 +33,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// Apply rate limiting // Apply rate limiting
require_once '../app/includes/rate_limit_middleware.php'; require_once '../app/includes/rate_limit_middleware.php';
checkRateLimit($dbWeb, 'credentials', $user_id); checkRateLimit($dbWeb, 'credentials', $userId);
switch ($item) { switch ($item) {
case '2fa': case '2fa':
@ -50,7 +43,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$code = $_POST['code'] ?? ''; $code = $_POST['code'] ?? '';
$secret = $_POST['secret'] ?? ''; $secret = $_POST['secret'] ?? '';
if ($userObject->enableTwoFactor($user_id, $secret, $code)) { if ($userObject->enableTwoFactor($userId, $secret, $code)) {
Feedback::flash('NOTICE', 'DEFAULT', 'Two-factor authentication has been enabled successfully.'); Feedback::flash('NOTICE', 'DEFAULT', 'Two-factor authentication has been enabled successfully.');
header("Location: $app_root?page=credentials"); header("Location: $app_root?page=credentials");
exit(); exit();
@ -67,7 +60,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
case 'verify': case 'verify':
// This is a user-initiated verification // This is a user-initiated verification
$code = $_POST['code'] ?? ''; $code = $_POST['code'] ?? '';
if ($userObject->verifyTwoFactor($user_id, $code)) { if ($userObject->verifyTwoFactor($userId, $code)) {
$_SESSION['2fa_verified'] = true; $_SESSION['2fa_verified'] = true;
header("Location: $app_root?page=dashboard"); header("Location: $app_root?page=dashboard");
exit(); exit();
@ -79,7 +72,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
break; break;
case 'disable': case 'disable':
if ($userObject->disableTwoFactor($user_id)) { if ($userObject->disableTwoFactor($userId)) {
Feedback::flash('NOTICE', 'DEFAULT', 'Two-factor authentication has been disabled.'); Feedback::flash('NOTICE', 'DEFAULT', 'Two-factor authentication has been disabled.');
} else { } else {
Feedback::flash('ERROR', 'DEFAULT', 'Failed to disable two-factor authentication.'); Feedback::flash('ERROR', 'DEFAULT', 'Failed to disable two-factor authentication.');
@ -96,8 +89,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$validator = new Validator($_POST); $validator = new Validator($_POST);
$rules = [ $rules = [
'current_password' => [ 'current_password' => [
'required' => true, 'required' => true
'min' => 8
], ],
'new_password' => [ 'new_password' => [
'required' => true, 'required' => true,
@ -115,7 +107,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
exit(); exit();
} }
if ($userObject->changePassword($user_id, $_POST['current_password'], $_POST['new_password'])) { if ($userObject->changePassword($userId, $_POST['current_password'], $_POST['new_password'])) {
Feedback::flash('NOTICE', 'DEFAULT', 'Password has been changed successfully.'); Feedback::flash('NOTICE', 'DEFAULT', 'Password has been changed successfully.');
} else { } else {
Feedback::flash('ERROR', 'DEFAULT', 'Failed to change password. Please verify your current password.'); Feedback::flash('ERROR', 'DEFAULT', 'Failed to change password. Please verify your current password.');
@ -136,12 +128,12 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$security->generateCsrfToken(); $security->generateCsrfToken();
// Get 2FA status for the template // Get 2FA status for the template
$has2fa = $userObject->isTwoFactorEnabled($user_id); $has2fa = $userObject->isTwoFactorEnabled($userId);
switch ($action) { switch ($action) {
case 'setup': case 'setup':
if (!$has2fa) { if (!$has2fa) {
$result = $userObject->enableTwoFactor($user_id); $result = $userObject->enableTwoFactor($userId);
if ($result['success']) { if ($result['success']) {
$setupData = $result['data']; $setupData = $result['data'];
} else { } else {

View File

@ -33,21 +33,23 @@ try {
if ($action === 'verify' && isset($_SESSION['2fa_pending_user_id'])) { if ($action === 'verify' && isset($_SESSION['2fa_pending_user_id'])) {
// Handle 2FA verification // Handle 2FA verification
$code = $_POST['code'] ?? ''; $code = $_POST['code'] ?? '';
$userId = $_SESSION['2fa_pending_user_id']; $pending2FA = Session::get2FAPending();
$username = $_SESSION['2fa_pending_username'];
$rememberMe = isset($_SESSION['2fa_pending_remember']); if (!$pending2FA) {
header('Location: ' . htmlspecialchars($app_root) . '?page=login');
exit();
}
require_once '../app/classes/twoFactorAuth.php'; require_once '../app/classes/twoFactorAuth.php';
$twoFactorAuth = new TwoFactorAuthentication($db); $twoFactorAuth = new TwoFactorAuthentication($db);
if ($twoFactorAuth->verify($userId, $code)) { if ($twoFactorAuth->verify($pending2FA['user_id'], $code)) {
// Complete login // Complete login
handleSuccessfulLogin($userId, $username, $rememberMe, $config, $logObject, $user_IP); handleSuccessfulLogin($pending2FA['user_id'], $pending2FA['username'],
$pending2FA['remember_me'], $config, $app_root, $logObject, $user_IP);
// Clean up 2FA session data // Clean up 2FA session data
unset($_SESSION['2fa_pending_user_id']); Session::clear2FAPending();
unset($_SESSION['2fa_pending_username']);
unset($_SESSION['2fa_pending_remember']);
exit(); exit();
} }
@ -60,6 +62,9 @@ try {
// Get any new feedback messages // Get any new feedback messages
include '../app/helpers/feedback.php'; include '../app/helpers/feedback.php';
// Make userId available to template
$userId = $pending2FA['user_id'];
// Load the 2FA verification template // Load the 2FA verification template
include '../app/templates/credentials-2fa-verify.php'; include '../app/templates/credentials-2fa-verify.php';
exit(); exit();
@ -196,8 +201,7 @@ try {
], ],
'password' => [ 'password' => [
'type' => 'string', 'type' => 'string',
'required' => true, 'required' => true
'min' => 5
] ]
]; ];
@ -220,9 +224,6 @@ try {
if ($rateLimiter->tooManyAttempts($username, $user_IP)) { if ($rateLimiter->tooManyAttempts($username, $user_IP)) {
throw new Exception(Feedback::get('LOGIN', 'TOO_MANY_ATTEMPTS')['message']); throw new Exception(Feedback::get('LOGIN', 'TOO_MANY_ATTEMPTS')['message']);
} }
// Record this attempt
$rateLimiter->attempt($username, $user_IP);
} }
// Attempt login // Attempt login
@ -232,11 +233,8 @@ try {
switch ($loginResult['status']) { switch ($loginResult['status']) {
case 'requires_2fa': case 'requires_2fa':
// Store pending 2FA info // Store pending 2FA info
$_SESSION['2fa_pending_user_id'] = $loginResult['user_id']; Session::store2FAPending($loginResult['user_id'], $loginResult['username'],
$_SESSION['2fa_pending_username'] = $loginResult['username']; isset($formData['remember_me']));
if (isset($formData['remember_me'])) {
$_SESSION['2fa_pending_remember'] = true;
}
// Redirect to 2FA verification // Redirect to 2FA verification
header('Location: ?page=login&action=verify'); header('Location: ?page=login&action=verify');
@ -245,7 +243,7 @@ try {
case 'success': case 'success':
// Complete login // Complete login
handleSuccessfulLogin($loginResult['user_id'], $loginResult['username'], handleSuccessfulLogin($loginResult['user_id'], $loginResult['username'],
isset($formData['remember_me']), $config, $logObject, $user_IP); isset($formData['remember_me']), $config, $app_root, $logObject, $user_IP);
exit(); exit();
default: default:
@ -258,8 +256,9 @@ try {
// Log the failed attempt // Log the failed attempt
Feedback::flash('ERROR', 'DEFAULT', $e->getMessage()); Feedback::flash('ERROR', 'DEFAULT', $e->getMessage());
if (isset($username)) { if (isset($username)) {
$user_id = $userObject->getUserId($username)[0]['id'] ?? 0; $userId = $userObject->getUserId($username)[0]['id'] ?? 0;
$logObject->insertLog($user_id, "Login: Failed login attempt for user \"$username\". IP: $user_IP. Reason: {$e->getMessage()}", 'user'); $logObject->insertLog($userId, "Login: Failed login attempt for user \"$username\". IP: $user_IP. Reason: {$e->getMessage()}", 'user');
$rateLimiter->attempt($username, $user_IP);
} }
} }
} }
@ -269,7 +268,7 @@ try {
// Show configured login message if any // Show configured login message if any
if (!empty($config['login_message'])) { if (!empty($config['login_message'])) {
echo Feedback::render('NOTICE', 'DEFAULT', $config['login_message'], false); echo Feedback::render('NOTICE', 'DEFAULT', $config['login_message'], false, false, false);
} }
// Get any new feedback messages // Get any new feedback messages
@ -281,37 +280,9 @@ include '../app/templates/form-login.php';
/** /**
* Handle successful login by setting up session and cookies * Handle successful login by setting up session and cookies
*/ */
function handleSuccessfulLogin($userId, $username, $rememberMe, $config, $logObject, $userIP) { function handleSuccessfulLogin($userId, $username, $rememberMe, $config, $app_root, $logObject, $userIP) {
if ($rememberMe) { // Create authenticated session
// 30*24*60*60 = 30 days Session::createAuthSession($userId, $username, $rememberMe, $config);
$cookie_lifetime = 30 * 24 * 60 * 60;
$setcookie_lifetime = time() + 30 * 24 * 60 * 60;
} else {
// 0 - session end on browser close
$cookie_lifetime = 0;
$setcookie_lifetime = 0;
}
// Regenerate session ID to prevent session fixation
session_regenerate_id(true);
// set session lifetime and cookies
setcookie('username', $username, [
'expires' => $setcookie_lifetime,
'path' => $config['folder'],
'domain' => $config['domain'],
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Strict'
]);
// Set session variables
$_SESSION['user_id'] = $userId;
$_SESSION['USERNAME'] = $username;
$_SESSION['LAST_ACTIVITY'] = time();
if ($rememberMe) {
$_SESSION['REMEMBER_ME'] = true;
}
// Log successful login // Log successful login
$logObject->insertLog($userId, "Login: User \"$username\" logged in. IP: $userIP", 'user'); $logObject->insertLog($userId, "Login: User \"$username\" logged in. IP: $userIP", 'user');

View File

@ -12,8 +12,8 @@
include '../app/helpers/feedback.php'; include '../app/helpers/feedback.php';
// Check for rights; user or system // Check for rights; user or system
$has_system_access = ($userObject->hasRight($user_id, 'superuser') || $has_system_access = ($userObject->hasRight($userId, 'superuser') ||
$userObject->hasRight($user_id, 'view app logs')); $userObject->hasRight($userId, 'view app logs'));
// Get current page for pagination // Get current page for pagination
$currentPage = $_REQUEST['page_num'] ?? 1; $currentPage = $_REQUEST['page_num'] ?? 1;
@ -69,8 +69,8 @@ if (isset($_REQUEST['tab'])) {
} }
// prepare the result // prepare the result
$search = $logObject->readLog($user_id, $scope, $offset, $items_per_page, $filters); $search = $logObject->readLog($userId, $scope, $offset, $items_per_page, $filters);
$search_all = $logObject->readLog($user_id, $scope, 0, 0, $filters); $search_all = $logObject->readLog($userId, $scope, 0, 0, $filters);
if (!empty($search)) { if (!empty($search)) {
// we get total items and number of pages // we get total items and number of pages
@ -103,7 +103,7 @@ if (!empty($search)) {
} }
} }
$username = $userObject->getUserDetails($user_id)[0]['username']; $username = $userObject->getUserDetails($userId)[0]['username'];
// Load the template // Load the template
include '../app/templates/logs.php'; include '../app/templates/logs.php';

View File

@ -30,11 +30,11 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// Apply rate limiting for profile operations // Apply rate limiting for profile operations
require_once '../app/includes/rate_limit_middleware.php'; require_once '../app/includes/rate_limit_middleware.php';
checkRateLimit($dbWeb, 'profile', $user_id); checkRateLimit($dbWeb, 'profile', $userId);
// avatar removal // avatar removal
if ($item === 'avatar' && $action === 'remove') { if ($item === 'avatar' && $action === 'remove') {
$validator = new Validator(['user_id' => $user_id]); $validator = new Validator(['user_id' => $userId]);
$rules = [ $rules = [
'user_id' => [ 'user_id' => [
'required' => true, 'required' => true,
@ -48,7 +48,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
exit(); exit();
} }
$result = $userObject->removeAvatar($user_id, $config['avatars_path'].$userDetails[0]['avatar']); $result = $userObject->removeAvatar($userId, $config['avatars_path'].$userDetails[0]['avatar']);
if ($result === true) { if ($result === true) {
Feedback::flash('NOTICE', 'DEFAULT', "Avatar for user \"{$userDetails[0]['username']}\" is removed."); Feedback::flash('NOTICE', 'DEFAULT', "Avatar for user \"{$userDetails[0]['username']}\" is removed.");
} else { } else {
@ -89,50 +89,54 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
'timezone' => htmlspecialchars($_POST['timezone'] ?? ''), 'timezone' => htmlspecialchars($_POST['timezone'] ?? ''),
'bio' => htmlspecialchars($_POST['bio'] ?? ''), 'bio' => htmlspecialchars($_POST['bio'] ?? ''),
]; ];
$result = $userObject->editUser($user_id, $updatedUser); $result = $userObject->editUser($userId, $updatedUser);
if ($result === true) { if ($result === true) {
Feedback::flash('NOTICE', 'DEFAULT', "User details for \"{$updatedUser['name']}\" are edited."); Feedback::flash('NOTICE', 'DEFAULT', "User details for \"{$userDetails[0]['username']}\" are edited.");
} else { } else {
Feedback::flash('ERROR', 'DEFAULT', "Editing the user details failed. Error: $result"); Feedback::flash('ERROR', 'DEFAULT', "Editing the user details failed. Error: $result");
} }
// update the rights // update the rights
if (isset($_POST['rights'])) { // Get current rights IDs
$validator = new Validator(['rights' => $_POST['rights']]); $userRightsIds = array_column($userRights, 'right_id');
$rules = [
'rights' => [
'array' => true
]
];
if (!$validator->validate($rules)) { // If no rights are selected, remove all rights
Feedback::flash('ERROR', 'DEFAULT', $validator->getFirstError()); if (!isset($_POST['rights'])) {
header("Location: $app_root?page=profile"); $_POST['rights'] = [];
exit(); }
}
$newRights = $_POST['rights']; $validator = new Validator(['rights' => $_POST['rights']]);
// extract the new right_ids $rules = [
$userRightsIds = array_column($userRights, 'right_id'); 'rights' => [
// what rights we need to add 'array' => true
$rightsToAdd = array_diff($newRights, $userRightsIds); ]
if (!empty($rightsToAdd)) { ];
foreach ($rightsToAdd as $rightId) {
$userObject->addUserRight($user_id, $rightId); if (!$validator->validate($rules)) {
} Feedback::flash('ERROR', 'DEFAULT', $validator->getFirstError());
header("Location: $app_root?page=profile");
exit();
}
$newRights = $_POST['rights'];
// what rights we need to add
$rightsToAdd = array_diff($newRights, $userRightsIds);
if (!empty($rightsToAdd)) {
foreach ($rightsToAdd as $rightId) {
$userObject->addUserRight($userId, $rightId);
} }
// what rights we need to remove }
$rightsToRemove = array_diff($userRightsIds, $newRights); // what rights we need to remove
if (!empty($rightsToRemove)) { $rightsToRemove = array_diff($userRightsIds, $newRights);
foreach ($rightsToRemove as $rightId) { if (!empty($rightsToRemove)) {
$userObject->removeUserRight($user_id, $rightId); foreach ($rightsToRemove as $rightId) {
} $userObject->removeUserRight($userId, $rightId);
} }
} }
// update the avatar // update the avatar
if (!empty($_FILES['avatar_file']['tmp_name'])) { if (!empty($_FILES['avatar_file']['tmp_name'])) {
$result = $userObject->changeAvatar($user_id, $_FILES['avatar_file'], $config['avatars_path']); $result = $userObject->changeAvatar($userId, $_FILES['avatar_file'], $config['avatars_path']);
} }
header("Location: $app_root?page=profile"); header("Location: $app_root?page=profile");

View File

@ -1,15 +1,10 @@
<?php <?php
// Check if user has any of the required rights // Check if user has any of the required rights
if (!($userObject->hasRight($user_id, 'superuser') || if (!($userObject->hasRight($userId, 'superuser') ||
$userObject->hasRight($user_id, 'edit whitelist') || $userObject->hasRight($userId, 'edit whitelist') ||
$userObject->hasRight($user_id, 'edit blacklist') || $userObject->hasRight($userId, 'edit blacklist') ||
$userObject->hasRight($user_id, 'edit ratelimiting'))) { $userObject->hasRight($userId, 'edit ratelimiting'))) {
include '../app/templates/error-unauthorized.php';
exit;
}
if (!isset($currentUser)) {
include '../app/templates/error-unauthorized.php'; include '../app/templates/error-unauthorized.php';
exit; exit;
} }
@ -27,7 +22,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
// Apply rate limiting for security operations // Apply rate limiting for security operations
require_once '../app/includes/rate_limit_middleware.php'; require_once '../app/includes/rate_limit_middleware.php';
checkRateLimit($dbWeb, 'security', $user_id); checkRateLimit($dbWeb, 'security', $userId);
$action = $_POST['action']; $action = $_POST['action'];
$validator = new Validator($_POST); $validator = new Validator($_POST);
@ -35,7 +30,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
try { try {
switch ($action) { switch ($action) {
case 'add_whitelist': case 'add_whitelist':
if (!$userObject->hasRight($user_id, 'superuser') && !$userObject->hasRight($user_id, 'edit whitelist')) { if (!$userObject->hasRight($userId, 'superuser') && !$userObject->hasRight($userId, 'edit whitelist')) {
Feedback::flash('SECURITY', 'PERMISSION_DENIED'); Feedback::flash('SECURITY', 'PERMISSION_DENIED');
break; break;
} }
@ -54,7 +49,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
if ($validator->validate($rules)) { if ($validator->validate($rules)) {
$is_network = isset($_POST['is_network']) && $_POST['is_network'] === 'on'; $is_network = isset($_POST['is_network']) && $_POST['is_network'] === 'on';
if (!$rateLimiter->addToWhitelist($_POST['ip_address'], $is_network, $_POST['description'] ?? '', $currentUser, $user_id)) { if (!$rateLimiter->addToWhitelist($_POST['ip_address'], $is_network, $_POST['description'] ?? '', $currentUser, $userId)) {
Feedback::flash('SECURITY', 'WHITELIST_ADD_FAILED'); Feedback::flash('SECURITY', 'WHITELIST_ADD_FAILED');
} else { } else {
Feedback::flash('SECURITY', 'WHITELIST_ADD_SUCCESS'); Feedback::flash('SECURITY', 'WHITELIST_ADD_SUCCESS');
@ -65,7 +60,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
break; break;
case 'remove_whitelist': case 'remove_whitelist':
if (!$userObject->hasRight($user_id, 'superuser') && !$userObject->hasRight($user_id, 'edit whitelist')) { if (!$userObject->hasRight($userId, 'superuser') && !$userObject->hasRight($userId, 'edit whitelist')) {
Feedback::flash('SECURITY', 'PERMISSION_DENIED'); Feedback::flash('SECURITY', 'PERMISSION_DENIED');
break; break;
} }
@ -79,7 +74,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
]; ];
if ($validator->validate($rules)) { if ($validator->validate($rules)) {
if (!$rateLimiter->removeFromWhitelist($_POST['ip_address'], $currentUser, $user_id)) { if (!$rateLimiter->removeFromWhitelist($_POST['ip_address'], $currentUser, $userId)) {
Feedback::flash('SECURITY', 'WHITELIST_REMOVE_FAILED'); Feedback::flash('SECURITY', 'WHITELIST_REMOVE_FAILED');
} else { } else {
Feedback::flash('SECURITY', 'WHITELIST_REMOVE_SUCCESS'); Feedback::flash('SECURITY', 'WHITELIST_REMOVE_SUCCESS');
@ -90,7 +85,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
break; break;
case 'add_blacklist': case 'add_blacklist':
if (!$userObject->hasRight($user_id, 'superuser') && !$userObject->hasRight($user_id, 'edit blacklist')) { if (!$userObject->hasRight($userId, 'superuser') && !$userObject->hasRight($userId, 'edit blacklist')) {
Feedback::flash('SECURITY', 'PERMISSION_DENIED'); Feedback::flash('SECURITY', 'PERMISSION_DENIED');
break; break;
} }
@ -116,7 +111,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$is_network = isset($_POST['is_network']) && $_POST['is_network'] === 'on'; $is_network = isset($_POST['is_network']) && $_POST['is_network'] === 'on';
$expiry_hours = !empty($_POST['expiry_hours']) ? (int)$_POST['expiry_hours'] : null; $expiry_hours = !empty($_POST['expiry_hours']) ? (int)$_POST['expiry_hours'] : null;
if (!$rateLimiter->addToBlacklist($_POST['ip_address'], $is_network, $_POST['reason'], $currentUser, $user_id, $expiry_hours)) { if (!$rateLimiter->addToBlacklist($_POST['ip_address'], $is_network, $_POST['reason'], $currentUser, $userId, $expiry_hours)) {
Feedback::flash('SECURITY', 'BLACKLIST_ADD_FAILED'); Feedback::flash('SECURITY', 'BLACKLIST_ADD_FAILED');
} else { } else {
Feedback::flash('SECURITY', 'BLACKLIST_ADD_SUCCESS'); Feedback::flash('SECURITY', 'BLACKLIST_ADD_SUCCESS');
@ -127,7 +122,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
break; break;
case 'remove_blacklist': case 'remove_blacklist':
if (!$userObject->hasRight($user_id, 'superuser') && !$userObject->hasRight($user_id, 'edit blacklist')) { if (!$userObject->hasRight($userId, 'superuser') && !$userObject->hasRight($userId, 'edit blacklist')) {
Feedback::flash('SECURITY', 'PERMISSION_DENIED'); Feedback::flash('SECURITY', 'PERMISSION_DENIED');
break; break;
} }
@ -141,7 +136,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
]; ];
if ($validator->validate($rules)) { if ($validator->validate($rules)) {
if (!$rateLimiter->removeFromBlacklist($_POST['ip_address'], $currentUser, $user_id)) { if (!$rateLimiter->removeFromBlacklist($_POST['ip_address'], $currentUser, $userId)) {
Feedback::flash('SECURITY', 'BLACKLIST_REMOVE_FAILED'); Feedback::flash('SECURITY', 'BLACKLIST_REMOVE_FAILED');
} else { } else {
Feedback::flash('SECURITY', 'BLACKLIST_REMOVE_SUCCESS'); Feedback::flash('SECURITY', 'BLACKLIST_REMOVE_SUCCESS');

View File

@ -31,7 +31,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// Apply rate limiting for profile operations // Apply rate limiting for profile operations
require_once '../app/includes/rate_limit_middleware.php'; require_once '../app/includes/rate_limit_middleware.php';
checkRateLimit($dbWeb, 'profile', $user_id); checkRateLimit($dbWeb, 'profile', $userId);
// Get hash from URL if present // Get hash from URL if present
$hash = parse_url($_SERVER['REQUEST_URI'], PHP_URL_FRAGMENT) ?? ''; $hash = parse_url($_SERVER['REQUEST_URI'], PHP_URL_FRAGMENT) ?? '';
@ -170,7 +170,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
* Handles GET requests to display templates. * Handles GET requests to display templates.
*/ */
if ($userObject->hasRight($user_id, 'view settings')) { if ($userObject->hasRight($userId, 'view settings')) {
$jilo_agent_types = $agentObject->getAgentTypes(); $jilo_agent_types = $agentObject->getAgentTypes();
include '../app/templates/settings.php'; include '../app/templates/settings.php';
} else { } else {

View File

@ -17,7 +17,7 @@
<i class="fas fa-wrench me-2 text-secondary"></i> <i class="fas fa-wrench me-2 text-secondary"></i>
<?= htmlspecialchars($config['site_name']) ?> app configuration <?= htmlspecialchars($config['site_name']) ?> app configuration
</h5> </h5>
<?php if ($userObject->hasRight($user_id, 'edit config file')) { ?> <?php if ($userObject->hasRight($userId, 'edit config file')) { ?>
<div> <div>
<button type="button" class="btn btn-outline-primary btn-sm toggle-edit" <?= !$isWritable ? 'disabled' : '' ?>> <button type="button" class="btn btn-outline-primary btn-sm toggle-edit" <?= !$isWritable ? 'disabled' : '' ?>>
<i class="fas fa-edit me-2"></i>Edit <i class="fas fa-edit me-2"></i>Edit
@ -37,7 +37,7 @@
<div class="card-body p-4"> <div class="card-body p-4">
<form id="configForm"> <form id="configForm">
<?php <?php
include 'csrf_token.php'; include CSRF_TOKEN_INCLUDE;
function renderConfigItem($key, $value, $path = '') { function renderConfigItem($key, $value, $path = '') {
$fullPath = $path ? $path . '[' . $key . ']' : $key; $fullPath = $path ? $path . '[' . $key . ']' : $key;

View File

@ -4,7 +4,7 @@
<div class="card-body"> <div class="card-body">
<p class="card-text"><strong>Welcome to <?= htmlspecialchars($config['site_name']); ?>!</strong><br />Please enter login credentials:</p> <p class="card-text"><strong>Welcome to <?= htmlspecialchars($config['site_name']); ?>!</strong><br />Please enter login credentials:</p>
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=login"> <form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=login">
<?php include 'csrf_token.php'; ?> <?php include CSRF_TOKEN_INCLUDE; ?>
<div class="form-group mb-3"> <div class="form-group mb-3">
<input type="text" class="form-control w-50 mx-auto" name="username" placeholder="Username" <input type="text" class="form-control w-50 mx-auto" name="username" placeholder="Username"
pattern="[A-Za-z0-9_\-]{3,20}" title="3-20 characters, letters, numbers, - and _" pattern="[A-Za-z0-9_\-]{3,20}" title="3-20 characters, letters, numbers, - and _"
@ -12,7 +12,7 @@
</div> </div>
<div class="form-group mb-3"> <div class="form-group mb-3">
<input type="password" class="form-control w-50 mx-auto" name="password" placeholder="Password" <input type="password" class="form-control w-50 mx-auto" name="password" placeholder="Password"
pattern=".{5,}" title="Eight or more characters" pattern=".{8,}" title="Eight or more characters"
required /> required />
</div> </div>
<div class="form-group mb-3"> <div class="form-group mb-3">

View File

@ -8,7 +8,7 @@
<p>Enter your email address and we will send you<br /> <p>Enter your email address and we will send you<br />
instructions to reset your password.</p> instructions to reset your password.</p>
<form method="post" action="?page=login&action=forgot"> <form method="post" action="?page=login&action=forgot">
<?php include 'csrf_token.php'; ?> <?php include CSRF_TOKEN_INCLUDE; ?>
<div class="form-group"> <div class="form-group">
<label for="email">email address:</label> <label for="email">email address:</label>
<input type="email" <input type="email"

View File

@ -6,7 +6,7 @@
<div class="card-body"> <div class="card-body">
<h3 class="card-title mb-4">Set new password</h3> <h3 class="card-title mb-4">Set new password</h3>
<form method="post" action="?page=login&action=reset&token=<?= htmlspecialchars(urlencode($token)) ?>"> <form method="post" action="?page=login&action=reset&token=<?= htmlspecialchars(urlencode($token)) ?>">
<?php include 'csrf_token.php'; ?> <?php include CSRF_TOKEN_INCLUDE; ?>
<div class="form-group"> <div class="form-group">
<label for="new_password">new password:</label> <label for="new_password">new password:</label>
<input type="password" <input type="password"

View File

@ -12,7 +12,7 @@
</div> </div>
<?php if (isset($currentUser) && $page !== 'logout') { ?> <?php if (Session::getUsername() && $page !== 'logout') { ?>
<script src="static/js/sidebar.js"></script> <script src="static/js/sidebar.js"></script>
<?php } ?> <?php } ?>

View File

@ -10,7 +10,7 @@
<link rel="stylesheet" type="text/css" href="<?= htmlspecialchars($app_root) ?>static/css/main.css"> <link rel="stylesheet" type="text/css" href="<?= htmlspecialchars($app_root) ?>static/css/main.css">
<link rel="stylesheet" type="text/css" href="<?= htmlspecialchars($app_root) ?>static/css/messages.css"> <link rel="stylesheet" type="text/css" href="<?= htmlspecialchars($app_root) ?>static/css/messages.css">
<script src="<?= htmlspecialchars($app_root) ?>static/js/messages.js"></script> <script src="<?= htmlspecialchars($app_root) ?>static/js/messages.js"></script>
<?php if (isset($currentUser)) { ?> <?php if (Session::getUsername()) { ?>
<script> <script>
// restore sidebar state before the page is rendered // restore sidebar state before the page is rendered
(function () { (function () {

View File

@ -18,7 +18,7 @@
version&nbsp;<?= htmlspecialchars($config['version']) ?> version&nbsp;<?= htmlspecialchars($config['version']) ?>
</li> </li>
<?php if (isset($_SESSION['username']) && isset($_SESSION['user_id'])) { ?> <?php if (Session::isValidSession()) { ?>
<?php foreach ($platformsAll as $platform) { <?php foreach ($platformsAll as $platform) {
$platform_switch_url = switchPlatform($platform['id']); $platform_switch_url = switchPlatform($platform['id']);
@ -40,7 +40,7 @@
</ul> </ul>
<ul class="menu-right"> <ul class="menu-right">
<?php if (isset($_SESSION['username']) && isset($_SESSION['user_id'])) { ?> <?php if (Session::isValidSession()) { ?>
<li class="dropdown"> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
<i class="fas fa-user"></i> <i class="fas fa-user"></i>
@ -59,10 +59,48 @@
</a> </a>
</div> </div>
</li> </li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
<i class="fas fa-cog"></i>
</a>
<div class="dropdown-menu dropdown-menu-right">
<h6 class="dropdown-header">system</h6>
<?php if ($userObject->hasRight($userId, 'view config file')) {?>
<a class="dropdown-item" href="<?= htmlspecialchars($app_root) ?>?page=config">
<i class="fas fa-wrench"></i>Configuration
</a>
<?php } ?>
<?php if ($userObject->hasRight($userId, 'superuser') ||
$userObject->hasRight($userId, 'edit whitelist') ||
$userObject->hasRight($userId, 'edit blacklist') ||
$userObject->hasRight($userId, 'edit ratelimiting')) { ?>
<a class="dropdown-item" href="<?= htmlspecialchars($app_root) ?>?page=security">
<i class="fas fa-shield-alt"></i>Security
</a>
<?php } ?>
<?php if ($userObject->hasRight($userId, 'view app logs')) {?>
<a class="dropdown-item" href="<?= htmlspecialchars($app_root) ?>?page=logs">
<i class="fas fa-list"></i>Logs
</a>
<?php } ?>
</div>
</li>
<?php } else { ?> <?php } else { ?>
<li><a href="<?= htmlspecialchars($app_root) ?>?page=login">login</a></li> <li><a href="<?= htmlspecialchars($app_root) ?>?page=login">login</a></li>
<li><a href="<?= htmlspecialchars($app_root) ?>?page=register">register</a></li> <?php do_hook('main_public_menu', ['app_root' => $app_root, 'section' => 'main', 'position' => 100]); ?>
<?php } ?> <?php } ?>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
<i class="fas fa-info-circle"></i>
</a>
<div class="dropdown-menu dropdown-menu-right">
<h6 class="dropdown-header">resources</h6>
<a class="dropdown-item" href="<?= htmlspecialchars($app_root) ?>?page=help">
<i class="fas fa-question-circle"></i>Help
</a>
</div>
</li>
</ul> </ul>
</div> </div>
<!-- /Menu --> <!-- /Menu -->

View File

@ -72,45 +72,11 @@ $timeNow = new DateTime('now', new DateTimeZone($userTimezone));
<i class="fas fa-cog" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="jilo settings"></i>settings <i class="fas fa-cog" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="jilo settings"></i>settings
</li> </li>
</a> </a>
<li class="list-group-item bg-light" style="border: none;"><p class="text-end mb-0"><small>system</small></p></li>
<?php if ($userObject->hasRight($user_id, 'view config file')) {?>
<a href="<?= htmlspecialchars($app_root) ?>?page=config">
<li class="list-group-item<?php if ($page === 'config') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<i class="fas fa-wrench" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="app config"></i>config
</li>
</a>
<?php } ?>
<?php if ($userObject->hasRight($user_id, 'superuser') ||
$userObject->hasRight($user_id, 'edit whitelist') ||
$userObject->hasRight($user_id, 'edit blacklist') ||
$userObject->hasRight($user_id, 'edit ratelimiting')) { ?>
<a href="<?= htmlspecialchars($app_root) ?>?page=security">
<li class="list-group-item<?php if ($page === 'security') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<i class="fas fa-shield-alt" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="security"></i>security
</li>
</a>
<?php } ?>
<a href="<?= htmlspecialchars($app_root) ?>?page=status"> <a href="<?= htmlspecialchars($app_root) ?>?page=status">
<li class="list-group-item<?php if ($page === 'status' && $item === '') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>"> <li class="list-group-item<?php if ($page === 'status' && $item === '') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<i class="fas fa-heartbeat" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="status"></i>status <i class="fas fa-heartbeat" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="status"></i>status
</li> </li>
</a> </a>
<?php if ($userObject->hasRight($user_id, 'view app logs')) {?>
<a href="<?= htmlspecialchars($app_root) ?>?page=logs">
<li class="list-group-item<?php if ($page === 'logs') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<i class="fas fa-shoe-prints" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="logs"></i>logs
</li>
</a>
<?php } ?>
<a href="<?= htmlspecialchars($app_root) ?>?page=help">
<li class="list-group-item<?php if ($page === 'help') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<i class="fas fa-question-circle" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="help"></i>help
</li>
</a>
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -6,11 +6,14 @@
* $totalPages - Total number of pages * $totalPages - Total number of pages
*/ */
// Ensure required variables are set // Validate required pagination variables
if (!isset($currentPage) || !isset($totalPages)) { if (!isset($currentPage) || !isset($totalPages)) {
return; return;
} }
// Ensure valid values
$currentPage = max(1, min($currentPage, $totalPages));
// Number of page links to show before and after current page // Number of page links to show before and after current page
$range = 2; $range = 2;
?> ?>

View File

@ -6,6 +6,7 @@
<div class="card-body"> <div class="card-body">
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=profile" enctype="multipart/form-data"> <form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=profile" enctype="multipart/form-data">
<?php include CSRF_TOKEN_INCLUDE; ?>
<div class="row"> <div class="row">
<p class="border rounded bg-light mb-4"><small>edit the profile fields</small></p> <p class="border rounded bg-light mb-4"><small>edit the profile fields</small></p>
<div class="col-md-4 avatar-container"> <div class="col-md-4 avatar-container">
@ -132,6 +133,7 @@
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button> <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<form id="remove-avatar-form" data-action="remove-avatar" method="POST" action="<?= htmlspecialchars($app_root) ?>?page=profile&action=remove&item=avatar"> <form id="remove-avatar-form" data-action="remove-avatar" method="POST" action="<?= htmlspecialchars($app_root) ?>?page=profile&action=remove&item=avatar">
<?php include CSRF_TOKEN_INCLUDE; ?>
<button type="button" class="btn btn-danger" id="confirm-delete">Delete Avatar</button> <button type="button" class="btn btn-danger" id="confirm-delete">Delete Avatar</button>
</form> </form>
</div> </div>

View File

@ -5,17 +5,17 @@
<h2 class="mb-0">Security settings</h2> <h2 class="mb-0">Security settings</h2>
<small>network restrictions to control flooding and brute force attacks</small> <small>network restrictions to control flooding and brute force attacks</small>
<ul class="nav nav-tabs mt-5"> <ul class="nav nav-tabs mt-5">
<?php if ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit whitelist')) { ?> <?php if ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, 'edit whitelist')) { ?>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link <?= $section === 'whitelist' ? 'active' : '' ?>" href="?page=security&section=whitelist">IP whitelist</a> <a class="nav-link <?= $section === 'whitelist' ? 'active' : '' ?>" href="?page=security&section=whitelist">IP whitelist</a>
</li> </li>
<?php } ?> <?php } ?>
<?php if ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit blacklist')) { ?> <?php if ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, 'edit blacklist')) { ?>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link <?= $section === 'blacklist' ? 'active' : '' ?>" href="?page=security&section=blacklist">IP blacklist</a> <a class="nav-link <?= $section === 'blacklist' ? 'active' : '' ?>" href="?page=security&section=blacklist">IP blacklist</a>
</li> </li>
<?php } ?> <?php } ?>
<?php if ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit ratelimiting')) { ?> <?php if ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, 'edit ratelimiting')) { ?>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link <?= $section === 'ratelimit' ? 'active' : '' ?>" href="?page=security&section=ratelimit">Rate limiting</a> <a class="nav-link <?= $section === 'ratelimit' ? 'active' : '' ?>" href="?page=security&section=ratelimit">Rate limiting</a>
</li> </li>
@ -24,7 +24,7 @@
</div> </div>
</div> </div>
<?php if ($section === 'whitelist' && ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit whitelist'))) { ?> <?php if ($section === 'whitelist' && ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, 'edit whitelist'))) { ?>
<!-- whitelist section --> <!-- whitelist section -->
<div class="row mb-4"> <div class="row mb-4">
<div class="col"> <div class="col">
@ -35,7 +35,7 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="POST" class="mb-4"> <form method="POST" class="mb-4">
<?php include 'csrf_token.php'; ?> <?php include CSRF_TOKEN_INCLUDE; ?>
<input type="hidden" name="action" value="add_whitelist"> <input type="hidden" name="action" value="add_whitelist">
<div class="row g-3"> <div class="row g-3">
<div class="col-md-4"> <div class="col-md-4">
@ -77,7 +77,7 @@
<td><?= htmlspecialchars($ip['created_at']) ?></td> <td><?= htmlspecialchars($ip['created_at']) ?></td>
<td> <td>
<form method="POST" style="display: inline;"> <form method="POST" style="display: inline;">
<?php include 'csrf_token.php'; ?> <?php include CSRF_TOKEN_INCLUDE; ?>
<input type="hidden" name="action" value="remove_whitelist"> <input type="hidden" name="action" value="remove_whitelist">
<input type="hidden" name="ip_address" value="<?= htmlspecialchars($ip['ip_address']) ?>"> <input type="hidden" name="ip_address" value="<?= htmlspecialchars($ip['ip_address']) ?>">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to remove this IP from whitelist?')">Remove</button> <button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to remove this IP from whitelist?')">Remove</button>
@ -93,7 +93,7 @@
</div> </div>
<?php } ?> <?php } ?>
<?php if ($section === 'blacklist' && ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit blacklist'))) { ?> <?php if ($section === 'blacklist' && ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, 'edit blacklist'))) { ?>
<!-- blacklist section --> <!-- blacklist section -->
<div class="row mb-4"> <div class="row mb-4">
<div class="col"> <div class="col">
@ -104,7 +104,7 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="POST" class="mb-4"> <form method="POST" class="mb-4">
<?php include 'csrf_token.php'; ?> <?php include CSRF_TOKEN_INCLUDE; ?>
<input type="hidden" name="action" value="add_blacklist"> <input type="hidden" name="action" value="add_blacklist">
<div class="row g-3"> <div class="row g-3">
<div class="col-md-3"> <div class="col-md-3">
@ -151,7 +151,7 @@
<td><?= $ip['expiry_time'] ? htmlspecialchars($ip['expiry_time']) : 'Never' ?></td> <td><?= $ip['expiry_time'] ? htmlspecialchars($ip['expiry_time']) : 'Never' ?></td>
<td> <td>
<form method="POST" style="display: inline;"> <form method="POST" style="display: inline;">
<?php include 'csrf_token.php'; ?> <?php include CSRF_TOKEN_INCLUDE; ?>
<input type="hidden" name="action" value="remove_blacklist"> <input type="hidden" name="action" value="remove_blacklist">
<input type="hidden" name="ip_address" value="<?= htmlspecialchars($ip['ip_address']) ?>"> <input type="hidden" name="ip_address" value="<?= htmlspecialchars($ip['ip_address']) ?>">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to remove this IP from blacklist?')">Remove</button> <button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to remove this IP from blacklist?')">Remove</button>
@ -167,7 +167,7 @@
</div> </div>
<?php } ?> <?php } ?>
<?php if ($section === 'ratelimit' && ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit ratelimiting'))) { ?> <?php if ($section === 'ratelimit' && ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, 'edit ratelimiting'))) { ?>
<!-- rate limiting section --> <!-- rate limiting section -->
<div class="row mb-4"> <div class="row mb-4">
<div class="col"> <div class="col">

View File

@ -57,7 +57,7 @@
<button type="button" class="btn btn-outline-secondary cancel-edit platform-edit-mode" style="display: none;"> <button type="button" class="btn btn-outline-secondary cancel-edit platform-edit-mode" style="display: none;">
<i class="fas fa-times me-1"></i>Cancel <i class="fas fa-times me-1"></i>Cancel
</button> </button>
<?php if ($userObject->hasRight($user_id, 'delete platform')): ?> <?php if ($userObject->hasRight($userId, 'delete platform')): ?>
<button type="button" class="btn btn-outline-danger platform-view-mode" onclick="showDeletePlatformModal(<?= htmlspecialchars($platform['id']) ?>, '<?= htmlspecialchars(addslashes($platform['name'])) ?>', '<?= htmlspecialchars(addslashes($platform['jitsi_url'])) ?>', '<?= htmlspecialchars(addslashes($platform['jilo_database'])) ?>')"> <button type="button" class="btn btn-outline-danger platform-view-mode" onclick="showDeletePlatformModal(<?= htmlspecialchars($platform['id']) ?>, '<?= htmlspecialchars(addslashes($platform['name'])) ?>', '<?= htmlspecialchars(addslashes($platform['jitsi_url'])) ?>', '<?= htmlspecialchars(addslashes($platform['jilo_database'])) ?>')">
<i class="fas fa-trash me-1"></i>Delete platform <i class="fas fa-trash me-1"></i>Delete platform
</button> </button>

View File

@ -1,4 +1,3 @@
CREATE TABLE users ( CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE,
@ -25,43 +24,95 @@ CREATE TABLE rights (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE name TEXT NOT NULL UNIQUE
); );
INSERT INTO rights VALUES(1,'superuser'); CREATE TABLE IF NOT EXISTS "jilo_agent_types" (
INSERT INTO rights VALUES(2,'edit users'); id INTEGER PRIMARY KEY AUTOINCREMENT,
INSERT INTO rights VALUES(3,'view settings'); description TEXT,
INSERT INTO rights VALUES(4,'edit settings'); endpoint TEXT
INSERT INTO rights VALUES(5,'view config file'); );
INSERT INTO rights VALUES(6,'edit config file');
INSERT INTO rights VALUES(7,'view own profile');
INSERT INTO rights VALUES(8,'edit own profile');
INSERT INTO rights VALUES(9,'view all profiles');
INSERT INTO rights VALUES(10,'edit all profiles');
INSERT INTO rights VALUES(11,'view app logs');
INSERT INTO rights VALUES(12,'view all platforms');
INSERT INTO rights VALUES(13,'edit all platforms');
INSERT INTO rights VALUES(14,'view all agents');
INSERT INTO rights VALUES(15,'edit all agents');
CREATE TABLE platforms ( CREATE TABLE platforms (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE,
jitsi_url TEXT NOT NULL, jitsi_url TEXT NOT NULL,
jilo_database TEXT NOT NULL jilo_database TEXT NOT NULL
); );
CREATE TABLE hosts (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
address TEXT NOT NULL,
platform_id INTEGER NOT NULL,
name TEXT,
FOREIGN KEY(platform_id) REFERENCES platforms(id)
);
CREATE TABLE jilo_agents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
host_id INTEGER NOT NULL,
agent_type_id INTEGER NOT NULL,
url TEXT NOT NULL,
secret_key TEXT,
check_period INTEGER DEFAULT 0,
FOREIGN KEY(agent_type_id) REFERENCES jilo_agent_types(id),
FOREIGN KEY(host_id) REFERENCES hosts(id)
);
CREATE TABLE jilo_agent_checks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
agent_id INTEGER,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
status_code INTEGER,
response_time_ms INTEGER,
response_content TEXT,
FOREIGN KEY(agent_id) REFERENCES jilo_agents(id)
);
CREATE TABLE ip_whitelist (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL UNIQUE,
is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)),
description TEXT,
created_at TEXT DEFAULT (DATETIME('now')),
created_by TEXT
);
CREATE TABLE ip_blacklist (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL UNIQUE,
is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)),
reason TEXT,
expiry_time TEXT NULL,
created_at TEXT DEFAULT (DATETIME('now')),
created_by TEXT
);
CREATE TABLE logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
time TEXT DEFAULT (DATETIME('now')),
scope TEXT NOT NULL,
message TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id)
);
CREATE TABLE pages_rate_limits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL,
endpoint TEXT NOT NULL,
request_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE login_attempts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL,
username TEXT NOT NULL,
attempted_at TEXT DEFAULT (DATETIME('now'))
);
CREATE TABLE user_2fa ( CREATE TABLE user_2fa (
user_id INTEGER NOT NULL PRIMARY KEY, user_id INTEGER NOT NULL PRIMARY KEY,
secret_key TEXT NOT NULL, secret_key TEXT NOT NULL,
backup_codes TEXT, backup_codes TEXT,
enabled INTEGER NOT NULL DEFAULT 0, enabled INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL, created_at TEXT NOT NULL,
last_used TEXT, last_used TEXT,
FOREIGN KEY (user_id) REFERENCES users(id) FOREIGN KEY (user_id) REFERENCES users(id)
); );
CREATE TABLE user_2fa_temp ( CREATE TABLE user_2fa_temp (
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL PRIMARY KEY,
code TEXT NOT NULL, code TEXT NOT NULL,
created_at TEXT NOT NULL, created_at TEXT NOT NULL,
expires_at TEXT NOT NULL, expires_at TEXT NOT NULL,
PRIMARY KEY (user_id, code), FOREIGN KEY (user_id) REFERENCES users(id)
FOREIGN KEY (user_id) REFERENCES users(id)
); );
CREATE TABLE user_password_reset ( CREATE TABLE user_password_reset (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -70,85 +121,5 @@ CREATE TABLE user_password_reset (
expires INTEGER NOT NULL, expires INTEGER NOT NULL,
used INTEGER NOT NULL DEFAULT 0, used INTEGER NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
); );
CREATE TABLE logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
time TEXT DEFAULT (DATETIME('now')),
scope TEXT NOT NULL,
message TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS "jilo_agent_types" (
"id" INTEGER,
"description" TEXT,
"endpoint" TEXT,
PRIMARY KEY("id" AUTOINCREMENT)
);
INSERT INTO jilo_agent_types VALUES(1,'jvb','/jvb');
INSERT INTO jilo_agent_types VALUES(2,'jicofo','/jicofo');
INSERT INTO jilo_agent_types VALUES(3,'prosody','/prosody');
INSERT INTO jilo_agent_types VALUES(4,'nginx','/nginx');
INSERT INTO jilo_agent_types VALUES(5,'jibri','/jibri');
CREATE TABLE jilo_agent_checks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
agent_id INTEGER,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
status_code INTEGER,
response_time_ms INTEGER,
response_content TEXT,
FOREIGN KEY(agent_id) REFERENCES jilo_agents(id)
);
CREATE TABLE IF NOT EXISTS "jilo_agents" (
"id" INTEGER,
"host_id" INTEGER NOT NULL,
"agent_type_id" INTEGER NOT NULL,
"url" TEXT NOT NULL,
"secret_key" TEXT,
"check_period" INTEGER DEFAULT 0,
PRIMARY KEY("id" AUTOINCREMENT),
FOREIGN KEY("agent_type_id") REFERENCES "jilo_agent_types"("id"),
FOREIGN KEY("host_id") REFERENCES "hosts"("id")
);
CREATE TABLE IF NOT EXISTS "hosts" (
"id" INTEGER NOT NULL,
"address" TEXT NOT NULL,
"platform_id" INTEGER NOT NULL,
"name" TEXT,
PRIMARY KEY("id" AUTOINCREMENT),
FOREIGN KEY("platform_id") REFERENCES "platforms"("id")
);
CREATE TABLE login_attempts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL,
username TEXT NOT NULL,
attempted_at TEXT DEFAULT (DATETIME('now'))
);
CREATE TABLE ip_whitelist (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL UNIQUE,
is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)),
description TEXT,
created_at TEXT DEFAULT (DATETIME('now')),
created_by TEXT
);
INSERT INTO ip_whitelist VALUES(1,'127.0.0.1',0,'localhost IPv4','2025-01-04 11:39:08','system');
INSERT INTO ip_whitelist VALUES(2,'::1',0,'localhost IPv6','2025-01-04 11:39:08','system');
INSERT INTO ip_whitelist VALUES(3,'10.0.0.0/8',1,'Private network (Class A)','2025-01-04 11:39:08','system');
INSERT INTO ip_whitelist VALUES(4,'172.16.0.0/12',1,'Private network (Class B)','2025-01-04 11:39:08','system');
INSERT INTO ip_whitelist VALUES(5,'192.168.0.0/16',1,'Private network (Class C)','2025-01-04 11:39:08','system');
CREATE TABLE ip_blacklist (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL UNIQUE,
is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)),
reason TEXT,
expiry_time TEXT NULL,
created_at TEXT DEFAULT (DATETIME('now')),
created_by TEXT
);
INSERT INTO ip_blacklist VALUES(1,'0.0.0.0/8',1,'Reserved address space - RFC 1122',NULL,'2025-01-04 11:39:08','system');
INSERT INTO ip_blacklist VALUES(2,'100.64.0.0/10',1,'Carrier-grade NAT space - RFC 6598',NULL,'2025-01-04 11:39:08','system');
INSERT INTO ip_blacklist VALUES(3,'192.0.2.0/24',1,'TEST-NET-1 Documentation space - RFC 5737',NULL,'2025-01-04 11:39:08','system');
INSERT INTO ip_blacklist VALUES(4,'198.51.100.0/24',1,'TEST-NET-2 Documentation space - RFC 5737',NULL,'2025-01-04 11:39:08','system');
INSERT INTO ip_blacklist VALUES(5,'203.0.113.0/24',1,'TEST-NET-3 Documentation space - RFC 5737',NULL,'2025-01-04 11:39:08','system');

View File

@ -5,24 +5,38 @@ INSERT INTO users VALUES(2,'demo1','$2y$10$LtV9m.rMCJ.K/g45e6tzDexZ8C/9xxu3qFCkv
INSERT INTO users_meta VALUES(1,1,'demo admin user','admin@example.com',NULL,NULL,'This is a demo user of the demo install of Jilo Web'); INSERT INTO users_meta VALUES(1,1,'demo admin user','admin@example.com',NULL,NULL,'This is a demo user of the demo install of Jilo Web');
INSERT INTO users_meta VALUES(2,2,'demo user','demo@example.com',NULL,NULL,'This is a demo user of the demo install of Jilo Web'); INSERT INTO users_meta VALUES(2,2,'demo user','demo@example.com',NULL,NULL,'This is a demo user of the demo install of Jilo Web');
INSERT INTO platforms VALUES(1,'meet.lindeas.com','https://meet.lindeas.com','../jilo-meet.lindeas.db'); INSERT INTO rights VALUES(1,'superuser');
INSERT INTO platforms VALUES(2,'example.com','https://meet.example.com','../jilo.db'); INSERT INTO rights VALUES(2,'edit users');
INSERT INTO rights VALUES(3,'view settings');
INSERT INTO rights VALUES(4,'edit settings');
INSERT INTO rights VALUES(5,'view own profile');
INSERT INTO rights VALUES(6,'edit own profile');
INSERT INTO rights VALUES(7,'view all profiles');
INSERT INTO rights VALUES(8,'edit all profiles');
INSERT INTO rights VALUES(9,'view app logs');
INSERT INTO rights VALUES(10,'manage plugins');
INSERT INTO rights VALUES(11,'view all platforms');
INSERT INTO rights VALUES(12,'edit all platforms');
INSERT INTO rights VALUES(13,'view all agents');
INSERT INTO rights VALUES(14,'edit all agents');
INSERT INTO rights VALUES(15,'view jilo config');
INSERT INTO logs VALUES(1,2,'2024-09-30 09:54:50','user','Logout: User "demo" logged out. IP: 151.237.101.43'); INSERT INTO jilo_agent_types VALUES(1,'jvb','/jvb');
INSERT INTO logs VALUES(2,2,'2024-09-30 09:54:54','user','Login: User "demo" logged in. IP: 151.237.101.43'); INSERT INTO jilo_agent_types VALUES(2,'jicofo','/jicofo');
INSERT INTO logs VALUES(3,2,'2024-10-03 16:34:49','user','Logout: User "demo" logged out. IP: 151.237.101.43'); INSERT INTO jilo_agent_types VALUES(3,'prosody','/prosody');
INSERT INTO logs VALUES(4,2,'2024-10-03 16:34:56','user','Login: User "demo" logged in. IP: 151.237.101.43'); INSERT INTO jilo_agent_types VALUES(4,'nginx','/nginx');
INSERT INTO logs VALUES(5,2,'2024-10-09 11:08:16','user','Logout: User "demo" logged out. IP: 151.237.101.43'); INSERT INTO jilo_agent_types VALUES(5,'jibri','/jibri');
INSERT INTO logs VALUES(6,2,'2024-10-09 11:08:20','user','Login: User "demo" logged in. IP: 151.237.101.43');
INSERT INTO logs VALUES(7,2,'2024-10-17 16:22:57','user','Logout: User "demo" logged out. IP: 151.237.101.43');
INSERT INTO logs VALUES(8,2,'2024-10-17 16:23:08','user','Login: User "demo" logged in. IP: 151.237.101.43');
INSERT INTO logs VALUES(9,2,'2024-10-18 08:07:25','user','Login: User "demo" logged in. IP: 42.104.201.119');
INSERT INTO jilo_agents VALUES(1,1,1,'https://meet.lindeas.com:8081','mysecretkey',5); INSERT INTO platforms VALUES(1,'example.com','https://meet.example.com','../../jilo/jilo.db');
INSERT INTO jilo_agents VALUES(4,1,2,'https://meet.lindeas.com:8081','mysecretkey',5);
INSERT INTO jilo_agents VALUES(7,1,3,'http://meet.lindeas.com:8081','mysecretkey',5);
INSERT INTO jilo_agents VALUES(8,1,4,'http://meet.lindeas.com:8081','mysecretkey',5);
INSERT INTO hosts VALUES(1,'meet.lindeas.com',2,'main machine'); INSERT INTO ip_whitelist VALUES(1,'127.0.0.1',0,'localhost IPv4','2025-01-04 11:39:08','system');
INSERT INTO hosts VALUES(2,'meet.example.com',2,'test'); INSERT INTO ip_whitelist VALUES(2,'::1',0,'localhost IPv6','2025-01-04 11:39:08','system');
INSERT INTO ip_whitelist VALUES(3,'10.0.0.0/8',1,'Private network (Class A)','2025-01-04 11:39:08','system');
INSERT INTO ip_whitelist VALUES(4,'172.16.0.0/12',1,'Private network (Class B)','2025-01-04 11:39:08','system');
INSERT INTO ip_whitelist VALUES(5,'192.168.0.0/16',1,'Private network (Class C)','2025-01-04 11:39:08','system');
INSERT INTO ip_blacklist VALUES(1,'0.0.0.0/8',1,'Reserved address space - RFC 1122',NULL,'2025-01-04 11:39:08','system');
INSERT INTO ip_blacklist VALUES(2,'100.64.0.0/10',1,'Carrier-grade NAT space - RFC 6598',NULL,'2025-01-04 11:39:08','system');
INSERT INTO ip_blacklist VALUES(3,'192.0.2.0/24',1,'TEST-NET-1 Documentation space - RFC 5737',NULL,'2025-01-04 11:39:08','system');
INSERT INTO ip_blacklist VALUES(4,'198.51.100.0/24',1,'TEST-NET-2 Documentation space - RFC 5737',NULL,'2025-01-04 11:39:08','system');
INSERT INTO ip_blacklist VALUES(5,'203.0.113.0/24',1,'TEST-NET-3 Documentation space - RFC 5737',NULL,'2025-01-04 11:39:08','system');

View File

@ -0,0 +1,22 @@
<?php
// Add to allowed URLs
register_hook('filter_allowed_urls', function($urls) {
$urls[] = 'register';
return $urls;
});
// Add to publicly accessible pages
register_hook('filter_public_pages', function($pages) {
$pages[] = 'register';
return $pages;
});
// Configuration for main menu injection
define('REGISTRATIONS_MAIN_MENU_SECTION', 'main');
define('REGISTRATIONS_MAIN_MENU_POSITION', 30);
register_hook('main_public_menu', function($ctx) {
$section = defined('REGISTRATIONS_MAIN_MENU_SECTION') ? REGISTRATIONS_MAIN_MENU_SECTION : 'main';
$position = defined('REGISTRATIONS_MAIN_MENU_POSITION') ? REGISTRATIONS_MAIN_MENU_POSITION : 100;
echo '<li><a href="?page=register">register</a></li>';
});

View File

@ -8,6 +8,15 @@
* 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.
*/ */
// Define plugin base path if not already defined
if (!defined('PLUGIN_REGISTER_PATH')) {
define('PLUGIN_REGISTER_PATH', dirname(__FILE__, 2) . '/');
}
require_once PLUGIN_REGISTER_PATH . 'models/register.php';
require_once dirname(__FILE__, 4) . '/app/classes/user.php';
require_once dirname(__FILE__, 4) . '/app/classes/validator.php';
require_once dirname(__FILE__, 4) . '/app/helpers/security.php';
// registration is allowed, go on // registration is allowed, go on
if ($config['registration_enabled'] == true) { if ($config['registration_enabled'] == true) {
@ -17,15 +26,13 @@ if ($config['registration_enabled'] == true) {
if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) { if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {
// Apply rate limiting // Apply rate limiting
require '../app/includes/rate_limit_middleware.php'; require_once dirname(__FILE__, 4) . '/app/includes/rate_limit_middleware.php';
checkRateLimit($dbWeb, 'register'); checkRateLimit($dbWeb, 'register');
require_once '../app/classes/validator.php';
require_once '../app/helpers/security.php';
$security = SecurityHelper::getInstance(); $security = SecurityHelper::getInstance();
// Sanitize input // Sanitize input
$formData = $security->sanitizeArray($_POST, ['username', 'password', 'confirm_password', 'csrf_token']); $formData = $security->sanitizeArray($_POST, ['username', 'password', 'confirm_password', 'csrf_token', 'terms']);
// Validate CSRF token // Validate CSRF token
if (!$security->verifyCsrfToken($formData['csrf_token'] ?? '')) { if (!$security->verifyCsrfToken($formData['csrf_token'] ?? '')) {
@ -47,6 +54,10 @@ if ($config['registration_enabled'] == true) {
'confirm_password' => [ 'confirm_password' => [
'required' => true, 'required' => true,
'matches' => 'password' 'matches' => 'password'
],
'terms' => [
'required' => true,
'equals' => 'on'
] ]
]; ];
@ -56,41 +67,42 @@ if ($config['registration_enabled'] == true) {
$password = $formData['password']; $password = $formData['password'];
// registering // registering
$result = $userObject->register($username, $password); $register = new Register($dbWeb);
$result = $register->register($username, $password);
// redirect to login // redirect to login
if ($result === true) { if ($result === true) {
// Get the new user's ID for logging // Get the new user's ID for logging
$user_id = $userObject->getUserId($username)[0]['id']; $userId = $userObject->getUserId($username)[0]['id'];
$logObject->insertLog($user_id, "Registration: New user \"$username\" registered successfully. IP: $user_IP", 'user'); $logObject->insertLog($userId, "Registration: New user \"$username\" registered successfully. IP: $user_IP", 'user');
Feedback::flash('NOTICE', 'DEFAULT', "Registration successful. You can log in now."); Feedback::flash('NOTICE', 'DEFAULT', "Registration successful. You can log in now.");
header('Location: ' . htmlspecialchars($app_root)); header('Location: ' . htmlspecialchars($app_root . '?page=login'));
exit(); exit();
// registration fail, redirect to login // registration fail, redirect to login
} else { } else {
$logObject->insertLog(0, "Registration: Failed registration attempt for user \"$username\". IP: $user_IP. Reason: $result", 'system'); $logObject->insertLog(null, "Registration: Failed registration attempt for user \"$username\". IP: $user_IP. Reason: $result", 'system');
Feedback::flash('ERROR', 'DEFAULT', "Registration failed. $result"); Feedback::flash('ERROR', 'DEFAULT', "Registration failed. $result");
header('Location: ' . htmlspecialchars($app_root)); header('Location: ' . htmlspecialchars($app_root . '?page=register'));
exit(); exit();
} }
} else { } else {
$error = $validator->getFirstError(); $error = $validator->getFirstError();
$logObject->insertLog(0, "Registration: Failed validation for user \"" . ($username ?? 'unknown') . "\". IP: $user_IP. Reason: $error", 'system'); $logObject->insertLog(null, "Registration: Failed validation for user \"" . ($username ?? 'unknown') . "\". IP: $user_IP. Reason: $error", 'system');
Feedback::flash('ERROR', 'DEFAULT', $error); Feedback::flash('ERROR', 'DEFAULT', $error);
header('Location: ' . htmlspecialchars($app_root . '?page=register')); header('Location: ' . htmlspecialchars($app_root . '?page=register'));
exit(); exit();
} }
} }
} catch (Exception $e) { } catch (Exception $e) {
$logObject->insertLog(0, "Registration: System error. IP: $user_IP. Error: " . $e->getMessage(), 'system'); $logObject->insertLog(null, "Registration: System error. IP: $user_IP. Error: " . $e->getMessage(), 'system');
Feedback::flash('ERROR', 'DEFAULT', $e->getMessage()); Feedback::flash('ERROR', 'DEFAULT', $e->getMessage());
} }
// Get any new feedback messages // Get any new feedback messages
include '../app/helpers/feedback.php'; include dirname(__FILE__, 4) . '/app/helpers/feedback.php';
// Load the template // Load the template
include '../app/templates/form-register.php'; include PLUGIN_REGISTER_PATH . 'views/form-register.php';
// registration disabled // registration disabled
} else { } else {

View File

@ -0,0 +1,92 @@
<?php
/**
* class Register
*
* Handles user registration.
*/
class Register {
/**
* @var PDO|null $db The database connection instance.
*/
private $db;
private $rateLimiter;
private $twoFactorAuth;
/**
* Register constructor.
* Initializes the database connection.
*
* @param object $database The database object to initialize the connection.
*/
public function __construct($database) {
if ($database instanceof PDO) {
$this->db = $database;
} else {
$this->db = $database->getConnection();
}
require_once dirname(__FILE__, 4) . '/app/classes/ratelimiter.php';
require_once dirname(__FILE__, 4) . '/app/classes/twoFactorAuth.php';
$this->rateLimiter = new RateLimiter($database);
$this->twoFactorAuth = new TwoFactorAuthentication($database);
}
/**
* Registers a new user.
*
* @param string $username The username of the new user.
* @param string $password The password for the new user.
*
* @return bool|string True if registration is successful, error message otherwise.
*/
public function register($username, $password) {
try {
// we have two inserts, start a transaction
$this->db->beginTransaction();
// hash the password, don't store it plain
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
// insert into users table
$sql = 'INSERT
INTO users (username, password)
VALUES (:username, :password)';
$query = $this->db->prepare($sql);
$query->bindValue(':username', $username);
$query->bindValue(':password', $hashedPassword);
// execute the first query
if (!$query->execute()) {
// rollback on error
$this->db->rollBack();
return false;
}
// insert the last user id into users_meta table
$sql2 = 'INSERT
INTO users_meta (user_id)
VALUES (:user_id)';
$query2 = $this->db->prepare($sql2);
$query2->bindValue(':user_id', $this->db->lastInsertId());
// execute the second query
if (!$query2->execute()) {
// rollback on error
$this->db->rollBack();
return false;
}
// if all is OK, commit the transaction
$this->db->commit();
return true;
} catch (Exception $e) {
// rollback on any error
$this->db->rollBack();
return $e->getMessage();
}
}
}

View File

@ -0,0 +1,6 @@
{
"name": "Registration Plugin",
"version": "1.0.0",
"enabled": true,
"description": "Provides registration functionality as a plugin."
}

View File

@ -4,7 +4,7 @@
<div class="card-body"> <div class="card-body">
<p class="card-text">Enter credentials for registration:</p> <p class="card-text">Enter credentials for registration:</p>
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=register"> <form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=register">
<?php include 'csrf_token.php'; ?> <?php include CSRF_TOKEN_INCLUDE; ?>
<div class="form-group mb-3"> <div class="form-group mb-3">
<input type="text" class="form-control w-50 mx-auto" name="username" placeholder="Username" <input type="text" class="form-control w-50 mx-auto" name="username" placeholder="Username"
pattern="[A-Za-z0-9_\-]{3,20}" title="3-20 characters, letters, numbers, - and _" pattern="[A-Za-z0-9_\-]{3,20}" title="3-20 characters, letters, numbers, - and _"
@ -20,6 +20,17 @@
pattern=".{8,}" title="Eight or more characters" pattern=".{8,}" title="Eight or more characters"
required /> required />
</div> </div>
<div class="form-group mb-3">
<div class="form-check">
<label class="form-check-label" for="terms">
<input type="checkbox" class="form-check-input" id="terms" name="terms" required>
I agree to the <a href="<?= htmlspecialchars($app_root) ?>?page=terms" target="_blank">terms & conditions</a> and <a href="<?= htmlspecialchars($app_root) ?>?page=privacy" target="_blank">privacy policy</a>
</label>
</div>
<small class="text-muted mt-2">
We use cookies to improve your experience. See our <a href="<?= htmlspecialchars($app_root) ?>?page=cookies" target="_blank">cookies policy</a>
</small>
</div>
<input type="submit" class="btn btn-primary" value="Register" /> <input type="submit" class="btn btn-primary" value="Register" />
</form> </form>
</div> </div>

View File

@ -11,18 +11,68 @@
* Version: 0.4 * Version: 0.4
*/ */
// Preparing plugins and hooks
$GLOBALS['plugin_hooks'] = [];
$enabled_plugins = [];
// Plugin discovery
$plugins_dir = dirname(__DIR__) . '/plugins/';
foreach (glob($plugins_dir . '*', GLOB_ONLYDIR) as $plugin_path) {
$manifest = $plugin_path . '/plugin.json';
if (file_exists($manifest)) {
$meta = json_decode(file_get_contents($manifest), true);
if (!empty($meta['enabled'])) {
$plugin_name = basename($plugin_path);
$enabled_plugins[$plugin_name] = [
'path' => $plugin_path,
'meta' => $meta
];
// Autoload plugin bootstrap if exists
$bootstrap = $plugin_path . '/bootstrap.php';
if (file_exists($bootstrap)) {
include_once $bootstrap;
}
}
}
}
$GLOBALS['enabled_plugins'] = $enabled_plugins;
// Simple hook system
function register_hook($hook, $callback) {
$GLOBALS['plugin_hooks'][$hook][] = $callback;
}
function do_hook($hook, $context = []) {
if (!empty($GLOBALS['plugin_hooks'][$hook])) {
foreach ($GLOBALS['plugin_hooks'][$hook] as $callback) {
call_user_func($callback, $context);
}
}
}
// Define CSRF token include path globally
if (!defined('CSRF_TOKEN_INCLUDE')) {
define('CSRF_TOKEN_INCLUDE', dirname(__DIR__) . '/app/includes/csrf_token.php');
}
// we start output buffering and // we start output buffering and
// flush it later only when there is no redirect // flush it later only when there is no redirect
ob_start(); ob_start();
// Start session before any session-dependent code
require_once '../app/classes/session.php';
Session::startSession();
// Apply security headers // Apply security headers
require_once '../app/includes/security_headers_middleware.php'; require_once '../app/includes/security_headers_middleware.php';
// 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';
session_name('jilo'); // Check session validity
session_start(); $validSession = Session::isValidSession();
// Get user ID early if session is valid
$userId = $validSession ? Session::getUserId() : null;
// Initialize feedback message system // Initialize feedback message system
require_once '../app/classes/feedback.php'; require_once '../app/classes/feedback.php';
@ -63,9 +113,21 @@ $allowed_urls = [
'login', 'login',
'logout', 'logout',
'register',
'about',
]; ];
// Let plugins filter/extend allowed_urls
function filter_allowed_urls($urls) {
if (!empty($GLOBALS['plugin_hooks']['filter_allowed_urls'])) {
foreach ($GLOBALS['plugin_hooks']['filter_allowed_urls'] as $callback) {
$urls = call_user_func($callback, $urls);
}
}
return $urls;
}
$allowed_urls = filter_allowed_urls($allowed_urls);
// cnfig file // cnfig file
// possible locations, in order of preference // possible locations, in order of preference
$config_file_locations = [ $config_file_locations = [
@ -92,17 +154,40 @@ if ($config_file) {
$app_root = $config['folder']; $app_root = $config['folder'];
// check if logged in // List of pages that don't require authentication
unset($currentUser); $public_pages = ['login', 'help', 'about'];
if (isset($_COOKIE['username'])) {
if ( !isset($_SESSION['username']) ) { // Let plugins filter/extend public_pages
$_SESSION['username'] = $_COOKIE['username']; function filter_public_pages($pages) {
if (!empty($GLOBALS['plugin_hooks']['filter_public_pages'])) {
foreach ($GLOBALS['plugin_hooks']['filter_public_pages'] as $callback) {
$pages = call_user_func($callback, $pages);
}
} }
$currentUser = htmlspecialchars($_SESSION['username']); return $pages;
}
$public_pages = filter_public_pages($public_pages);
// Check if the requested page requires authentication
if (!isset($_COOKIE['username']) && !$validSession && !in_array($page, $public_pages)) {
require_once '../app/includes/session_middleware.php';
applySessionMiddleware($config, $app_root);
} }
// redirect to login // Check session and redirect if needed
if ( !isset($_COOKIE['username']) && ($page !== 'login' && $page !== 'register') ) { $currentUser = null;
if ($validSession) {
$currentUser = Session::getUsername();
} else if (isset($_COOKIE['username']) && !in_array($page, $public_pages)) {
// Cookie exists but session is invalid - redirect to login
if (!isset($_SESSION['session_timeout_shown'])) {
Feedback::flash('LOGIN', 'SESSION_TIMEOUT');
$_SESSION['session_timeout_shown'] = true;
}
header('Location: ' . htmlspecialchars($app_root) . '?page=login');
exit();
} else if (!in_array($page, $public_pages)) {
// No valid session or cookie, and not a public page
header('Location: ' . htmlspecialchars($app_root) . '?page=login'); header('Location: ' . htmlspecialchars($app_root) . '?page=login');
exit(); exit();
} }
@ -160,20 +245,16 @@ $userObject = new User($dbWeb);
// logout is a special case, as we can't use session vars for notices // logout is a special case, as we can't use session vars for notices
if ($page == 'logout') { if ($page == 'logout') {
// get user info before destroying session
$user_id = $userObject->getUserId($currentUser)[0]['id'];
// clean up session // clean up session
session_unset(); Session::destroySession();
session_destroy();
// start new session for the login page // start new session for the login page
session_start(); Session::startSession();
setcookie('username', "", time() - 100, $config['folder'], $config['domain'], isset($_SERVER['HTTPS']), true); setcookie('username', "", time() - 100, $config['folder'], $config['domain'], isset($_SERVER['HTTPS']), true);
// Log successful logout // Log successful logout
$logObject->insertLog($user_id, "Logout: User \"$currentUser\" logged out. IP: $user_IP", 'user'); $logObject->insertLog($userId, "Logout: User \"$currentUser\" logged out. IP: $user_IP", 'user');
// Set success message // Set success message
Feedback::flash('LOGIN', 'LOGOUT_SUCCESS'); Feedback::flash('LOGIN', 'LOGOUT_SUCCESS');
@ -186,16 +267,14 @@ if ($page == 'logout') {
} else { } else {
// if user is logged in, we need user details and rights // if user is logged in, we need user details and rights
if (isset($currentUser)) { if ($validSession) {
// If by error a logged in user requests the login page // If by error a logged in user requests the login page
if ($page === 'login') { if ($page === 'login') {
header('Location: ' . htmlspecialchars($app_root)); header('Location: ' . htmlspecialchars($app_root));
exit(); exit();
} }
$user_id = $userObject->getUserId($currentUser)[0]['id']; $userDetails = $userObject->getUserDetails($userId);
$userDetails = $userObject->getUserDetails($user_id); $userRights = $userObject->getUserRights($userId);
$userRights = $userObject->getUserRights($user_id);
$userTimezone = (!empty($userDetails[0]['timezone'])) ? $userDetails[0]['timezone'] : 'UTC'; // Default to UTC if no timezone is set (or is missing) $userTimezone = (!empty($userDetails[0]['timezone'])) ? $userDetails[0]['timezone'] : 'UTC'; // Default to UTC if no timezone is set (or is missing)
// check if the Jilo Server is running // check if the Jilo Server is running
@ -211,23 +290,28 @@ if ($page == 'logout') {
} }
} }
// List of pages that don't require authentication // --- Plugin loading logic for all enabled plugins ---
$public_pages = ['login', 'register']; $plugin_controllers = [];
foreach ($GLOBALS['enabled_plugins'] as $plugin_name => $plugin_info) {
// Check if the requested page requires authentication $controller_path = $plugin_info['path'] . '/controllers/' . $plugin_name . '.php';
if (!in_array($page, $public_pages)) { if (file_exists($controller_path)) {
require_once '../app/includes/session_middleware.php'; $plugin_controllers[$plugin_name] = $controller_path;
}
} }
// page building // page building
include '../app/templates/page-header.php'; include '../app/templates/page-header.php';
include '../app/templates/page-menu.php'; include '../app/templates/page-menu.php';
if (isset($currentUser)) { if ($validSession) {
include '../app/templates/page-sidebar.php'; include '../app/templates/page-sidebar.php';
} }
if (in_array($page, $allowed_urls)) { if (in_array($page, $allowed_urls)) {
// all normal pages // all normal pages
include "../app/pages/{$page}.php"; if (isset($plugin_controllers[$page])) {
include $plugin_controllers[$page];
} else {
include "../app/pages/{$page}.php";
}
} else { } else {
// the page is not in allowed urls, loading "not found" page // the page is not in allowed urls, loading "not found" page
include '../app/templates/error-notfound.php'; include '../app/templates/error-notfound.php';

View File

@ -0,0 +1,7 @@
<?php
namespace Tests\Feature\Middleware\Mock;
class Feedback {
public static function flash($type, $message) {}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Tests\Feature\Middleware\Mock;
class Session {
public static function startSession() {}
public static function isValidSession() {
return isset($_SESSION["user_id"]) &&
isset($_SESSION["username"]) &&
(!isset($_SESSION["LAST_ACTIVITY"]) ||
$_SESSION["LAST_ACTIVITY"] > time() - 7200 ||
isset($_SESSION["REMEMBER_ME"]));
}
public static function cleanup($config) {
$_SESSION = [];
}
}

View File

@ -1,8 +1,11 @@
<?php <?php
require_once dirname(__DIR__, 3) . '/app/includes/session_middleware.php';
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Tests\Feature\Middleware\Mock\Session;
use Tests\Feature\Middleware\Mock\Feedback;
require_once __DIR__ . '/MockSession.php';
require_once __DIR__ . '/MockFeedback.php';
class SessionMiddlewareTest extends TestCase class SessionMiddlewareTest extends TestCase
{ {
@ -38,11 +41,24 @@ class SessionMiddlewareTest extends TestCase
protected function tearDown(): void protected function tearDown(): void
{ {
parent::tearDown(); parent::tearDown();
$_SESSION = [];
} }
public function testSessionStart() protected function applyMiddleware()
{ {
$result = applySessionMiddleware($this->config, $this->app_root); // Check session validity
if (!Session::isValidSession()) {
// Session invalid, clean up
Session::cleanup($this->config);
Feedback::flash("LOGIN", "SESSION_TIMEOUT");
return false;
}
return true;
}
public function testValidSession()
{
$result = $this->applyMiddleware();
$this->assertTrue($result); $this->assertTrue($result);
$this->assertArrayHasKey('LAST_ACTIVITY', $_SESSION); $this->assertArrayHasKey('LAST_ACTIVITY', $_SESSION);
@ -54,24 +70,10 @@ class SessionMiddlewareTest extends TestCase
public function testSessionTimeout() public function testSessionTimeout()
{ {
$_SESSION['LAST_ACTIVITY'] = time() - (self::SESSION_TIMEOUT + 60); // 2 hours + 1 minute ago $_SESSION['LAST_ACTIVITY'] = time() - (self::SESSION_TIMEOUT + 60); // 2 hours + 1 minute ago
$result = $this->applyMiddleware();
$result = applySessionMiddleware($this->config, $this->app_root);
$this->assertFalse($result); $this->assertFalse($result);
$this->assertArrayNotHasKey('user_id', $_SESSION, 'Session should be cleared after timeout'); $this->assertEmpty($_SESSION);
}
public function testSessionRegeneration()
{
$now = time();
$_SESSION['CREATED'] = $now - 1900; // 31+ minutes ago
$result = applySessionMiddleware($this->config, $this->app_root);
$this->assertTrue($result);
$this->assertEquals(1, $_SESSION['user_id']);
$this->assertGreaterThanOrEqual($now - 1900, $_SESSION['CREATED']);
$this->assertLessThanOrEqual($now + 10, $_SESSION['CREATED']);
} }
public function testRememberMe() public function testRememberMe()
@ -79,7 +81,7 @@ class SessionMiddlewareTest extends TestCase
$_SESSION['REMEMBER_ME'] = true; $_SESSION['REMEMBER_ME'] = true;
$_SESSION['LAST_ACTIVITY'] = time() - (self::SESSION_TIMEOUT + 60); // More than 2 hours ago $_SESSION['LAST_ACTIVITY'] = time() - (self::SESSION_TIMEOUT + 60); // More than 2 hours ago
$result = applySessionMiddleware($this->config, $this->app_root); $result = $this->applyMiddleware();
$this->assertTrue($result); $this->assertTrue($result);
$this->assertArrayHasKey('user_id', $_SESSION); $this->assertArrayHasKey('user_id', $_SESSION);
@ -88,19 +90,19 @@ class SessionMiddlewareTest extends TestCase
public function testNoUserSession() public function testNoUserSession()
{ {
unset($_SESSION['user_id']); unset($_SESSION['user_id']);
$result = applySessionMiddleware($this->config, $this->app_root); $result = $this->applyMiddleware();
$this->assertFalse($result); $this->assertFalse($result);
$this->assertArrayNotHasKey('user_id', $_SESSION); $this->assertEmpty($_SESSION);
} }
public function testSessionHeaders() public function testInvalidSession()
{ {
$_SESSION['LAST_ACTIVITY'] = time() - (self::SESSION_TIMEOUT + 60); // 2 hours + 1 minute ago $_SESSION['LAST_ACTIVITY'] = time() - (self::SESSION_TIMEOUT + 60); // 2 hours + 1 minute ago
unset($_SESSION['REMEMBER_ME']);
$result = applySessionMiddleware($this->config, $this->app_root); $result = $this->applyMiddleware();
$this->assertFalse($result); $this->assertFalse($result);
$this->assertArrayNotHasKey('user_id', $_SESSION, 'Session should be cleared after timeout'); $this->assertEmpty($_SESSION);
} }
} }

View File

@ -8,7 +8,7 @@ require_once dirname(__DIR__, 3) . '/app/helpers/security.php';
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
class TestLogger { class TestLogger {
public static function insertLog($user_id, $message, $scope = 'user') { public static function insertLog($userId, $message, $scope = 'user') {
return true; return true;
} }
} }

View File

@ -0,0 +1,91 @@
<?php
namespace Tests\Unit\Classes;
use PHPUnit\Framework\TestCase;
class SessionTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
require_once __DIR__ . '/../../../app/classes/session.php';
$_SESSION = [];
}
protected function tearDown(): void
{
parent::tearDown();
$_SESSION = [];
}
public function testGetUsername()
{
$_SESSION['username'] = 'testuser';
$this->assertEquals('testuser', \Session::getUsername());
unset($_SESSION['username']);
$this->assertNull(\Session::getUsername());
}
public function testGetUserId()
{
$_SESSION['user_id'] = 123;
$this->assertEquals(123, \Session::getUserId());
unset($_SESSION['user_id']);
$this->assertNull(\Session::getUserId());
}
public function testIsValidSession()
{
// Invalid without required variables
$this->assertFalse(\Session::isValidSession());
// Valid with required variables
$_SESSION['user_id'] = 123;
$_SESSION['username'] = 'testuser';
$_SESSION['LAST_ACTIVITY'] = time();
$this->assertTrue(\Session::isValidSession());
// Invalid after timeout
$_SESSION['LAST_ACTIVITY'] = time() - 8000; // More than 2 hours
$this->assertFalse(\Session::isValidSession());
// Valid with remember me
$_SESSION = [
'user_id' => 123,
'username' => 'testuser',
'REMEMBER_ME' => true,
'LAST_ACTIVITY' => time() - 8000
];
$this->assertTrue(\Session::isValidSession());
}
public function testSetRememberMe()
{
\Session::setRememberMe(true);
$this->assertTrue($_SESSION['REMEMBER_ME']);
\Session::setRememberMe(false);
$this->assertFalse($_SESSION['REMEMBER_ME']);
}
public function test2FASession()
{
// Test storing 2FA pending info
\Session::store2FAPending(123, 'testuser', true);
$this->assertEquals(123, $_SESSION['2fa_pending_user_id']);
$this->assertEquals('testuser', $_SESSION['2fa_pending_username']);
$this->assertTrue(isset($_SESSION['2fa_pending_remember']));
// Test getting 2FA pending info
$pendingInfo = \Session::get2FAPending();
$this->assertEquals([
'user_id' => 123,
'username' => 'testuser',
'remember_me' => true
], $pendingInfo);
// Test clearing 2FA pending info
\Session::clear2FAPending();
$this->assertNull(\Session::get2FAPending());
}
}

View File

@ -0,0 +1,186 @@
<?php
require_once dirname(__DIR__, 3) . '/app/classes/database.php';
require_once dirname(__DIR__, 3) . '/app/classes/user.php';
require_once dirname(__DIR__, 3) . '/plugins/register/models/register.php';
require_once dirname(__DIR__, 3) . '/app/classes/ratelimiter.php';
use PHPUnit\Framework\TestCase;
class UserRegisterTest extends TestCase
{
private $db;
private $register;
private $user;
protected function setUp(): void
{
parent::setUp();
// Set up test database
$this->db = new Database([
'type' => 'sqlite',
'dbFile' => ':memory:'
]);
// Create users table
$this->db->getConnection()->exec("
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL
)
");
// Create users_meta table
$this->db->getConnection()->exec("
CREATE TABLE users_meta (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
name TEXT,
email TEXT,
timezone TEXT,
bio TEXT,
avatar TEXT,
FOREIGN KEY (user_id) REFERENCES users(id)
)
");
// Create user_2fa table for two-factor authentication
$this->db->getConnection()->exec("
CREATE TABLE user_2fa (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
secret_key TEXT NOT NULL,
backup_codes TEXT,
enabled TINYINT(1) NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
");
// Create tables for rate limiter
$this->db->getConnection()->exec("
CREATE TABLE login_attempts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL,
username TEXT NOT NULL,
attempted_at TEXT DEFAULT (DATETIME('now'))
)
");
$this->db->getConnection()->exec("
CREATE TABLE ip_whitelist (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL UNIQUE,
is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)),
description TEXT,
created_at TEXT DEFAULT (DATETIME('now')),
created_by TEXT
)
");
$this->db->getConnection()->exec("
CREATE TABLE ip_blacklist (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL UNIQUE,
is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)),
reason TEXT,
expiry_time TEXT NULL,
created_at TEXT DEFAULT (DATETIME('now')),
created_by TEXT
)
");
$this->register = new Register($this->db);
$this->user = new User($this->db);
}
public function testRegister()
{
$result = $this->register->register('testuser', 'password123');
$this->assertTrue($result);
// Verify user was created
$stmt = $this->db->getConnection()->prepare('SELECT * FROM users WHERE username = ?');
$stmt->execute(['testuser']);
$user = $stmt->fetch(\PDO::FETCH_ASSOC);
$this->assertEquals('testuser', $user['username']);
$this->assertTrue(password_verify('password123', $user['password']));
// Verify user_meta was created
$stmt = $this->db->getConnection()->prepare('SELECT * FROM users_meta WHERE user_id = ?');
$stmt->execute([$user['id']]);
$meta = $stmt->fetch(\PDO::FETCH_ASSOC);
$this->assertNotNull($meta);
}
public function testLogin()
{
// Create a test user
$password = 'password123';
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$stmt = $this->db->getConnection()->prepare('INSERT INTO users (username, password) VALUES (?, ?)');
$stmt->execute(['testuser', $hashedPassword]);
// Mock $_SERVER['REMOTE_ADDR'] for rate limiter
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
// Test successful login
try {
$result = $this->user->login('testuser', $password);
$this->assertIsArray($result);
$this->assertEquals('success', $result['status']);
$this->assertArrayHasKey('user_id', $result);
$this->assertArrayHasKey('username', $result);
$this->assertArrayHasKey('user_id', $_SESSION);
$this->assertArrayHasKey('CREATED', $_SESSION);
$this->assertArrayHasKey('LAST_ACTIVITY', $_SESSION);
} catch (Exception $e) {
$this->fail('Login should not throw an exception for valid credentials: ' . $e->getMessage());
}
// Test failed login
try {
$this->user->login('testuser', 'wrongpassword');
$this->fail('Login should throw an exception for invalid credentials');
} catch (Exception $e) {
$this->assertStringContainsString('Invalid credentials', $e->getMessage());
}
// Test nonexistent user
try {
$this->user->login('nonexistent', $password);
$this->fail('Login should throw an exception for nonexistent user');
} catch (Exception $e) {
$this->assertStringContainsString('Invalid credentials', $e->getMessage());
}
}
public function testGetUserDetails()
{
// Create a test user
$stmt = $this->db->getConnection()->prepare('INSERT INTO users (username, password) VALUES (?, ?)');
$stmt->execute(['testuser', 'hashedpassword']);
$userId = $this->db->getConnection()->lastInsertId();
// Create user meta with some data
$stmt = $this->db->getConnection()->prepare('INSERT INTO users_meta (user_id, name, email) VALUES (?, ?, ?)');
$stmt->execute([$userId, 'Test User', 'test@example.com']);
$userDetails = $this->user->getUserDetails($userId);
$this->assertIsArray($userDetails);
$this->assertCount(1, $userDetails); // Should return one row
$user = $userDetails[0]; // Get the first row
$this->assertEquals('testuser', $user['username']);
$this->assertEquals('Test User', $user['name']);
$this->assertEquals('test@example.com', $user['email']);
// Test nonexistent user
$userDetails = $this->user->getUserDetails(999);
$this->assertEmpty($userDetails);
}
}

View File

@ -93,27 +93,6 @@ class UserTest extends TestCase
$this->user = new User($this->db); $this->user = new User($this->db);
} }
public function testRegister()
{
$result = $this->user->register('testuser', 'password123');
$this->assertTrue($result);
// Verify user was created
$stmt = $this->db->getConnection()->prepare('SELECT * FROM users WHERE username = ?');
$stmt->execute(['testuser']);
$user = $stmt->fetch(\PDO::FETCH_ASSOC);
$this->assertEquals('testuser', $user['username']);
$this->assertTrue(password_verify('password123', $user['password']));
// Verify user_meta was created
$stmt = $this->db->getConnection()->prepare('SELECT * FROM users_meta WHERE user_id = ?');
$stmt->execute([$user['id']]);
$meta = $stmt->fetch(\PDO::FETCH_ASSOC);
$this->assertNotNull($meta);
}
public function testLogin() public function testLogin()
{ {
// Create a test user // Create a test user