Compare commits

..

No commits in common. "HEAD" and "v0.4" have entirely different histories.
HEAD ... v0.4

44 changed files with 617 additions and 1160 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.
*/
public function editConfigFile($updatedConfig, $config_file) {
global $logObject, $userId;
global $logObject, $user_id;
$allLogs = [];
$updated = [];
@ -140,7 +140,7 @@ class Config {
}
if (!empty($allLogs)) {
$logObject->insertLog($userId, implode("\n", $allLogs), 'system');
$logObject->insertLog($user_id, implode("\n", $allLogs), 'system');
}
return [
@ -148,7 +148,7 @@ class Config {
'updated' => $updated
];
} catch (Exception $e) {
$logObject->insertLog($userId, "Config update error: " . $e->getMessage(), 'system');
$logObject->insertLog($user_id, "Config update error: " . $e->getMessage(), 'system');
return [
'success' => false,
'error' => $e->getMessage()

View File

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

View File

@ -18,23 +18,19 @@ class Log {
* @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();
}
$this->db = $database->getConnection();
}
/**
* Insert a log event into the database.
*
* @param int $userId The ID of the user associated with the log event.
* @param int $user_id The ID of the user associated with the log event.
* @param string $message The log message to insert.
* @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.
*/
public function insertLog($userId, $message, $scope='user') {
public function insertLog($user_id, $message, $scope='user') {
try {
$sql = 'INSERT INTO logs
(user_id, scope, message)
@ -43,7 +39,7 @@ class Log {
$query = $this->db->prepare($sql);
$query->execute([
':user_id' => $userId,
':user_id' => $user_id,
':scope' => $scope,
':message' => $message,
]);
@ -58,7 +54,7 @@ class Log {
/**
* Retrieve log entries from the database.
*
* @param int $userId The ID of the user whose logs are being retrieved.
* @param int $user_id The ID of the user whose logs are being retrieved.
* @param string $scope The scope of the logs ('user' or 'system').
* @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.
@ -66,7 +62,7 @@ class Log {
*
* @return array An array of log entries.
*/
public function readLog($userId, $scope, $offset=0, $items_per_page='', $filters=[]) {
public function readLog($user_id, $scope, $offset=0, $items_per_page='', $filters=[]) {
$params = [];
$where_clauses = [];
@ -78,7 +74,7 @@ class Log {
// Add scope condition
if ($scope === 'user') {
$where_clauses[] = 'l.user_id = :user_id';
$params[':user_id'] = $userId;
$params[':user_id'] = $user_id;
}
// Add time range filters if specified

View File

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

View File

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

View File

@ -4,33 +4,94 @@
* Session Middleware
*
* Validates session status and handles session timeout.
* If session is invalid, redirects to login page.
* This middleware should be included in all protected pages.
*/
function applySessionMiddleware($config, $app_root, $isTest = false) {
// Start session if not already started
if (session_status() !== PHP_SESSION_ACTIVE) {
Session::startSession();
function applySessionMiddleware($config, $app_root) {
$isTest = defined('PHPUNIT_RUNNING');
// Access $_SESSION directly in test mode
if (!$isTest) {
// Start session if not already started
if (session_status() !== PHP_SESSION_ACTIVE && !headers_sent()) {
session_start([
'cookie_httponly' => 1,
'cookie_secure' => 1,
'cookie_samesite' => 'Strict',
'gc_maxlifetime' => 7200 // 2 hours
]);
}
}
// Check session validity
if (!Session::isValidSession()) {
// Only show session timeout message if there was an active session
// and we haven't shown it yet
if (isset($_SESSION['LAST_ACTIVITY']) && !isset($_SESSION['session_timeout_shown'])) {
Feedback::flash('LOGIN', 'SESSION_TIMEOUT');
$_SESSION['session_timeout_shown'] = true;
}
// Session invalid, clean up and redirect
Session::cleanup($config);
if (!$isTest) {
header('Location: ' . $app_root . '?page=login');
exit();
}
// Check if user is logged in with all required session variables
if (!isset($_SESSION['user_id']) || !isset($_SESSION['username'])) {
cleanupSession($config, $app_root, $isTest);
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)) {
// 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 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,7 +11,6 @@ return [
'LOGIN_SUCCESS' => 'Login successful.',
'LOGIN_FAILED' => 'Login failed. Please check your credentials.',
'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_NOT_WHITELISTED' => 'Access denied. Your IP address is not whitelisted.',
'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
require '../app/includes/rate_limit_middleware.php';
checkRateLimit($dbWeb, 'contact', $userId);
checkRateLimit($dbWeb, 'contact', $user_id);
// Validate agent ID for POST operations
if ($agentId === false || $agentId === null) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@
<div class="card-body">
<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">
<?php include CSRF_TOKEN_INCLUDE; ?>
<?php include 'csrf_token.php'; ?>
<div class="form-group mb-3">
<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 _"
@ -12,7 +12,7 @@
</div>
<div class="form-group mb-3">
<input type="password" class="form-control w-50 mx-auto" name="password" placeholder="Password"
pattern=".{8,}" title="Eight or more characters"
pattern=".{5,}" title="Eight or more characters"
required />
</div>
<div class="form-group mb-3">

View File

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

View File

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

View File

@ -4,7 +4,7 @@
<div class="card-body">
<p class="card-text">Enter credentials for registration:</p>
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=register">
<?php include CSRF_TOKEN_INCLUDE; ?>
<?php include 'csrf_token.php'; ?>
<div class="form-group mb-3">
<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 _"
@ -20,17 +20,6 @@
pattern=".{8,}" title="Eight or more characters"
required />
</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" />
</form>
</div>

View File

@ -12,7 +12,7 @@
</div>
<?php if (Session::getUsername() && $page !== 'logout') { ?>
<?php if (isset($currentUser) && $page !== 'logout') { ?>
<script src="static/js/sidebar.js"></script>
<?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/messages.css">
<script src="<?= htmlspecialchars($app_root) ?>static/js/messages.js"></script>
<?php if (Session::getUsername()) { ?>
<?php if (isset($currentUser)) { ?>
<script>
// restore sidebar state before the page is rendered
(function () {

View File

@ -18,7 +18,7 @@
version&nbsp;<?= htmlspecialchars($config['version']) ?>
</li>
<?php if (Session::isValidSession()) { ?>
<?php if (isset($_SESSION['username']) && isset($_SESSION['user_id'])) { ?>
<?php foreach ($platformsAll as $platform) {
$platform_switch_url = switchPlatform($platform['id']);
@ -40,7 +40,7 @@
</ul>
<ul class="menu-right">
<?php if (Session::isValidSession()) { ?>
<?php if (isset($_SESSION['username']) && isset($_SESSION['user_id'])) { ?>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
<i class="fas fa-user"></i>
@ -59,48 +59,10 @@
</a>
</div>
</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 { ?>
<li><a href="<?= htmlspecialchars($app_root) ?>?page=login">login</a></li>
<?php do_hook('main_public_menu', ['app_root' => $app_root, 'section' => 'main', 'position' => 100]); ?>
<li><a href="<?= htmlspecialchars($app_root) ?>?page=register">register</a></li>
<?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>
</div>
<!-- /Menu -->

View File

@ -72,11 +72,45 @@ $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
</li>
</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">
<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
</li>
</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>
</div>
</div>

View File

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

View File

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

View File

@ -5,17 +5,17 @@
<h2 class="mb-0">Security settings</h2>
<small>network restrictions to control flooding and brute force attacks</small>
<ul class="nav nav-tabs mt-5">
<?php if ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, 'edit whitelist')) { ?>
<?php if ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit whitelist')) { ?>
<li class="nav-item">
<a class="nav-link <?= $section === 'whitelist' ? 'active' : '' ?>" href="?page=security&section=whitelist">IP whitelist</a>
</li>
<?php } ?>
<?php if ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, 'edit blacklist')) { ?>
<?php if ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit blacklist')) { ?>
<li class="nav-item">
<a class="nav-link <?= $section === 'blacklist' ? 'active' : '' ?>" href="?page=security&section=blacklist">IP blacklist</a>
</li>
<?php } ?>
<?php if ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, 'edit ratelimiting')) { ?>
<?php if ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit ratelimiting')) { ?>
<li class="nav-item">
<a class="nav-link <?= $section === 'ratelimit' ? 'active' : '' ?>" href="?page=security&section=ratelimit">Rate limiting</a>
</li>
@ -24,7 +24,7 @@
</div>
</div>
<?php if ($section === 'whitelist' && ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, 'edit whitelist'))) { ?>
<?php if ($section === 'whitelist' && ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit whitelist'))) { ?>
<!-- whitelist section -->
<div class="row mb-4">
<div class="col">
@ -35,7 +35,7 @@
</div>
<div class="card-body">
<form method="POST" class="mb-4">
<?php include CSRF_TOKEN_INCLUDE; ?>
<?php include 'csrf_token.php'; ?>
<input type="hidden" name="action" value="add_whitelist">
<div class="row g-3">
<div class="col-md-4">
@ -77,7 +77,7 @@
<td><?= htmlspecialchars($ip['created_at']) ?></td>
<td>
<form method="POST" style="display: inline;">
<?php include CSRF_TOKEN_INCLUDE; ?>
<?php include 'csrf_token.php'; ?>
<input type="hidden" name="action" value="remove_whitelist">
<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>
@ -93,7 +93,7 @@
</div>
<?php } ?>
<?php if ($section === 'blacklist' && ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, 'edit blacklist'))) { ?>
<?php if ($section === 'blacklist' && ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit blacklist'))) { ?>
<!-- blacklist section -->
<div class="row mb-4">
<div class="col">
@ -104,7 +104,7 @@
</div>
<div class="card-body">
<form method="POST" class="mb-4">
<?php include CSRF_TOKEN_INCLUDE; ?>
<?php include 'csrf_token.php'; ?>
<input type="hidden" name="action" value="add_blacklist">
<div class="row g-3">
<div class="col-md-3">
@ -151,7 +151,7 @@
<td><?= $ip['expiry_time'] ? htmlspecialchars($ip['expiry_time']) : 'Never' ?></td>
<td>
<form method="POST" style="display: inline;">
<?php include CSRF_TOKEN_INCLUDE; ?>
<?php include 'csrf_token.php'; ?>
<input type="hidden" name="action" value="remove_blacklist">
<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>
@ -167,7 +167,7 @@
</div>
<?php } ?>
<?php if ($section === 'ratelimit' && ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, 'edit ratelimiting'))) { ?>
<?php if ($section === 'ratelimit' && ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit ratelimiting'))) { ?>
<!-- rate limiting section -->
<div class="row mb-4">
<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;">
<i class="fas fa-times me-1"></i>Cancel
</button>
<?php if ($userObject->hasRight($userId, 'delete platform')): ?>
<?php if ($userObject->hasRight($user_id, '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'])) ?>')">
<i class="fas fa-trash me-1"></i>Delete platform
</button>

View File

@ -1,3 +1,4 @@
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
@ -24,95 +25,43 @@ CREATE TABLE rights (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS "jilo_agent_types" (
id INTEGER PRIMARY KEY AUTOINCREMENT,
description TEXT,
endpoint TEXT
);
INSERT INTO rights VALUES(1,'superuser');
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 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 (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
jitsi_url 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 (
user_id INTEGER NOT NULL PRIMARY KEY,
secret_key TEXT NOT NULL,
backup_codes TEXT,
enabled INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
last_used TEXT,
FOREIGN KEY (user_id) REFERENCES users(id)
user_id INTEGER NOT NULL PRIMARY KEY,
secret_key TEXT NOT NULL,
backup_codes TEXT,
enabled INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
last_used TEXT,
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE TABLE user_2fa_temp (
user_id INTEGER NOT NULL PRIMARY KEY,
code TEXT NOT NULL,
created_at TEXT NOT NULL,
expires_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id)
user_id INTEGER NOT NULL,
code TEXT NOT NULL,
created_at TEXT NOT NULL,
expires_at TEXT NOT NULL,
PRIMARY KEY (user_id, code),
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE TABLE user_password_reset (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -121,5 +70,85 @@ CREATE TABLE user_password_reset (
expires INTEGER NOT NULL,
used INTEGER NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
FOREIGN KEY (user_id) REFERENCES user(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,38 +5,24 @@ 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(2,2,'demo user','demo@example.com',NULL,NULL,'This is a demo user of the demo install of Jilo Web');
INSERT INTO rights VALUES(1,'superuser');
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 platforms VALUES(1,'meet.lindeas.com','https://meet.lindeas.com','../jilo-meet.lindeas.db');
INSERT INTO platforms VALUES(2,'example.com','https://meet.example.com','../jilo.db');
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');
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 logs VALUES(2,2,'2024-09-30 09:54:54','user','Login: User "demo" logged in. IP: 151.237.101.43');
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 logs VALUES(4,2,'2024-10-03 16:34:56','user','Login: User "demo" logged in. IP: 151.237.101.43');
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 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 platforms VALUES(1,'example.com','https://meet.example.com','../../jilo/jilo.db');
INSERT INTO jilo_agents VALUES(1,1,1,'https://meet.lindeas.com:8081','mysecretkey',5);
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 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');
INSERT INTO hosts VALUES(1,'meet.lindeas.com',2,'main machine');
INSERT INTO hosts VALUES(2,'meet.example.com',2,'test');
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

@ -1,22 +0,0 @@
<?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

@ -1,92 +0,0 @@
<?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

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

View File

@ -11,68 +11,18 @@
* 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
// flush it later only when there is no redirect
ob_start();
// Start session before any session-dependent code
require_once '../app/classes/session.php';
Session::startSession();
// Apply security headers
require_once '../app/includes/security_headers_middleware.php';
// sanitize all input vars that may end up in URLs or forms
require '../app/includes/sanitize.php';
// Check session validity
$validSession = Session::isValidSession();
// Get user ID early if session is valid
$userId = $validSession ? Session::getUserId() : null;
session_name('jilo');
session_start();
// Initialize feedback message system
require_once '../app/classes/feedback.php';
@ -113,21 +63,9 @@ $allowed_urls = [
'login',
'logout',
'about',
'register',
];
// 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
// possible locations, in order of preference
$config_file_locations = [
@ -154,40 +92,17 @@ if ($config_file) {
$app_root = $config['folder'];
// List of pages that don't require authentication
$public_pages = ['login', 'help', 'about'];
// Let plugins filter/extend public_pages
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);
}
// check if logged in
unset($currentUser);
if (isset($_COOKIE['username'])) {
if ( !isset($_SESSION['username']) ) {
$_SESSION['username'] = $_COOKIE['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);
$currentUser = htmlspecialchars($_SESSION['username']);
}
// Check session and redirect if needed
$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
// redirect to login
if ( !isset($_COOKIE['username']) && ($page !== 'login' && $page !== 'register') ) {
header('Location: ' . htmlspecialchars($app_root) . '?page=login');
exit();
}
@ -245,16 +160,20 @@ $userObject = new User($dbWeb);
// logout is a special case, as we can't use session vars for notices
if ($page == 'logout') {
// get user info before destroying session
$user_id = $userObject->getUserId($currentUser)[0]['id'];
// clean up session
Session::destroySession();
session_unset();
session_destroy();
// start new session for the login page
Session::startSession();
session_start();
setcookie('username', "", time() - 100, $config['folder'], $config['domain'], isset($_SERVER['HTTPS']), true);
// Log successful logout
$logObject->insertLog($userId, "Logout: User \"$currentUser\" logged out. IP: $user_IP", 'user');
$logObject->insertLog($user_id, "Logout: User \"$currentUser\" logged out. IP: $user_IP", 'user');
// Set success message
Feedback::flash('LOGIN', 'LOGOUT_SUCCESS');
@ -267,14 +186,16 @@ if ($page == 'logout') {
} else {
// if user is logged in, we need user details and rights
if ($validSession) {
if (isset($currentUser)) {
// If by error a logged in user requests the login page
if ($page === 'login') {
header('Location: ' . htmlspecialchars($app_root));
exit();
}
$userDetails = $userObject->getUserDetails($userId);
$userRights = $userObject->getUserRights($userId);
$user_id = $userObject->getUserId($currentUser)[0]['id'];
$userDetails = $userObject->getUserDetails($user_id);
$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)
// check if the Jilo Server is running
@ -290,28 +211,23 @@ if ($page == 'logout') {
}
}
// --- Plugin loading logic for all enabled plugins ---
$plugin_controllers = [];
foreach ($GLOBALS['enabled_plugins'] as $plugin_name => $plugin_info) {
$controller_path = $plugin_info['path'] . '/controllers/' . $plugin_name . '.php';
if (file_exists($controller_path)) {
$plugin_controllers[$plugin_name] = $controller_path;
}
// List of pages that don't require authentication
$public_pages = ['login', 'register'];
// Check if the requested page requires authentication
if (!in_array($page, $public_pages)) {
require_once '../app/includes/session_middleware.php';
}
// page building
include '../app/templates/page-header.php';
include '../app/templates/page-menu.php';
if ($validSession) {
if (isset($currentUser)) {
include '../app/templates/page-sidebar.php';
}
if (in_array($page, $allowed_urls)) {
// all normal pages
if (isset($plugin_controllers[$page])) {
include $plugin_controllers[$page];
} else {
include "../app/pages/{$page}.php";
}
include "../app/pages/{$page}.php";
} else {
// the page is not in allowed urls, loading "not found" page
include '../app/templates/error-notfound.php';

View File

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

View File

@ -1,19 +0,0 @@
<?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,11 +1,8 @@
<?php
use PHPUnit\Framework\TestCase;
use Tests\Feature\Middleware\Mock\Session;
use Tests\Feature\Middleware\Mock\Feedback;
require_once dirname(__DIR__, 3) . '/app/includes/session_middleware.php';
require_once __DIR__ . '/MockSession.php';
require_once __DIR__ . '/MockFeedback.php';
use PHPUnit\Framework\TestCase;
class SessionMiddlewareTest extends TestCase
{
@ -41,24 +38,11 @@ class SessionMiddlewareTest extends TestCase
protected function tearDown(): void
{
parent::tearDown();
$_SESSION = [];
}
protected function applyMiddleware()
public function testSessionStart()
{
// 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();
$result = applySessionMiddleware($this->config, $this->app_root);
$this->assertTrue($result);
$this->assertArrayHasKey('LAST_ACTIVITY', $_SESSION);
@ -70,10 +54,24 @@ class SessionMiddlewareTest extends TestCase
public function testSessionTimeout()
{
$_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->assertEmpty($_SESSION);
$this->assertArrayNotHasKey('user_id', $_SESSION, 'Session should be cleared after timeout');
}
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()
@ -81,7 +79,7 @@ class SessionMiddlewareTest extends TestCase
$_SESSION['REMEMBER_ME'] = true;
$_SESSION['LAST_ACTIVITY'] = time() - (self::SESSION_TIMEOUT + 60); // More than 2 hours ago
$result = $this->applyMiddleware();
$result = applySessionMiddleware($this->config, $this->app_root);
$this->assertTrue($result);
$this->assertArrayHasKey('user_id', $_SESSION);
@ -90,19 +88,19 @@ class SessionMiddlewareTest extends TestCase
public function testNoUserSession()
{
unset($_SESSION['user_id']);
$result = $this->applyMiddleware();
$result = applySessionMiddleware($this->config, $this->app_root);
$this->assertFalse($result);
$this->assertEmpty($_SESSION);
$this->assertArrayNotHasKey('user_id', $_SESSION);
}
public function testInvalidSession()
public function testSessionHeaders()
{
$_SESSION['LAST_ACTIVITY'] = time() - (self::SESSION_TIMEOUT + 60); // 2 hours + 1 minute ago
unset($_SESSION['REMEMBER_ME']);
$result = $this->applyMiddleware();
$result = applySessionMiddleware($this->config, $this->app_root);
$this->assertFalse($result);
$this->assertEmpty($_SESSION);
$this->assertArrayNotHasKey('user_id', $_SESSION, 'Session should be cleared after timeout');
}
}

View File

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

View File

@ -1,91 +0,0 @@
<?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

@ -1,186 +0,0 @@
<?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,6 +93,27 @@ class UserTest extends TestCase
$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()
{
// Create a test user