Compare commits
No commits in common. "main" and "v0.4" have entirely different histories.
|
@ -10,103 +10,21 @@ jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
services:
|
|
||||||
mariadb:
|
|
||||||
image: mariadb:10.6
|
|
||||||
env:
|
|
||||||
MARIADB_ROOT_PASSWORD: root
|
|
||||||
MARIADB_DATABASE: jilo_test
|
|
||||||
MARIADB_USER: test_jilo
|
|
||||||
MARIADB_PASSWORD: test_password
|
|
||||||
ports:
|
|
||||||
- 3306:3306
|
|
||||||
options: >-
|
|
||||||
--health-cmd="mysqladmin ping -h127.0.0.1 -P3306 -uroot -proot"
|
|
||||||
--health-interval=10s
|
|
||||||
--health-timeout=10s
|
|
||||||
--health-retries=5
|
|
||||||
--health-start-period=30s
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
php-version: '8.2'
|
php-version: '8.1'
|
||||||
extensions: pdo, pdo_mysql, xdebug
|
extensions: pdo, pdo_sqlite
|
||||||
coverage: xdebug
|
|
||||||
|
|
||||||
- name: Wait for MariaDB
|
|
||||||
run: |
|
|
||||||
sudo apt-get install -y mariadb-client
|
|
||||||
while ! mysqladmin ping -h"127.0.0.1" -P"3306" -uroot -proot --silent; do
|
|
||||||
echo "Waiting for database connection..."
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
cd tests
|
cd tests
|
||||||
composer install
|
composer install
|
||||||
|
|
||||||
- name: Test database connection
|
|
||||||
run: |
|
|
||||||
mysql -h127.0.0.1 -P3306 -uroot -proot -e "SHOW DATABASES;"
|
|
||||||
mysql -h127.0.0.1 -P3306 -uroot -proot -e "SELECT User,Host FROM mysql.user;"
|
|
||||||
mysql -h127.0.0.1 -P3306 -uroot -proot -e "GRANT ALL PRIVILEGES ON jilo_test.* TO 'test_jilo'@'%';"
|
|
||||||
mysql -h127.0.0.1 -P3306 -uroot -proot -e "FLUSH PRIVILEGES;"
|
|
||||||
|
|
||||||
- name: Update database config for CI
|
|
||||||
run: |
|
|
||||||
# Create temporary test config
|
|
||||||
mkdir -p tests/config
|
|
||||||
cat > tests/config/ci-config.php << 'EOF'
|
|
||||||
<?php
|
|
||||||
define('CI_DB_PASSWORD', 'test_password');
|
|
||||||
define('CI_DB_HOST', '127.0.0.1');
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Verify config file was created
|
|
||||||
echo "Config file contents:"
|
|
||||||
cat tests/config/ci-config.php
|
|
||||||
echo "\nConfig file location:"
|
|
||||||
ls -la tests/config/ci-config.php
|
|
||||||
|
|
||||||
# Grant access from Docker network
|
|
||||||
mysql -h127.0.0.1 -P3306 -uroot -proot -e "
|
|
||||||
DROP USER IF EXISTS 'test_jilo'@'%';
|
|
||||||
CREATE USER 'test_jilo'@'%' IDENTIFIED BY 'test_password';
|
|
||||||
GRANT ALL PRIVILEGES ON jilo_test.* TO 'test_jilo'@'%';
|
|
||||||
CREATE DATABASE IF NOT EXISTS jilo_test;
|
|
||||||
FLUSH PRIVILEGES;
|
|
||||||
"
|
|
||||||
|
|
||||||
# Update test files to require the config (using absolute path)
|
|
||||||
CONFIG_PATH=$(realpath tests/config/ci-config.php)
|
|
||||||
echo "\nConfig path: $CONFIG_PATH"
|
|
||||||
|
|
||||||
# Add require statement at the very start
|
|
||||||
for file in tests/Unit/Classes/{DatabaseTest,UserTest}.php; do
|
|
||||||
echo "<?php" > "$file.tmp"
|
|
||||||
echo "require_once '$CONFIG_PATH';" >> "$file.tmp"
|
|
||||||
tail -n +2 "$file" >> "$file.tmp"
|
|
||||||
mv "$file.tmp" "$file"
|
|
||||||
echo "\nFirst 5 lines of $file:"
|
|
||||||
head -n 5 "$file"
|
|
||||||
done
|
|
||||||
|
|
||||||
# Test database connection directly
|
|
||||||
echo "\nTesting database connection:"
|
|
||||||
mysql -h127.0.0.1 -P3306 -utest_jilo -ptest_password -e "SELECT 'Database connection successful!'" || echo "Database connection failed"
|
|
||||||
|
|
||||||
- name: Run test suite
|
- name: Run test suite
|
||||||
run: |
|
run: |
|
||||||
cd tests
|
cd tests
|
||||||
./vendor/bin/phpunit
|
./vendor/bin/phpunit
|
||||||
# FIXME
|
|
||||||
# XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-html coverage
|
|
||||||
# env:
|
|
||||||
# COMPOSER_PROCESS_TIMEOUT: 0
|
|
||||||
# COMPOSER_NO_INTERACTION: 1
|
|
||||||
# COMPOSER_NO_AUDIT: 1
|
|
||||||
|
|
|
@ -13,8 +13,6 @@ All notable changes to this project will be documented in this file.
|
||||||
- gitlab: https://gitlab.com/lindeas/jilo-web/-/compare/v0.4...HEAD
|
- gitlab: https://gitlab.com/lindeas/jilo-web/-/compare/v0.4...HEAD
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Added CSS and JS to the default theme
|
|
||||||
- Added change theme menu entry
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|
|
@ -43,11 +43,11 @@ class Agent {
|
||||||
jat.endpoint AS agent_endpoint,
|
jat.endpoint AS agent_endpoint,
|
||||||
h.platform_id
|
h.platform_id
|
||||||
FROM
|
FROM
|
||||||
jilo_agent ja
|
jilo_agents ja
|
||||||
JOIN
|
JOIN
|
||||||
jilo_agent_type jat ON ja.agent_type_id = jat.id
|
jilo_agent_types jat ON ja.agent_type_id = jat.id
|
||||||
JOIN
|
JOIN
|
||||||
host h ON ja.host_id = h.id
|
hosts h ON ja.host_id = h.id
|
||||||
WHERE
|
WHERE
|
||||||
ja.host_id = :host_id';
|
ja.host_id = :host_id';
|
||||||
|
|
||||||
|
@ -87,11 +87,11 @@ class Agent {
|
||||||
jat.endpoint AS agent_endpoint,
|
jat.endpoint AS agent_endpoint,
|
||||||
h.platform_id
|
h.platform_id
|
||||||
FROM
|
FROM
|
||||||
jilo_agent ja
|
jilo_agents ja
|
||||||
JOIN
|
JOIN
|
||||||
jilo_agent_type jat ON ja.agent_type_id = jat.id
|
jilo_agent_types jat ON ja.agent_type_id = jat.id
|
||||||
JOIN
|
JOIN
|
||||||
host h ON ja.host_id = h.id
|
hosts h ON ja.host_id = h.id
|
||||||
WHERE
|
WHERE
|
||||||
ja.id = :agent_id';
|
ja.id = :agent_id';
|
||||||
|
|
||||||
|
@ -110,7 +110,7 @@ class Agent {
|
||||||
*/
|
*/
|
||||||
public function getAgentTypes() {
|
public function getAgentTypes() {
|
||||||
$sql = 'SELECT *
|
$sql = 'SELECT *
|
||||||
FROM jilo_agent_type
|
FROM jilo_agent_types
|
||||||
ORDER BY id';
|
ORDER BY id';
|
||||||
$query = $this->db->prepare($sql);
|
$query = $this->db->prepare($sql);
|
||||||
$query->execute();
|
$query->execute();
|
||||||
|
@ -131,7 +131,7 @@ class Agent {
|
||||||
id,
|
id,
|
||||||
agent_type_id
|
agent_type_id
|
||||||
FROM
|
FROM
|
||||||
jilo_agent
|
jilo_agents
|
||||||
WHERE
|
WHERE
|
||||||
host_id = :host_id';
|
host_id = :host_id';
|
||||||
$query = $this->db->prepare($sql);
|
$query = $this->db->prepare($sql);
|
||||||
|
@ -152,7 +152,7 @@ class Agent {
|
||||||
*/
|
*/
|
||||||
public function addAgent($host_id, $newAgent) {
|
public function addAgent($host_id, $newAgent) {
|
||||||
try {
|
try {
|
||||||
$sql = 'INSERT INTO jilo_agent
|
$sql = 'INSERT INTO jilo_agents
|
||||||
(host_id, agent_type_id, url, secret_key, check_period)
|
(host_id, agent_type_id, url, secret_key, check_period)
|
||||||
VALUES
|
VALUES
|
||||||
(:host_id, :agent_type_id, :url, :secret_key, :check_period)';
|
(:host_id, :agent_type_id, :url, :secret_key, :check_period)';
|
||||||
|
@ -184,7 +184,7 @@ class Agent {
|
||||||
*/
|
*/
|
||||||
public function editAgent($agent_id, $updatedAgent) {
|
public function editAgent($agent_id, $updatedAgent) {
|
||||||
try {
|
try {
|
||||||
$sql = 'UPDATE jilo_agent
|
$sql = 'UPDATE jilo_agents
|
||||||
SET
|
SET
|
||||||
agent_type_id = :agent_type_id,
|
agent_type_id = :agent_type_id,
|
||||||
url = :url,
|
url = :url,
|
||||||
|
@ -222,7 +222,7 @@ class Agent {
|
||||||
*/
|
*/
|
||||||
public function deleteAgent($agent_id) {
|
public function deleteAgent($agent_id) {
|
||||||
try {
|
try {
|
||||||
$sql = 'DELETE FROM jilo_agent
|
$sql = 'DELETE FROM jilo_agents
|
||||||
WHERE
|
WHERE
|
||||||
id = :agent_id';
|
id = :agent_id';
|
||||||
|
|
||||||
|
@ -420,13 +420,13 @@ class Agent {
|
||||||
jac.agent_id,
|
jac.agent_id,
|
||||||
jat.description
|
jat.description
|
||||||
FROM
|
FROM
|
||||||
jilo_agent_check jac
|
jilo_agent_checks jac
|
||||||
JOIN
|
JOIN
|
||||||
jilo_agent ja ON jac.agent_id = ja.id
|
jilo_agents ja ON jac.agent_id = ja.id
|
||||||
JOIN
|
JOIN
|
||||||
jilo_agent_type jat ON ja.agent_type_id = jat.id
|
jilo_agent_types jat ON ja.agent_type_id = jat.id
|
||||||
JOIN
|
JOIN
|
||||||
host h ON ja.host_id = h.id
|
hosts h ON ja.host_id = h.id
|
||||||
WHERE
|
WHERE
|
||||||
h.id = :host_id
|
h.id = :host_id
|
||||||
AND jat.description = :agent_type
|
AND jat.description = :agent_type
|
||||||
|
@ -520,13 +520,13 @@ class Agent {
|
||||||
jac.response_content,
|
jac.response_content,
|
||||||
COUNT(*) as checks_count
|
COUNT(*) as checks_count
|
||||||
FROM
|
FROM
|
||||||
jilo_agent_check jac
|
jilo_agent_checks jac
|
||||||
JOIN
|
JOIN
|
||||||
jilo_agent ja ON jac.agent_id = ja.id
|
jilo_agents ja ON jac.agent_id = ja.id
|
||||||
JOIN
|
JOIN
|
||||||
jilo_agent_type jat ON ja.agent_type_id = jat.id
|
jilo_agent_types jat ON ja.agent_type_id = jat.id
|
||||||
JOIN
|
JOIN
|
||||||
host h ON ja.host_id = h.id
|
hosts h ON ja.host_id = h.id
|
||||||
WHERE
|
WHERE
|
||||||
h.id = :host_id
|
h.id = :host_id
|
||||||
AND jat.description = :agent_type
|
AND jat.description = :agent_type
|
||||||
|
@ -591,13 +591,13 @@ class Agent {
|
||||||
jac.timestamp,
|
jac.timestamp,
|
||||||
jac.response_content
|
jac.response_content
|
||||||
FROM
|
FROM
|
||||||
jilo_agent_check jac
|
jilo_agent_checks jac
|
||||||
JOIN
|
JOIN
|
||||||
jilo_agent ja ON jac.agent_id = ja.id
|
jilo_agents ja ON jac.agent_id = ja.id
|
||||||
JOIN
|
JOIN
|
||||||
jilo_agent_type jat ON ja.agent_type_id = jat.id
|
jilo_agent_types jat ON ja.agent_type_id = jat.id
|
||||||
JOIN
|
JOIN
|
||||||
host h ON ja.host_id = h.id
|
hosts h ON ja.host_id = h.id
|
||||||
WHERE
|
WHERE
|
||||||
h.id = :host_id
|
h.id = :host_id
|
||||||
AND jat.description = :agent_type
|
AND jat.description = :agent_type
|
||||||
|
|
|
@ -94,11 +94,11 @@ class Component {
|
||||||
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
if (!empty($result)) {
|
if (!empty($result)) {
|
||||||
$logObject->log('info', "Retrieved " . count($result) . " Jitsi component events", ['user_id' => $userId, 'scope' => 'system']);
|
$logObject->insertLog(0, "Retrieved " . count($result) . " Jitsi component events");
|
||||||
}
|
}
|
||||||
return $result;
|
return $result;
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
$logObject->log('error', "Failed to retrieve Jitsi component events: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']);
|
$logObject->insertLog(0, "Failed to retrieve Jitsi component events: " . $e->getMessage());
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -162,7 +162,7 @@ class Component {
|
||||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
return (int)$result['total'];
|
return (int)$result['total'];
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
$logObject->log('error', "Failed to retrieve component events count: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']);
|
$logObject->insertLog(0, "Failed to retrieve component events count: " . $e->getMessage());
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ class Config {
|
||||||
* @return array Returns an array with 'success' and 'updated' keys on success, or 'success' and 'error' keys on failure.
|
* @return array Returns an array with 'success' and 'updated' keys on success, or 'success' and 'error' keys on failure.
|
||||||
*/
|
*/
|
||||||
public function editConfigFile($updatedConfig, $config_file) {
|
public function editConfigFile($updatedConfig, $config_file) {
|
||||||
global $logObject, $userId;
|
global $logObject, $user_id;
|
||||||
$allLogs = [];
|
$allLogs = [];
|
||||||
$updated = [];
|
$updated = [];
|
||||||
|
|
||||||
|
@ -140,7 +140,7 @@ class Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($allLogs)) {
|
if (!empty($allLogs)) {
|
||||||
$logObject->log('info', implode("\n", $allLogs), ['user_id' => $userId, 'scope' => 'system']);
|
$logObject->insertLog($user_id, implode("\n", $allLogs), 'system');
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
@ -148,7 +148,7 @@ class Config {
|
||||||
'updated' => $updated
|
'updated' => $updated
|
||||||
];
|
];
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$logObject->log('error', "Config update error: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']);
|
$logObject->insertLog($user_id, "Config update error: " . $e->getMessage(), 'system');
|
||||||
return [
|
return [
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'error' => $e->getMessage()
|
'error' => $e->getMessage()
|
||||||
|
|
|
@ -35,10 +35,6 @@ class Feedback {
|
||||||
'type' => self::TYPE_SUCCESS,
|
'type' => self::TYPE_SUCCESS,
|
||||||
'dismissible' => true
|
'dismissible' => true
|
||||||
],
|
],
|
||||||
'SESSION_TIMEOUT' => [
|
|
||||||
'type' => self::TYPE_ERROR,
|
|
||||||
'dismissible' => true
|
|
||||||
],
|
|
||||||
'IP_BLACKLISTED' => [
|
'IP_BLACKLISTED' => [
|
||||||
'type' => self::TYPE_ERROR,
|
'type' => self::TYPE_ERROR,
|
||||||
'dismissible' => false
|
'dismissible' => false
|
||||||
|
@ -53,21 +49,6 @@ class Feedback {
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
const REGISTER = [
|
|
||||||
'SUCCESS' => [
|
|
||||||
'type' => self::TYPE_SUCCESS,
|
|
||||||
'dismissible' => true
|
|
||||||
],
|
|
||||||
'FAILED' => [
|
|
||||||
'type' => self::TYPE_ERROR,
|
|
||||||
'dismissible' => true
|
|
||||||
],
|
|
||||||
'DISABLED' => [
|
|
||||||
'type' => self::TYPE_ERROR,
|
|
||||||
'dismissible' => false
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
const SECURITY = [
|
const SECURITY = [
|
||||||
'WHITELIST_ADD_SUCCESS' => [
|
'WHITELIST_ADD_SUCCESS' => [
|
||||||
'type' => self::TYPE_SUCCESS,
|
'type' => self::TYPE_SUCCESS,
|
||||||
|
@ -115,15 +96,19 @@ class Feedback {
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
const THEME = [
|
const REGISTER = [
|
||||||
'THEME_CHANGE_SUCCESS' => [
|
'SUCCESS' => [
|
||||||
'type' => self::TYPE_SUCCESS,
|
'type' => self::TYPE_SUCCESS,
|
||||||
'dismissible' => true
|
'dismissible' => true
|
||||||
],
|
],
|
||||||
'THEME_CHANGE_FAILED' => [
|
'FAILED' => [
|
||||||
'type' => self::TYPE_ERROR,
|
'type' => self::TYPE_ERROR,
|
||||||
'dismissible' => true
|
'dismissible' => true
|
||||||
]
|
],
|
||||||
|
'DISABLED' => [
|
||||||
|
'type' => self::TYPE_ERROR,
|
||||||
|
'dismissible' => false
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
const SYSTEM = [
|
const SYSTEM = [
|
||||||
|
|
|
@ -37,7 +37,7 @@ class Host {
|
||||||
platform_id,
|
platform_id,
|
||||||
name
|
name
|
||||||
FROM
|
FROM
|
||||||
host';
|
hosts';
|
||||||
|
|
||||||
if ($platform_id !== '' && $host_id !== '') {
|
if ($platform_id !== '' && $host_id !== '') {
|
||||||
$sql .= ' WHERE platform_id = :platform_id AND id = :host_id';
|
$sql .= ' WHERE platform_id = :platform_id AND id = :host_id';
|
||||||
|
@ -71,7 +71,7 @@ class Host {
|
||||||
*/
|
*/
|
||||||
public function addHost($newHost) {
|
public function addHost($newHost) {
|
||||||
try {
|
try {
|
||||||
$sql = 'INSERT INTO host
|
$sql = 'INSERT INTO hosts
|
||||||
(address, platform_id, name)
|
(address, platform_id, name)
|
||||||
VALUES
|
VALUES
|
||||||
(:address, :platform_id, :name)';
|
(:address, :platform_id, :name)';
|
||||||
|
@ -101,7 +101,7 @@ class Host {
|
||||||
*/
|
*/
|
||||||
public function editHost($platform_id, $updatedHost) {
|
public function editHost($platform_id, $updatedHost) {
|
||||||
try {
|
try {
|
||||||
$sql = 'UPDATE host SET
|
$sql = 'UPDATE hosts SET
|
||||||
address = :address,
|
address = :address,
|
||||||
name = :name
|
name = :name
|
||||||
WHERE
|
WHERE
|
||||||
|
@ -140,13 +140,13 @@ class Host {
|
||||||
$this->db->beginTransaction();
|
$this->db->beginTransaction();
|
||||||
|
|
||||||
// First delete all agents associated with this host
|
// First delete all agents associated with this host
|
||||||
$sql = 'DELETE FROM jilo_agent WHERE host_id = :host_id';
|
$sql = 'DELETE FROM jilo_agents WHERE host_id = :host_id';
|
||||||
$query = $this->db->prepare($sql);
|
$query = $this->db->prepare($sql);
|
||||||
$query->bindParam(':host_id', $host_id);
|
$query->bindParam(':host_id', $host_id);
|
||||||
$query->execute();
|
$query->execute();
|
||||||
|
|
||||||
// Then delete the host
|
// Then delete the host
|
||||||
$sql = 'DELETE FROM host WHERE id = :host_id';
|
$sql = 'DELETE FROM hosts WHERE id = :host_id';
|
||||||
$query = $this->db->prepare($sql);
|
$query = $this->db->prepare($sql);
|
||||||
$query->bindParam(':host_id', $host_id);
|
$query->bindParam(':host_id', $host_id);
|
||||||
$query->execute();
|
$query->execute();
|
||||||
|
|
|
@ -1,40 +1,122 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log wrapper that delegates to plugin Log or NullLogger fallback.
|
* class Log
|
||||||
* Used when code does require_once '../app/classes/log.php'.
|
*
|
||||||
|
* Handles logging events into a database and reading log entries.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// If there is already a Log plugin loaded
|
|
||||||
if (class_exists('Log')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load fallback NullLogger
|
|
||||||
require_once __DIR__ . '/../core/NullLogger.php';
|
|
||||||
|
|
||||||
class Log {
|
class Log {
|
||||||
private $logger;
|
/**
|
||||||
|
* @var PDO|null $db The database connection instance.
|
||||||
|
*/
|
||||||
|
private $db;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed $database Database or DatabaseConnector instance
|
* Logs constructor.
|
||||||
|
* Initializes the database connection.
|
||||||
|
*
|
||||||
|
* @param object $database The database object to initialize the connection.
|
||||||
*/
|
*/
|
||||||
public function __construct($database) {
|
public function __construct($database) {
|
||||||
global $logObject;
|
$this->db = $database->getConnection();
|
||||||
if (isset($logObject) && method_exists($logObject, 'insertLog')) {
|
}
|
||||||
$this->logger = $logObject;
|
|
||||||
} else {
|
/**
|
||||||
$this->logger = new \App\Core\NullLogger();
|
* Insert a log event into the database.
|
||||||
|
*
|
||||||
|
* @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($user_id, $message, $scope='user') {
|
||||||
|
try {
|
||||||
|
$sql = 'INSERT INTO logs
|
||||||
|
(user_id, scope, message)
|
||||||
|
VALUES
|
||||||
|
(:user_id, :scope, :message)';
|
||||||
|
|
||||||
|
$query = $this->db->prepare($sql);
|
||||||
|
$query->execute([
|
||||||
|
':user_id' => $user_id,
|
||||||
|
':scope' => $scope,
|
||||||
|
':message' => $message,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return $e->getMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PSR-3 compatible log method
|
* Retrieve log entries from the database.
|
||||||
* @param string $level
|
*
|
||||||
* @param string $message
|
* @param int $user_id The ID of the user whose logs are being retrieved.
|
||||||
* @param array $context
|
* @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.
|
||||||
|
* @param array $filters Optional array of filters (from_time, until_time, message, id)
|
||||||
|
*
|
||||||
|
* @return array An array of log entries.
|
||||||
*/
|
*/
|
||||||
public function log(string $level, string $message, array $context = []): void {
|
public function readLog($user_id, $scope, $offset=0, $items_per_page='', $filters=[]) {
|
||||||
$this->logger->log($level, $message, $context);
|
$params = [];
|
||||||
|
$where_clauses = [];
|
||||||
|
|
||||||
|
// Base query with user join
|
||||||
|
$base_sql = 'SELECT l.*, u.username
|
||||||
|
FROM logs l
|
||||||
|
LEFT JOIN users u ON l.user_id = u.id';
|
||||||
|
|
||||||
|
// Add scope condition
|
||||||
|
if ($scope === 'user') {
|
||||||
|
$where_clauses[] = 'l.user_id = :user_id';
|
||||||
|
$params[':user_id'] = $user_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add time range filters if specified
|
||||||
|
if (!empty($filters['from_time'])) {
|
||||||
|
$where_clauses[] = 'l.time >= :from_time';
|
||||||
|
$params[':from_time'] = $filters['from_time'] . ' 00:00:00';
|
||||||
|
}
|
||||||
|
if (!empty($filters['until_time'])) {
|
||||||
|
$where_clauses[] = 'l.time <= :until_time';
|
||||||
|
$params[':until_time'] = $filters['until_time'] . ' 23:59:59';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add message search if specified
|
||||||
|
if (!empty($filters['message'])) {
|
||||||
|
$where_clauses[] = 'l.message LIKE :message';
|
||||||
|
$params[':message'] = '%' . $filters['message'] . '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add user ID search if specified
|
||||||
|
if (!empty($filters['id'])) {
|
||||||
|
$where_clauses[] = 'l.user_id = :search_user_id';
|
||||||
|
$params[':search_user_id'] = $filters['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine WHERE clauses
|
||||||
|
$sql = $base_sql;
|
||||||
|
if (!empty($where_clauses)) {
|
||||||
|
$sql .= ' WHERE ' . implode(' AND ', $where_clauses);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add ordering
|
||||||
|
$sql .= ' ORDER BY l.time DESC';
|
||||||
|
|
||||||
|
// Add pagination
|
||||||
|
if ($items_per_page) {
|
||||||
|
$items_per_page = (int)$items_per_page;
|
||||||
|
$sql .= ' LIMIT ' . $offset . ',' . $items_per_page;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $this->db->prepare($sql);
|
||||||
|
$query->execute($params);
|
||||||
|
|
||||||
|
return $query->fetchAll(PDO::FETCH_ASSOC);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,8 +26,8 @@ class PasswordReset {
|
||||||
// Check if email exists
|
// Check if email exists
|
||||||
$query = $this->db->prepare("
|
$query = $this->db->prepare("
|
||||||
SELECT u.id, um.email
|
SELECT u.id, um.email
|
||||||
FROM user u
|
FROM users u
|
||||||
JOIN user_meta um ON u.id = um.user_id
|
JOIN users_meta um ON u.id = um.user_id
|
||||||
WHERE um.email = :email"
|
WHERE um.email = :email"
|
||||||
);
|
);
|
||||||
$query->bindParam(':email', $email);
|
$query->bindParam(':email', $email);
|
||||||
|
|
|
@ -30,7 +30,7 @@ class Platform {
|
||||||
* @return array An associative array containing platform details.
|
* @return array An associative array containing platform details.
|
||||||
*/
|
*/
|
||||||
public function getPlatformDetails($platform_id = '') {
|
public function getPlatformDetails($platform_id = '') {
|
||||||
$sql = 'SELECT * FROM platform';
|
$sql = 'SELECT * FROM platforms';
|
||||||
if ($platform_id !== '') {
|
if ($platform_id !== '') {
|
||||||
$sql .= ' WHERE id = :platform_id';
|
$sql .= ' WHERE id = :platform_id';
|
||||||
$query = $this->db->prepare($sql);
|
$query = $this->db->prepare($sql);
|
||||||
|
@ -57,7 +57,7 @@ class Platform {
|
||||||
*/
|
*/
|
||||||
public function addPlatform($newPlatform) {
|
public function addPlatform($newPlatform) {
|
||||||
try {
|
try {
|
||||||
$sql = 'INSERT INTO platform
|
$sql = 'INSERT INTO platforms
|
||||||
(name, jitsi_url, jilo_database)
|
(name, jitsi_url, jilo_database)
|
||||||
VALUES
|
VALUES
|
||||||
(:name, :jitsi_url, :jilo_database)';
|
(:name, :jitsi_url, :jilo_database)';
|
||||||
|
@ -90,7 +90,7 @@ class Platform {
|
||||||
*/
|
*/
|
||||||
public function editPlatform($platform_id, $updatedPlatform) {
|
public function editPlatform($platform_id, $updatedPlatform) {
|
||||||
try {
|
try {
|
||||||
$sql = 'UPDATE platform SET
|
$sql = 'UPDATE platforms SET
|
||||||
name = :name,
|
name = :name,
|
||||||
jitsi_url = :jitsi_url,
|
jitsi_url = :jitsi_url,
|
||||||
jilo_database = :jilo_database
|
jilo_database = :jilo_database
|
||||||
|
@ -125,7 +125,7 @@ class Platform {
|
||||||
$this->db->beginTransaction();
|
$this->db->beginTransaction();
|
||||||
|
|
||||||
// First, get all hosts in this platform
|
// First, get all hosts in this platform
|
||||||
$sql = 'SELECT id FROM host WHERE platform_id = :platform_id';
|
$sql = 'SELECT id FROM hosts WHERE platform_id = :platform_id';
|
||||||
$query = $this->db->prepare($sql);
|
$query = $this->db->prepare($sql);
|
||||||
$query->bindParam(':platform_id', $platform_id);
|
$query->bindParam(':platform_id', $platform_id);
|
||||||
$query->execute();
|
$query->execute();
|
||||||
|
@ -133,20 +133,20 @@ class Platform {
|
||||||
|
|
||||||
// Delete all agents for each host
|
// Delete all agents for each host
|
||||||
foreach ($hosts as $host) {
|
foreach ($hosts as $host) {
|
||||||
$sql = 'DELETE FROM jilo_agent WHERE host_id = :host_id';
|
$sql = 'DELETE FROM jilo_agents WHERE host_id = :host_id';
|
||||||
$query = $this->db->prepare($sql);
|
$query = $this->db->prepare($sql);
|
||||||
$query->bindParam(':host_id', $host['id']);
|
$query->bindParam(':host_id', $host['id']);
|
||||||
$query->execute();
|
$query->execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete all hosts in this platform
|
// Delete all hosts in this platform
|
||||||
$sql = 'DELETE FROM host WHERE platform_id = :platform_id';
|
$sql = 'DELETE FROM hosts WHERE platform_id = :platform_id';
|
||||||
$query = $this->db->prepare($sql);
|
$query = $this->db->prepare($sql);
|
||||||
$query->bindParam(':platform_id', $platform_id);
|
$query->bindParam(':platform_id', $platform_id);
|
||||||
$query->execute();
|
$query->execute();
|
||||||
|
|
||||||
// Finally, delete the platform
|
// Finally, delete the platform
|
||||||
$sql = 'DELETE FROM platform WHERE id = :platform_id';
|
$sql = 'DELETE FROM platforms WHERE id = :platform_id';
|
||||||
$query = $this->db->prepare($sql);
|
$query = $this->db->prepare($sql);
|
||||||
$query->bindParam(':platform_id', $platform_id);
|
$query->bindParam(':platform_id', $platform_id);
|
||||||
$query->execute();
|
$query->execute();
|
||||||
|
|
|
@ -1,20 +1,16 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Core\NullLogger;
|
|
||||||
|
|
||||||
class RateLimiter {
|
class RateLimiter {
|
||||||
public $db;
|
public $db;
|
||||||
private $database;
|
private $log;
|
||||||
/** @var mixed NullLogger (or PSR-3 logger) or plugin Log */
|
|
||||||
private $logger;
|
|
||||||
public $maxAttempts = 5; // Maximum login attempts
|
public $maxAttempts = 5; // Maximum login attempts
|
||||||
public $decayMinutes = 15; // Time window in minutes
|
public $decayMinutes = 15; // Time window in minutes
|
||||||
public $autoBlacklistThreshold = 10; // Attempts before auto-blacklist
|
public $autoBlacklistThreshold = 10; // Attempts before auto-blacklist
|
||||||
public $autoBlacklistDuration = 24; // Hours to blacklist for
|
public $autoBlacklistDuration = 24; // Hours to blacklist for
|
||||||
public $authRatelimitTable = 'security_rate_auth'; // For rate limiting username/password attempts
|
public $authRatelimitTable = 'login_attempts'; // For username/password attempts
|
||||||
public $pagesRatelimitTable = 'security_rate_page'; // For rate limiting page requests
|
public $pagesRatelimitTable = 'pages_rate_limits'; // For page requests
|
||||||
public $whitelistTable = 'security_ip_whitelist'; // For whitelisting IPs and network ranges
|
public $whitelistTable = 'ip_whitelist';
|
||||||
public $blacklistTable = 'security_ip_blacklist'; // For blacklisting IPs and network ranges
|
public $blacklistTable = 'ip_blacklist';
|
||||||
private $pageLimits = [
|
private $pageLimits = [
|
||||||
// Default rate limits per minute
|
// Default rate limits per minute
|
||||||
'default' => 60,
|
'default' => 60,
|
||||||
|
@ -26,23 +22,9 @@ class RateLimiter {
|
||||||
'config' => 10
|
'config' => 10
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
public function __construct($database) {
|
||||||
* @param mixed $database Database object
|
|
||||||
* @param mixed $logger Optional NullLogger (or PSR-3 logger) or plugin Log
|
|
||||||
*/
|
|
||||||
public function __construct($database, $logger = null) {
|
|
||||||
$this->database = $database;
|
|
||||||
$this->db = $database->getConnection();
|
$this->db = $database->getConnection();
|
||||||
// Initialize logger (plugin Log if present or NullLogger otherwise)
|
$this->log = new Log($database);
|
||||||
if ($logger !== null) {
|
|
||||||
$this->logger = $logger;
|
|
||||||
} else {
|
|
||||||
global $logObject;
|
|
||||||
$this->logger = isset($logObject) && is_object($logObject) && method_exists($logObject, 'info')
|
|
||||||
? $logObject
|
|
||||||
: new NullLogger();
|
|
||||||
}
|
|
||||||
// Initialize database tables
|
|
||||||
$this->createTablesIfNotExist();
|
$this->createTablesIfNotExist();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,47 +32,42 @@ class RateLimiter {
|
||||||
private function createTablesIfNotExist() {
|
private function createTablesIfNotExist() {
|
||||||
// Authentication attempts table
|
// Authentication attempts table
|
||||||
$sql = "CREATE TABLE IF NOT EXISTS {$this->authRatelimitTable} (
|
$sql = "CREATE TABLE IF NOT EXISTS {$this->authRatelimitTable} (
|
||||||
id int(11) PRIMARY KEY AUTO_INCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
ip_address VARCHAR(45) NOT NULL,
|
ip_address TEXT NOT NULL,
|
||||||
username VARCHAR(255) NOT NULL,
|
username TEXT NOT NULL,
|
||||||
attempted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
attempted_at TEXT DEFAULT (DATETIME('now'))
|
||||||
INDEX idx_ip_username (ip_address, username)
|
|
||||||
)";
|
)";
|
||||||
$this->db->exec($sql);
|
$this->db->exec($sql);
|
||||||
|
|
||||||
// Pages rate limits table
|
// Pages rate limits table
|
||||||
$sql = "CREATE TABLE IF NOT EXISTS {$this->pagesRatelimitTable} (
|
$sql = "CREATE TABLE IF NOT EXISTS {$this->pagesRatelimitTable} (
|
||||||
id int(11) PRIMARY KEY AUTO_INCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
ip_address VARCHAR(45) NOT NULL,
|
ip_address TEXT NOT NULL,
|
||||||
endpoint VARCHAR(255) NOT NULL,
|
endpoint TEXT NOT NULL,
|
||||||
request_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
request_time DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
INDEX idx_ip_endpoint (ip_address, endpoint),
|
|
||||||
INDEX idx_request_time (request_time)
|
|
||||||
)";
|
)";
|
||||||
$this->db->exec($sql);
|
$this->db->exec($sql);
|
||||||
|
|
||||||
// IP whitelist table
|
// IP whitelist table
|
||||||
$sql = "CREATE TABLE IF NOT EXISTS {$this->whitelistTable} (
|
$sql = "CREATE TABLE IF NOT EXISTS {$this->whitelistTable} (
|
||||||
id int(11) PRIMARY KEY AUTO_INCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
ip_address VARCHAR(45) NOT NULL,
|
ip_address TEXT NOT NULL UNIQUE,
|
||||||
is_network BOOLEAN DEFAULT FALSE,
|
is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)),
|
||||||
description VARCHAR(255),
|
description TEXT,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TEXT DEFAULT (DATETIME('now')),
|
||||||
created_by VARCHAR(255),
|
created_by TEXT
|
||||||
UNIQUE KEY unique_ip (ip_address)
|
|
||||||
)";
|
)";
|
||||||
$this->db->exec($sql);
|
$this->db->exec($sql);
|
||||||
|
|
||||||
// IP blacklist table
|
// IP blacklist table
|
||||||
$sql = "CREATE TABLE IF NOT EXISTS {$this->blacklistTable} (
|
$sql = "CREATE TABLE IF NOT EXISTS {$this->blacklistTable} (
|
||||||
id int(11) PRIMARY KEY AUTO_INCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
ip_address VARCHAR(45) NOT NULL,
|
ip_address TEXT NOT NULL UNIQUE,
|
||||||
is_network BOOLEAN DEFAULT FALSE,
|
is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)),
|
||||||
reason VARCHAR(255),
|
reason TEXT,
|
||||||
expiry_time TIMESTAMP NULL,
|
expiry_time TEXT NULL,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TEXT DEFAULT (DATETIME('now')),
|
||||||
created_by VARCHAR(255),
|
created_by TEXT
|
||||||
UNIQUE KEY unique_ip (ip_address)
|
|
||||||
)";
|
)";
|
||||||
$this->db->exec($sql);
|
$this->db->exec($sql);
|
||||||
|
|
||||||
|
@ -104,7 +81,7 @@ class RateLimiter {
|
||||||
];
|
];
|
||||||
|
|
||||||
// Insert default whitelisted IPs if they don't exist
|
// Insert default whitelisted IPs if they don't exist
|
||||||
$stmt = $this->db->prepare("INSERT IGNORE INTO {$this->whitelistTable}
|
$stmt = $this->db->prepare("INSERT OR IGNORE INTO {$this->whitelistTable}
|
||||||
(ip_address, is_network, description, created_by)
|
(ip_address, is_network, description, created_by)
|
||||||
VALUES (?, ?, ?, 'system')");
|
VALUES (?, ?, ?, 'system')");
|
||||||
foreach ($defaultIps as $ip) {
|
foreach ($defaultIps as $ip) {
|
||||||
|
@ -120,7 +97,7 @@ class RateLimiter {
|
||||||
['203.0.113.0/24', true, 'TEST-NET-3 Documentation space - RFC 5737']
|
['203.0.113.0/24', true, 'TEST-NET-3 Documentation space - RFC 5737']
|
||||||
];
|
];
|
||||||
|
|
||||||
$stmt = $this->db->prepare("INSERT IGNORE INTO {$this->blacklistTable}
|
$stmt = $this->db->prepare("INSERT OR IGNORE INTO {$this->blacklistTable}
|
||||||
(ip_address, is_network, reason, created_by)
|
(ip_address, is_network, reason, created_by)
|
||||||
VALUES (?, ?, ?, 'system')");
|
VALUES (?, ?, ?, 'system')");
|
||||||
|
|
||||||
|
@ -135,7 +112,7 @@ class RateLimiter {
|
||||||
*/
|
*/
|
||||||
public function getRecentAttempts($ip) {
|
public function getRecentAttempts($ip) {
|
||||||
$stmt = $this->db->prepare("SELECT COUNT(*) as attempts FROM {$this->authRatelimitTable}
|
$stmt = $this->db->prepare("SELECT COUNT(*) as attempts FROM {$this->authRatelimitTable}
|
||||||
WHERE ip_address = ? AND attempted_at > DATE_SUB(NOW(), INTERVAL ? MINUTE)");
|
WHERE ip_address = ? AND attempted_at > datetime('now', '-' || :minutes || ' minutes')");
|
||||||
$stmt->execute([$ip, $this->decayMinutes]);
|
$stmt->execute([$ip, $this->decayMinutes]);
|
||||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
return intval($result['attempts']);
|
return intval($result['attempts']);
|
||||||
|
@ -232,19 +209,15 @@ class RateLimiter {
|
||||||
if ($this->isIpBlacklisted($ip)) {
|
if ($this->isIpBlacklisted($ip)) {
|
||||||
$message = "Cannot whitelist {$ip} - IP is currently blacklisted";
|
$message = "Cannot whitelist {$ip} - IP is currently blacklisted";
|
||||||
if ($userId) {
|
if ($userId) {
|
||||||
$this->logger->log('info', "IP Whitelist: {$message}", ['user_id' => $userId, 'scope' => 'system']);
|
$this->log->insertLog($userId, "IP Whitelist: {$message}", 'system');
|
||||||
Feedback::flash('ERROR', 'DEFAULT', $message);
|
Feedback::flash('ERROR', 'DEFAULT', $message);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt = $this->db->prepare("INSERT INTO {$this->whitelistTable}
|
$stmt = $this->db->prepare("INSERT OR REPLACE INTO {$this->whitelistTable}
|
||||||
(ip_address, is_network, description, created_by)
|
(ip_address, is_network, description, created_by)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?)");
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
is_network = VALUES(is_network),
|
|
||||||
description = VALUES(description),
|
|
||||||
created_by = VALUES(created_by)");
|
|
||||||
|
|
||||||
$result = $stmt->execute([$ip, $isNetwork, $description, $createdBy]);
|
$result = $stmt->execute([$ip, $isNetwork, $description, $createdBy]);
|
||||||
|
|
||||||
|
@ -256,14 +229,14 @@ class RateLimiter {
|
||||||
$createdBy,
|
$createdBy,
|
||||||
$description
|
$description
|
||||||
);
|
);
|
||||||
$this->logger->log('info', $logMessage, ['user_id' => $userId ?? null, 'scope' => 'system']);
|
$this->log->insertLog($userId ?? 0, $logMessage, 'system');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
if ($userId) {
|
if ($userId) {
|
||||||
$this->logger->log('error', "IP Whitelist: Failed to add {$ip}: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']);
|
$this->log->insertLog($userId, "IP Whitelist: Failed to add {$ip}: " . $e->getMessage(), 'system');
|
||||||
Feedback::flash('ERROR', 'DEFAULT', "IP Whitelist: Failed to add {$ip}: " . $e->getMessage());
|
Feedback::flash('ERROR', 'DEFAULT', "IP Whitelist: Failed to add {$ip}: " . $e->getMessage());
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -291,14 +264,14 @@ class RateLimiter {
|
||||||
$removedBy,
|
$removedBy,
|
||||||
$ipDetails['created_by']
|
$ipDetails['created_by']
|
||||||
);
|
);
|
||||||
$this->logger->log('info', $logMessage, ['user_id' => $userId ?? null, 'scope' => 'system']);
|
$this->log->insertLog($userId ?? 0, $logMessage, 'system');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
if ($userId) {
|
if ($userId) {
|
||||||
$this->logger->log('error', "IP Whitelist: Failed to remove {$ip}: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']);
|
$this->log->insertLog($userId, "IP Whitelist: Failed to remove {$ip}: " . $e->getMessage(), 'system');
|
||||||
Feedback::flash('ERROR', 'DEFAULT', "IP Whitelist: Failed to remove {$ip}: " . $e->getMessage());
|
Feedback::flash('ERROR', 'DEFAULT', "IP Whitelist: Failed to remove {$ip}: " . $e->getMessage());
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -311,7 +284,7 @@ class RateLimiter {
|
||||||
if ($this->isIpWhitelisted($ip)) {
|
if ($this->isIpWhitelisted($ip)) {
|
||||||
$message = "Cannot blacklist {$ip} - IP is currently whitelisted";
|
$message = "Cannot blacklist {$ip} - IP is currently whitelisted";
|
||||||
if ($userId) {
|
if ($userId) {
|
||||||
$this->logger->log('info', "IP Blacklist: {$message}", ['user_id' => $userId, 'scope' => 'system']);
|
$this->log->insertLog($userId, "IP Blacklist: {$message}", 'system');
|
||||||
Feedback::flash('ERROR', 'DEFAULT', $message);
|
Feedback::flash('ERROR', 'DEFAULT', $message);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -319,14 +292,9 @@ class RateLimiter {
|
||||||
|
|
||||||
$expiryTime = $expiryHours ? date('Y-m-d H:i:s', strtotime("+{$expiryHours} hours")) : null;
|
$expiryTime = $expiryHours ? date('Y-m-d H:i:s', strtotime("+{$expiryHours} hours")) : null;
|
||||||
|
|
||||||
$stmt = $this->db->prepare("INSERT INTO {$this->blacklistTable}
|
$stmt = $this->db->prepare("INSERT OR REPLACE INTO {$this->blacklistTable}
|
||||||
(ip_address, is_network, reason, expiry_time, created_by)
|
(ip_address, is_network, reason, expiry_time, created_by)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)");
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
is_network = VALUES(is_network),
|
|
||||||
reason = VALUES(reason),
|
|
||||||
expiry_time = VALUES(expiry_time),
|
|
||||||
created_by = VALUES(created_by)");
|
|
||||||
|
|
||||||
$result = $stmt->execute([$ip, $isNetwork, $reason, $expiryTime, $createdBy]);
|
$result = $stmt->execute([$ip, $isNetwork, $reason, $expiryTime, $createdBy]);
|
||||||
|
|
||||||
|
@ -339,13 +307,13 @@ class RateLimiter {
|
||||||
$reason,
|
$reason,
|
||||||
$expiryTime ?? 'never'
|
$expiryTime ?? 'never'
|
||||||
);
|
);
|
||||||
$this->logger->log('info', $logMessage, ['user_id' => $userId ?? null, 'scope' => 'system']);
|
$this->log->insertLog($userId ?? 0, $logMessage, 'system');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
if ($userId) {
|
if ($userId) {
|
||||||
$this->logger->log('error', "IP Blacklist: Failed to add {$ip}: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']);
|
$this->log->insertLog($userId, "IP Blacklist: Failed to add {$ip}: " . $e->getMessage(), 'system');
|
||||||
Feedback::flash('ERROR', 'DEFAULT', "IP Blacklist: Failed to add {$ip}: " . $e->getMessage());
|
Feedback::flash('ERROR', 'DEFAULT', "IP Blacklist: Failed to add {$ip}: " . $e->getMessage());
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -373,13 +341,13 @@ class RateLimiter {
|
||||||
$ipDetails['created_by'],
|
$ipDetails['created_by'],
|
||||||
$ipDetails['reason']
|
$ipDetails['reason']
|
||||||
);
|
);
|
||||||
$this->logger->log('info', $logMessage, ['user_id' => $userId ?? null, 'scope' => 'system']);
|
$this->log->insertLog($userId ?? 0, $logMessage, 'system');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
if ($userId) {
|
if ($userId) {
|
||||||
$this->logger->log('error', "IP Blacklist: Failed to remove {$ip}: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']);
|
$this->log->insertLog($userId, "IP Blacklist: Failed to remove {$ip}: " . $e->getMessage(), 'system');
|
||||||
Feedback::flash('ERROR', 'DEFAULT', "IP Blacklist: Failed to remove {$ip}: " . $e->getMessage());
|
Feedback::flash('ERROR', 'DEFAULT', "IP Blacklist: Failed to remove {$ip}: " . $e->getMessage());
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -404,17 +372,17 @@ class RateLimiter {
|
||||||
try {
|
try {
|
||||||
// Remove expired blacklist entries
|
// Remove expired blacklist entries
|
||||||
$stmt = $this->db->prepare("DELETE FROM {$this->blacklistTable}
|
$stmt = $this->db->prepare("DELETE FROM {$this->blacklistTable}
|
||||||
WHERE expiry_time IS NOT NULL AND expiry_time < NOW()");
|
WHERE expiry_time IS NOT NULL AND expiry_time < datetime('now')");
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
|
|
||||||
// Clean old login attempts
|
// Clean old login attempts
|
||||||
$stmt = $this->db->prepare("DELETE FROM {$this->authRatelimitTable}
|
$stmt = $this->db->prepare("DELETE FROM {$this->authRatelimitTable}
|
||||||
WHERE attempted_at < DATE_SUB(NOW(), INTERVAL :minutes MINUTE)");
|
WHERE attempted_at < datetime('now', '-' || :minutes || ' minutes')");
|
||||||
$stmt->execute([':minutes' => $this->decayMinutes]);
|
$stmt->execute([':minutes' => $this->decayMinutes]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$this->logger->log('error', "Failed to cleanup expired entries: " . $e->getMessage(), ['user_id' => $userId ?? null, 'scope' => 'system']);
|
$this->log->insertLog(0, "Failed to cleanup expired entries: " . $e->getMessage(), 'system');
|
||||||
Feedback::flash('ERROR', 'DEFAULT', "Failed to cleanup expired entries: " . $e->getMessage());
|
Feedback::flash('ERROR', 'DEFAULT', "Failed to cleanup expired entries: " . $e->getMessage());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -443,7 +411,7 @@ class RateLimiter {
|
||||||
$sql = "SELECT COUNT(*) as total_attempts
|
$sql = "SELECT COUNT(*) as total_attempts
|
||||||
FROM {$this->authRatelimitTable}
|
FROM {$this->authRatelimitTable}
|
||||||
WHERE ip_address = :ip
|
WHERE ip_address = :ip
|
||||||
AND attempted_at > DATE_SUB(NOW(), INTERVAL :minutes MINUTE)";
|
AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
|
||||||
$stmt = $this->db->prepare($sql);
|
$stmt = $this->db->prepare($sql);
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
':ip' => $ipAddress,
|
':ip' => $ipAddress,
|
||||||
|
@ -455,12 +423,7 @@ class RateLimiter {
|
||||||
return $result['total_attempts'] < $this->autoBlacklistThreshold;
|
return $result['total_attempts'] < $this->autoBlacklistThreshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function attempt($username, $ipAddress, $failed = true) {
|
public function attempt($username, $ipAddress) {
|
||||||
// Only record failed attempts
|
|
||||||
if (!$failed) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record this attempt
|
// Record this attempt
|
||||||
$sql = "INSERT INTO {$this->authRatelimitTable} (ip_address, username) VALUES (:ip, :username)";
|
$sql = "INSERT INTO {$this->authRatelimitTable} (ip_address, username) VALUES (:ip, :username)";
|
||||||
$stmt = $this->db->prepare($sql);
|
$stmt = $this->db->prepare($sql);
|
||||||
|
@ -481,7 +444,7 @@ class RateLimiter {
|
||||||
FROM {$this->authRatelimitTable}
|
FROM {$this->authRatelimitTable}
|
||||||
WHERE ip_address = :ip
|
WHERE ip_address = :ip
|
||||||
AND username = :username
|
AND username = :username
|
||||||
AND attempted_at > DATE_SUB(NOW(), INTERVAL :minutes MINUTE)";
|
AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
|
||||||
|
|
||||||
$stmt = $this->db->prepare($sql);
|
$stmt = $this->db->prepare($sql);
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
|
@ -517,7 +480,7 @@ class RateLimiter {
|
||||||
|
|
||||||
public function clearOldAttempts() {
|
public function clearOldAttempts() {
|
||||||
$sql = "DELETE FROM {$this->authRatelimitTable}
|
$sql = "DELETE FROM {$this->authRatelimitTable}
|
||||||
WHERE attempted_at < DATE_SUB(NOW(), INTERVAL :minutes MINUTE)";
|
WHERE attempted_at < datetime('now', '-' || :minutes || ' minutes')";
|
||||||
|
|
||||||
$stmt = $this->db->prepare($sql);
|
$stmt = $this->db->prepare($sql);
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
|
@ -530,7 +493,7 @@ class RateLimiter {
|
||||||
FROM {$this->authRatelimitTable}
|
FROM {$this->authRatelimitTable}
|
||||||
WHERE ip_address = :ip
|
WHERE ip_address = :ip
|
||||||
AND username = :username
|
AND username = :username
|
||||||
AND attempted_at > DATE_SUB(NOW(), INTERVAL :minutes MINUTE)";
|
AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
|
||||||
|
|
||||||
$stmt = $this->db->prepare($sql);
|
$stmt = $this->db->prepare($sql);
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
|
@ -572,7 +535,7 @@ class RateLimiter {
|
||||||
FROM {$this->pagesRatelimitTable}
|
FROM {$this->pagesRatelimitTable}
|
||||||
WHERE ip_address = :ip
|
WHERE ip_address = :ip
|
||||||
AND endpoint = :endpoint
|
AND endpoint = :endpoint
|
||||||
AND request_time >= DATE_SUB(NOW(), INTERVAL 1 MINUTE)";
|
AND request_time >= DATETIME('now', '-1 minute')";
|
||||||
|
|
||||||
$stmt = $this->db->prepare($sql);
|
$stmt = $this->db->prepare($sql);
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
|
@ -603,7 +566,7 @@ class RateLimiter {
|
||||||
*/
|
*/
|
||||||
private function cleanOldPageRequests() {
|
private function cleanOldPageRequests() {
|
||||||
$sql = "DELETE FROM {$this->pagesRatelimitTable}
|
$sql = "DELETE FROM {$this->pagesRatelimitTable}
|
||||||
WHERE request_time < DATE_SUB(NOW(), INTERVAL 1 MINUTE)";
|
WHERE request_time < DATETIME('now', '-1 minute')";
|
||||||
|
|
||||||
$stmt = $this->db->prepare($sql);
|
$stmt = $this->db->prepare($sql);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
|
@ -615,10 +578,8 @@ class RateLimiter {
|
||||||
private function getPageLimitForEndpoint($endpoint, $userId = null) {
|
private function getPageLimitForEndpoint($endpoint, $userId = null) {
|
||||||
// Admin users get higher limits
|
// Admin users get higher limits
|
||||||
if ($userId) {
|
if ($userId) {
|
||||||
// Check admin rights directly from database
|
$userObj = new User($this->db);
|
||||||
$stmt = $this->db->prepare('SELECT COUNT(*) FROM `user_right` ur JOIN `right` r ON ur.right_id = r.id WHERE ur.user_id = ? AND r.name = ?');
|
if ($userObj->hasRight($userId, 'admin')) {
|
||||||
$stmt->execute([$userId, 'superuser']);
|
|
||||||
if ($stmt->fetchColumn() > 0) {
|
|
||||||
return $this->pageLimits['admin'];
|
return $this->pageLimits['admin'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -654,7 +615,7 @@ class RateLimiter {
|
||||||
FROM {$this->pagesRatelimitTable}
|
FROM {$this->pagesRatelimitTable}
|
||||||
WHERE ip_address = :ip
|
WHERE ip_address = :ip
|
||||||
AND endpoint = :endpoint
|
AND endpoint = :endpoint
|
||||||
AND request_time > DATE_SUB(NOW(), INTERVAL 1 MINUTE)";
|
AND request_time > DATETIME('now', '-1 minute')";
|
||||||
|
|
||||||
$stmt = $this->db->prepare($sql);
|
$stmt = $this->db->prepare($sql);
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
|
|
|
@ -1,265 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Session Class
|
|
||||||
*
|
|
||||||
* Core session management functionality for the application
|
|
||||||
*/
|
|
||||||
class Session {
|
|
||||||
private static $initialized = false;
|
|
||||||
private static $sessionName = ''; // Will be set from config, if not we'll have a random session name
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a random session name
|
|
||||||
*/
|
|
||||||
private static function generateRandomSessionName(): string {
|
|
||||||
return 'sess_' . bin2hex(random_bytes(8)); // 16-character random string
|
|
||||||
}
|
|
||||||
private static $sessionOptions = [
|
|
||||||
'cookie_httponly' => 1,
|
|
||||||
'cookie_secure' => 1,
|
|
||||||
'cookie_samesite' => 'Strict',
|
|
||||||
'gc_maxlifetime' => 7200 // 2 hours
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize session configuration
|
|
||||||
*/
|
|
||||||
private static function initialize() {
|
|
||||||
if (self::$initialized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
global $config;
|
|
||||||
|
|
||||||
// Get session name from config or generate a random one
|
|
||||||
self::$sessionName = $config['session']['name'] ?? self::generateRandomSessionName();
|
|
||||||
|
|
||||||
// Set session name before starting the session
|
|
||||||
session_name(self::$sessionName);
|
|
||||||
|
|
||||||
// Set session cookie parameters
|
|
||||||
$thisPath = $config['folder'] ?? '/';
|
|
||||||
$thisDomain = $config['domain'] ?? '';
|
|
||||||
$isSecure = isset($_SERVER['HTTPS']);
|
|
||||||
|
|
||||||
session_set_cookie_params([
|
|
||||||
'lifetime' => 0, // Session cookie (browser session)
|
|
||||||
'path' => $thisPath,
|
|
||||||
'domain' => $thisDomain,
|
|
||||||
'secure' => $isSecure,
|
|
||||||
'httponly' => true,
|
|
||||||
'samesite' => 'Strict'
|
|
||||||
]);
|
|
||||||
|
|
||||||
self::$initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get session name from config or generate a random one
|
|
||||||
*/
|
|
||||||
private static function getSessionNameFromConfig($config) {
|
|
||||||
if (isset($config['session']['name']) && !empty($config['session']['name'])) {
|
|
||||||
return $config['session']['name'];
|
|
||||||
}
|
|
||||||
return self::generateRandomSessionName();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start or resume a session with secure options
|
|
||||||
*/
|
|
||||||
public static function startSession() {
|
|
||||||
self::initialize();
|
|
||||||
|
|
||||||
if (session_status() === PHP_SESSION_NONE) {
|
|
||||||
if (!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
|
|
||||||
*
|
|
||||||
* @param bool $strict If true, will return false for new/unauthenticated sessions
|
|
||||||
* @return bool True if session is valid, false otherwise
|
|
||||||
*/
|
|
||||||
public static function isValidSession($strict = true) {
|
|
||||||
// If session is not started or empty, it's not valid
|
|
||||||
if (session_status() !== PHP_SESSION_ACTIVE || empty($_SESSION)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In non-strict mode, consider empty session as valid (for login/logout)
|
|
||||||
if (!$strict && !isset($_SESSION['user_id']) && !isset($_SESSION['username'])) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In strict mode, require user_id and username
|
|
||||||
if ($strict && (!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) {
|
|
||||||
// Ensure session is started
|
|
||||||
self::startSession();
|
|
||||||
|
|
||||||
// Set session variables
|
|
||||||
$_SESSION['user_id'] = $userId;
|
|
||||||
$_SESSION['username'] = $username;
|
|
||||||
$_SESSION['LAST_ACTIVITY'] = time();
|
|
||||||
$_SESSION['REMEMBER_ME'] = $rememberMe;
|
|
||||||
|
|
||||||
// Set cookie lifetime based on remember me
|
|
||||||
$cookieLifetime = $rememberMe ? time() + (30 * 24 * 60 * 60) : 0;
|
|
||||||
|
|
||||||
// Update session cookie with remember me setting
|
|
||||||
if (!headers_sent()) {
|
|
||||||
setcookie(
|
|
||||||
session_name(),
|
|
||||||
session_id(),
|
|
||||||
[
|
|
||||||
'expires' => $cookieLifetime,
|
|
||||||
'path' => $config['folder'] ?? '/',
|
|
||||||
'domain' => $config['domain'] ?? '',
|
|
||||||
'secure' => isset($_SERVER['HTTPS']),
|
|
||||||
'httponly' => true,
|
|
||||||
'samesite' => 'Strict'
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set username cookie
|
|
||||||
setcookie('username', $username, [
|
|
||||||
'expires' => $cookieLifetime,
|
|
||||||
'path' => $config['folder'] ?? '/',
|
|
||||||
'domain' => $config['domain'] ?? '',
|
|
||||||
'secure' => isset($_SERVER['HTTPS']),
|
|
||||||
'httponly' => true,
|
|
||||||
'samesite' => 'Strict'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
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'])
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,7 +3,7 @@
|
||||||
/**
|
/**
|
||||||
* class User
|
* class User
|
||||||
*
|
*
|
||||||
* Handles user-related functionalities such as login, rights management, and profile updates.
|
* Handles user-related functionalities such as registration, login, rights management, and profile updates.
|
||||||
*/
|
*/
|
||||||
class User {
|
class User {
|
||||||
/**
|
/**
|
||||||
|
@ -33,6 +33,63 @@ class User {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a new user.
|
||||||
|
*
|
||||||
|
* @param string $username The username of the new user.
|
||||||
|
* @param string $password The password for the new user.
|
||||||
|
*
|
||||||
|
* @return bool|string True if registration is successful, error message otherwise.
|
||||||
|
*/
|
||||||
|
public function register($username, $password) {
|
||||||
|
try {
|
||||||
|
// we have two inserts, start a transaction
|
||||||
|
$this->db->beginTransaction();
|
||||||
|
|
||||||
|
// hash the password, don't store it plain
|
||||||
|
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
|
||||||
|
// insert into users table
|
||||||
|
$sql = 'INSERT
|
||||||
|
INTO users (username, password)
|
||||||
|
VALUES (:username, :password)';
|
||||||
|
$query = $this->db->prepare($sql);
|
||||||
|
$query->bindValue(':username', $username);
|
||||||
|
$query->bindValue(':password', $hashedPassword);
|
||||||
|
|
||||||
|
// execute the first query
|
||||||
|
if (!$query->execute()) {
|
||||||
|
// rollback on error
|
||||||
|
$this->db->rollBack();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert the last user id into users_meta table
|
||||||
|
$sql2 = 'INSERT
|
||||||
|
INTO users_meta (user_id)
|
||||||
|
VALUES (:user_id)';
|
||||||
|
$query2 = $this->db->prepare($sql2);
|
||||||
|
$query2->bindValue(':user_id', $this->db->lastInsertId());
|
||||||
|
|
||||||
|
// execute the second query
|
||||||
|
if (!$query2->execute()) {
|
||||||
|
// rollback on error
|
||||||
|
$this->db->rollBack();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if all is OK, commit the transaction
|
||||||
|
$this->db->commit();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// rollback on any error
|
||||||
|
$this->db->rollBack();
|
||||||
|
return $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs in a user by verifying credentials.
|
* Logs in a user by verifying credentials.
|
||||||
*
|
*
|
||||||
|
@ -44,8 +101,12 @@ class User {
|
||||||
*/
|
*/
|
||||||
public function login($username, $password, $twoFactorCode = null) {
|
public function login($username, $password, $twoFactorCode = null) {
|
||||||
// Get user's IP address
|
// Get user's IP address
|
||||||
|
require_once __DIR__ . '/../helpers/logs.php';
|
||||||
$ipAddress = getUserIP();
|
$ipAddress = getUserIP();
|
||||||
|
|
||||||
|
// Record attempt
|
||||||
|
$this->rateLimiter->attempt($username, $ipAddress);
|
||||||
|
|
||||||
// Check rate limiting first
|
// Check rate limiting first
|
||||||
if (!$this->rateLimiter->isAllowed($username, $ipAddress)) {
|
if (!$this->rateLimiter->isAllowed($username, $ipAddress)) {
|
||||||
$remainingTime = $this->rateLimiter->getDecayMinutes();
|
$remainingTime = $this->rateLimiter->getDecayMinutes();
|
||||||
|
@ -53,7 +114,7 @@ class User {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then check credentials
|
// Then check credentials
|
||||||
$query = $this->db->prepare("SELECT * FROM user WHERE username = :username");
|
$query = $this->db->prepare("SELECT * FROM users WHERE username = :username");
|
||||||
$query->bindParam(':username', $username);
|
$query->bindParam(':username', $username);
|
||||||
$query->execute();
|
$query->execute();
|
||||||
|
|
||||||
|
@ -92,10 +153,7 @@ class User {
|
||||||
|
|
||||||
// Get remaining attempts AFTER this failed attempt
|
// Get remaining attempts AFTER this failed attempt
|
||||||
$remainingAttempts = $this->rateLimiter->getRemainingAttempts($username, $ipAddress);
|
$remainingAttempts = $this->rateLimiter->getRemainingAttempts($username, $ipAddress);
|
||||||
return [
|
throw new Exception("Invalid credentials. {$remainingAttempts} attempts remaining.");
|
||||||
'status' => 'failed',
|
|
||||||
'message' => "Invalid credentials. {$remainingAttempts} attempts remaining."
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -108,7 +166,7 @@ class User {
|
||||||
*/
|
*/
|
||||||
// FIXME not used now?
|
// FIXME not used now?
|
||||||
public function getUserId($username) {
|
public function getUserId($username) {
|
||||||
$sql = 'SELECT id FROM user WHERE username = :username';
|
$sql = 'SELECT id FROM users WHERE username = :username';
|
||||||
$query = $this->db->prepare($sql);
|
$query = $this->db->prepare($sql);
|
||||||
$query->bindParam(':username', $username);
|
$query->bindParam(':username', $username);
|
||||||
|
|
||||||
|
@ -122,24 +180,24 @@ class User {
|
||||||
/**
|
/**
|
||||||
* Fetches user details by user ID.
|
* 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.
|
* @return array|null User details or null if not found.
|
||||||
*/
|
*/
|
||||||
public function getUserDetails($userId) {
|
public function getUserDetails($user_id) {
|
||||||
$sql = 'SELECT
|
$sql = 'SELECT
|
||||||
um.*,
|
um.*,
|
||||||
u.username
|
u.username
|
||||||
FROM
|
FROM
|
||||||
user_meta um
|
users_meta um
|
||||||
LEFT JOIN user u
|
LEFT JOIN users u
|
||||||
ON um.user_id = u.id
|
ON um.user_id = u.id
|
||||||
WHERE
|
WHERE
|
||||||
u.id = :user_id';
|
u.id = :user_id';
|
||||||
|
|
||||||
$query = $this->db->prepare($sql);
|
$query = $this->db->prepare($sql);
|
||||||
$query->execute([
|
$query->execute([
|
||||||
':user_id' => $userId,
|
':user_id' => $user_id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $query->fetchAll(PDO::FETCH_ASSOC);
|
return $query->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
@ -150,19 +208,19 @@ class User {
|
||||||
/**
|
/**
|
||||||
* Grants a user a specific right.
|
* Grants a user a specific right.
|
||||||
*
|
*
|
||||||
* @param int $userId The user ID.
|
* @param int $user_id The user ID.
|
||||||
* @param int $right_id The right ID to grant.
|
* @param int $right_id The right ID to grant.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function addUserRight($userId, $right_id) {
|
public function addUserRight($user_id, $right_id) {
|
||||||
$sql = 'INSERT INTO user_right
|
$sql = 'INSERT INTO users_rights
|
||||||
(user_id, right_id)
|
(user_id, right_id)
|
||||||
VALUES
|
VALUES
|
||||||
(:user_id, :right_id)';
|
(:user_id, :right_id)';
|
||||||
$query = $this->db->prepare($sql);
|
$query = $this->db->prepare($sql);
|
||||||
$query->execute([
|
$query->execute([
|
||||||
':user_id' => $userId,
|
':user_id' => $user_id,
|
||||||
':right_id' => $right_id,
|
':right_id' => $right_id,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -171,20 +229,20 @@ class User {
|
||||||
/**
|
/**
|
||||||
* Revokes a specific right from a user.
|
* Revokes a specific right from a user.
|
||||||
*
|
*
|
||||||
* @param int $userId The user ID.
|
* @param int $user_id The user ID.
|
||||||
* @param int $right_id The right ID to revoke.
|
* @param int $right_id The right ID to revoke.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function removeUserRight($userId, $right_id) {
|
public function removeUserRight($user_id, $right_id) {
|
||||||
$sql = 'DELETE FROM user_right
|
$sql = 'DELETE FROM users_rights
|
||||||
WHERE
|
WHERE
|
||||||
user_id = :user_id
|
user_id = :user_id
|
||||||
AND
|
AND
|
||||||
right_id = :right_id';
|
right_id = :right_id';
|
||||||
$query = $this->db->prepare($sql);
|
$query = $this->db->prepare($sql);
|
||||||
$query->execute([
|
$query->execute([
|
||||||
':user_id' => $userId,
|
':user_id' => $user_id,
|
||||||
':right_id' => $right_id,
|
':right_id' => $right_id,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -199,7 +257,7 @@ class User {
|
||||||
$sql = 'SELECT
|
$sql = 'SELECT
|
||||||
id AS right_id,
|
id AS right_id,
|
||||||
name AS right_name
|
name AS right_name
|
||||||
FROM `right`
|
FROM rights
|
||||||
ORDER BY id ASC';
|
ORDER BY id ASC';
|
||||||
$query = $this->db->prepare($sql);
|
$query = $this->db->prepare($sql);
|
||||||
$query->execute();
|
$query->execute();
|
||||||
|
@ -212,27 +270,27 @@ class User {
|
||||||
/**
|
/**
|
||||||
* Retrieves the rights assigned to a specific 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.
|
* @return array List of user rights.
|
||||||
*/
|
*/
|
||||||
public function getUserRights($userId) {
|
public function getUserRights($user_id) {
|
||||||
$sql = 'SELECT
|
$sql = 'SELECT
|
||||||
u.id AS user_id,
|
u.id AS user_id,
|
||||||
r.id AS right_id,
|
r.id AS right_id,
|
||||||
r.name AS right_name
|
r.name AS right_name
|
||||||
FROM
|
FROM
|
||||||
`user` u
|
users u
|
||||||
LEFT JOIN `user_right` ur
|
LEFT JOIN users_rights ur
|
||||||
ON u.id = ur.user_id
|
ON u.id = ur.user_id
|
||||||
LEFT JOIN `right` r
|
LEFT JOIN rights r
|
||||||
ON ur.right_id = r.id
|
ON ur.right_id = r.id
|
||||||
WHERE
|
WHERE
|
||||||
u.id = :user_id';
|
u.id = :user_id';
|
||||||
|
|
||||||
$query = $this->db->prepare($sql);
|
$query = $this->db->prepare($sql);
|
||||||
$query->execute([
|
$query->execute([
|
||||||
':user_id' => $userId,
|
':user_id' => $user_id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$result = $query->fetchAll(PDO::FETCH_ASSOC);
|
$result = $query->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
@ -241,7 +299,7 @@ class User {
|
||||||
$specialEntries = [];
|
$specialEntries = [];
|
||||||
|
|
||||||
// user 1 is always superuser
|
// user 1 is always superuser
|
||||||
if ($userId == 1) {
|
if ($user_id == 1) {
|
||||||
$specialEntries = [
|
$specialEntries = [
|
||||||
[
|
[
|
||||||
'user_id' => 1,
|
'user_id' => 1,
|
||||||
|
@ -251,7 +309,7 @@ class User {
|
||||||
];
|
];
|
||||||
|
|
||||||
// user 2 is always demo
|
// user 2 is always demo
|
||||||
} elseif ($userId == 2) {
|
} elseif ($user_id == 2) {
|
||||||
$specialEntries = [
|
$specialEntries = [
|
||||||
[
|
[
|
||||||
'user_id' => 2,
|
'user_id' => 2,
|
||||||
|
@ -275,17 +333,17 @@ class User {
|
||||||
/**
|
/**
|
||||||
* Check if the user has a specific right.
|
* Check if the user has a specific right.
|
||||||
*
|
*
|
||||||
* @param int $userId The user ID.
|
* @param int $user_id The user ID.
|
||||||
* @param string $right_name The human-readable name of the user right.
|
* @param string $right_name The human-readable name of the user right.
|
||||||
*
|
*
|
||||||
* @return bool True if the user has the right, false otherwise.
|
* @return bool True if the user has the right, false otherwise.
|
||||||
*/
|
*/
|
||||||
function hasRight($userId, $right_name) {
|
function hasRight($user_id, $right_name) {
|
||||||
$userRights = $this->getUserRights($userId);
|
$userRights = $this->getUserRights($user_id);
|
||||||
$userHasRight = false;
|
$userHasRight = false;
|
||||||
|
|
||||||
// superuser always has all the rights
|
// superuser always has all the rights
|
||||||
if ($userId === 1) {
|
if ($user_id === 1) {
|
||||||
$userHasRight = true;
|
$userHasRight = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -304,8 +362,8 @@ class User {
|
||||||
/**
|
/**
|
||||||
* Updates a user's metadata in the database.
|
* Updates a user's metadata in the database.
|
||||||
*
|
*
|
||||||
* @param int $userId The ID of the user to update.
|
* @param int $user_id The ID of the user to update.
|
||||||
* @param array $updatedUser An associative array containing updated user data:
|
* @param array $updatedUser An associative array containing updated user data:
|
||||||
* - 'name' (string): The updated name of the user.
|
* - 'name' (string): The updated name of the user.
|
||||||
* - 'email' (string): The updated email of the user.
|
* - 'email' (string): The updated email of the user.
|
||||||
* - 'timezone' (string): The updated timezone of the user.
|
* - 'timezone' (string): The updated timezone of the user.
|
||||||
|
@ -313,9 +371,9 @@ class User {
|
||||||
*
|
*
|
||||||
* @return bool|string Returns true if the update is successful, or an error message if an exception occurs.
|
* @return bool|string Returns true if the update is successful, or an error message if an exception occurs.
|
||||||
*/
|
*/
|
||||||
public function editUser($userId, $updatedUser) {
|
public function editUser($user_id, $updatedUser) {
|
||||||
try {
|
try {
|
||||||
$sql = 'UPDATE user_meta SET
|
$sql = 'UPDATE users_meta SET
|
||||||
name = :name,
|
name = :name,
|
||||||
email = :email,
|
email = :email,
|
||||||
timezone = :timezone,
|
timezone = :timezone,
|
||||||
|
@ -323,7 +381,7 @@ class User {
|
||||||
WHERE user_id = :user_id';
|
WHERE user_id = :user_id';
|
||||||
$query = $this->db->prepare($sql);
|
$query = $this->db->prepare($sql);
|
||||||
$query->execute([
|
$query->execute([
|
||||||
':user_id' => $userId,
|
':user_id' => $user_id,
|
||||||
':name' => $updatedUser['name'],
|
':name' => $updatedUser['name'],
|
||||||
':email' => $updatedUser['email'],
|
':email' => $updatedUser['email'],
|
||||||
':timezone' => $updatedUser['timezone'],
|
':timezone' => $updatedUser['timezone'],
|
||||||
|
@ -342,20 +400,20 @@ class User {
|
||||||
/**
|
/**
|
||||||
* Removes a user's avatar from the database and deletes the associated file.
|
* Removes a user's avatar from the database and deletes the associated file.
|
||||||
*
|
*
|
||||||
* @param int $userId The ID of the user whose avatar is being removed.
|
* @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.
|
* @param string $old_avatar Optional. The file path of the current avatar to delete. Default is an empty string.
|
||||||
*
|
*
|
||||||
* @return bool|string Returns true if the avatar is successfully removed, or an error message if an exception occurs.
|
* @return bool|string Returns true if the avatar is successfully removed, or an error message if an exception occurs.
|
||||||
*/
|
*/
|
||||||
public function removeAvatar($userId, $old_avatar = '') {
|
public function removeAvatar($user_id, $old_avatar = '') {
|
||||||
try {
|
try {
|
||||||
// remove from database
|
// remove from database
|
||||||
$sql = 'UPDATE user_meta SET
|
$sql = 'UPDATE users_meta SET
|
||||||
avatar = NULL
|
avatar = NULL
|
||||||
WHERE user_id = :user_id';
|
WHERE user_id = :user_id';
|
||||||
$query = $this->db->prepare($sql);
|
$query = $this->db->prepare($sql);
|
||||||
$query->execute([
|
$query->execute([
|
||||||
':user_id' => $userId,
|
':user_id' => $user_id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// delete the old avatar file
|
// delete the old avatar file
|
||||||
|
@ -375,14 +433,14 @@ class User {
|
||||||
/**
|
/**
|
||||||
* Updates a user's avatar by uploading a new file and saving its path in the database.
|
* Updates a user's avatar by uploading a new file and saving its path in the database.
|
||||||
*
|
*
|
||||||
* @param int $userId The ID of the user whose avatar is being updated.
|
* @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.
|
* @param array $avatar_file The uploaded avatar file from the $_FILES array.
|
||||||
* Should include 'tmp_name', 'name', 'error', etc.
|
* Should include 'tmp_name', 'name', 'error', etc.
|
||||||
* @param string $avatars_path The directory path where avatar files should be saved.
|
* @param string $avatars_path The directory path where avatar files should be saved.
|
||||||
*
|
*
|
||||||
* @return bool|string Returns true if the avatar is successfully updated, or an error message if an exception occurs.
|
* @return bool|string Returns true if the avatar is successfully updated, or an error message if an exception occurs.
|
||||||
*/
|
*/
|
||||||
public function changeAvatar($userId, $avatar_file, $avatars_path) {
|
public function changeAvatar($user_id, $avatar_file, $avatars_path) {
|
||||||
try {
|
try {
|
||||||
// check if the file was uploaded
|
// check if the file was uploaded
|
||||||
if (isset($avatar_file) && $avatar_file['error'] === UPLOAD_ERR_OK) {
|
if (isset($avatar_file) && $avatar_file['error'] === UPLOAD_ERR_OK) {
|
||||||
|
@ -399,13 +457,13 @@ class User {
|
||||||
if (move_uploaded_file($fileTmpPath, $dest_path)) {
|
if (move_uploaded_file($fileTmpPath, $dest_path)) {
|
||||||
try {
|
try {
|
||||||
// update user's avatar path in DB
|
// update user's avatar path in DB
|
||||||
$sql = 'UPDATE user_meta SET
|
$sql = 'UPDATE users_meta SET
|
||||||
avatar = :avatar
|
avatar = :avatar
|
||||||
WHERE user_id = :user_id';
|
WHERE user_id = :user_id';
|
||||||
$query = $this->db->prepare($sql);
|
$query = $this->db->prepare($sql);
|
||||||
$query->execute([
|
$query->execute([
|
||||||
':avatar' => $newFileName,
|
':avatar' => $newFileName,
|
||||||
':user_id' => $userId
|
':user_id' => $user_id
|
||||||
]);
|
]);
|
||||||
// all went OK
|
// all went OK
|
||||||
$_SESSION['notice'] .= 'Avatar updated successfully. ';
|
$_SESSION['notice'] .= 'Avatar updated successfully. ';
|
||||||
|
@ -435,7 +493,7 @@ class User {
|
||||||
*/
|
*/
|
||||||
public function getUsers() {
|
public function getUsers() {
|
||||||
$sql = "SELECT id, username
|
$sql = "SELECT id, username
|
||||||
FROM `user`
|
FROM users
|
||||||
ORDER BY username ASC";
|
ORDER BY username ASC";
|
||||||
|
|
||||||
$stmt = $this->db->prepare($sql);
|
$stmt = $this->db->prepare($sql);
|
||||||
|
@ -447,9 +505,9 @@ class User {
|
||||||
/**
|
/**
|
||||||
* Enable two-factor authentication for a user
|
* Enable two-factor authentication for a user
|
||||||
*
|
*
|
||||||
* @param int $userId User ID
|
* @param int $userId User ID
|
||||||
* @param string $secret Secret key to use
|
* @param string $secret Secret key to use
|
||||||
* @param string $code Verification code to validate
|
* @param string $code Verification code to validate
|
||||||
* @return bool True if enabled successfully
|
* @return bool True if enabled successfully
|
||||||
*/
|
*/
|
||||||
public function enableTwoFactor($userId, $secret = null, $code = null) {
|
public function enableTwoFactor($userId, $secret = null, $code = null) {
|
||||||
|
@ -469,8 +527,8 @@ class User {
|
||||||
/**
|
/**
|
||||||
* Verify a two-factor authentication code
|
* Verify a two-factor authentication code
|
||||||
*
|
*
|
||||||
* @param int $userId User ID
|
* @param int $userId User ID
|
||||||
* @param string $code The verification code
|
* @param string $code The verification code
|
||||||
* @return bool True if verified
|
* @return bool True if verified
|
||||||
*/
|
*/
|
||||||
public function verifyTwoFactor($userId, $code) {
|
public function verifyTwoFactor($userId, $code) {
|
||||||
|
@ -490,15 +548,15 @@ class User {
|
||||||
/**
|
/**
|
||||||
* Change a user's password
|
* Change a user's password
|
||||||
*
|
*
|
||||||
* @param int $userId User ID
|
* @param int $userId User ID
|
||||||
* @param string $currentPassword Current password for verification
|
* @param string $currentPassword Current password for verification
|
||||||
* @param string $newPassword New password to set
|
* @param string $newPassword New password to set
|
||||||
* @return bool True if password was changed successfully
|
* @return bool True if password was changed successfully
|
||||||
*/
|
*/
|
||||||
public function changePassword($userId, $currentPassword, $newPassword) {
|
public function changePassword($userId, $currentPassword, $newPassword) {
|
||||||
try {
|
try {
|
||||||
// First verify the current password
|
// First verify the current password
|
||||||
$sql = "SELECT password FROM user WHERE id = :user_id";
|
$sql = "SELECT password FROM users WHERE id = :user_id";
|
||||||
$query = $this->db->prepare($sql);
|
$query = $this->db->prepare($sql);
|
||||||
$query->execute([':user_id' => $userId]);
|
$query->execute([':user_id' => $userId]);
|
||||||
$user = $query->fetch(PDO::FETCH_ASSOC);
|
$user = $query->fetch(PDO::FETCH_ASSOC);
|
||||||
|
@ -511,7 +569,7 @@ class User {
|
||||||
$hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT);
|
$hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT);
|
||||||
|
|
||||||
// Update the password
|
// Update the password
|
||||||
$sql = "UPDATE user SET password = :password WHERE id = :user_id";
|
$sql = "UPDATE users SET password = :password WHERE id = :user_id";
|
||||||
$query = $this->db->prepare($sql);
|
$query = $this->db->prepare($sql);
|
||||||
return $query->execute([
|
return $query->execute([
|
||||||
':password' => $hashedPassword,
|
':password' => $hashedPassword,
|
||||||
|
|
|
@ -10,17 +10,8 @@ return [
|
||||||
'domain' => 'localhost',
|
'domain' => 'localhost',
|
||||||
// subfolder for the web app, if any
|
// subfolder for the web app, if any
|
||||||
'folder' => '/jilo-web/',
|
'folder' => '/jilo-web/',
|
||||||
// site name used in emails and in the interface
|
// site name used in emails and in the inteerface
|
||||||
'site_name' => 'Jilo Web',
|
'site_name' => 'Jilo Web',
|
||||||
// session configuration
|
|
||||||
'session' => [
|
|
||||||
// session name, if empty a random one will be generated
|
|
||||||
'name' => 'jilo',
|
|
||||||
// 2 hours (7200) default, when "remember me" is not checked
|
|
||||||
'lifetime' => 7200,
|
|
||||||
// 30 days (2592000) default, when "remember me" is checked
|
|
||||||
'remember_me_lifetime' => 2592000,
|
|
||||||
],
|
|
||||||
// set to false to disable new registrations
|
// set to false to disable new registrations
|
||||||
'registration_enabled' => true,
|
'registration_enabled' => true,
|
||||||
// will be displayed on login screen
|
// will be displayed on login screen
|
||||||
|
@ -31,20 +22,12 @@ return [
|
||||||
//*******************************************
|
//*******************************************
|
||||||
|
|
||||||
// database
|
// database
|
||||||
'db_type' => 'mariadb',
|
'db' => [
|
||||||
|
// DB type for the web app, currently only "sqlite" is used
|
||||||
'sqlite' => [
|
'db_type' => 'sqlite',
|
||||||
|
// default is ../app/jilo-web.db
|
||||||
'sqlite_file' => '../app/jilo-web.db',
|
'sqlite_file' => '../app/jilo-web.db',
|
||||||
],
|
],
|
||||||
|
|
||||||
'sql' => [
|
|
||||||
'sql_host' => 'localhost',
|
|
||||||
'sql_port' => '3306',
|
|
||||||
'sql_database' => 'jilo',
|
|
||||||
'sql_username' => 'jilouser',
|
|
||||||
'sql_password' => 'jilopass',
|
|
||||||
],
|
|
||||||
|
|
||||||
// avatars path
|
// avatars path
|
||||||
'avatars_path' => 'uploads/avatars/',
|
'avatars_path' => 'uploads/avatars/',
|
||||||
// default avatar
|
// default avatar
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
return [
|
|
||||||
// Active theme (can be overridden by user preference)
|
|
||||||
'active_theme' => 'default',
|
|
||||||
|
|
||||||
// Available themes with their display names
|
|
||||||
'available_themes' => [
|
|
||||||
'default' => 'Default built-in theme',
|
|
||||||
'modern' => 'Modern theme',
|
|
||||||
'retro' => 'Alternative retro theme'
|
|
||||||
],
|
|
||||||
|
|
||||||
// Path configurations
|
|
||||||
'paths' => [
|
|
||||||
// Base directory for all external themes
|
|
||||||
'themes' => __DIR__ . '/../../themes',
|
|
||||||
|
|
||||||
// Default templates location (built-in fallback)
|
|
||||||
'templates' => __DIR__ . '/../templates',
|
|
||||||
|
|
||||||
// Public assets directory (built-in fallback)
|
|
||||||
'public' => __DIR__ . '/../../public_html'
|
|
||||||
],
|
|
||||||
|
|
||||||
// Theme configuration defaults
|
|
||||||
'default_config' => [
|
|
||||||
'name' => 'Unnamed Theme',
|
|
||||||
'description' => 'A Jilo Web theme',
|
|
||||||
'version' => '1.0.0',
|
|
||||||
'author' => 'Lindeas Inc.',
|
|
||||||
'screenshot' => 'screenshot.png',
|
|
||||||
'options' => []
|
|
||||||
]
|
|
||||||
];
|
|
|
@ -1,41 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Core;
|
|
||||||
|
|
||||||
class ConfigLoader
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var string|null
|
|
||||||
*/
|
|
||||||
private static $configPath = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load configuration array from a set of possible file locations.
|
|
||||||
*
|
|
||||||
* @param string[] $locations
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function loadConfig(array $locations): array
|
|
||||||
{
|
|
||||||
$configFile = null;
|
|
||||||
foreach ($locations as $location) {
|
|
||||||
if (file_exists($location)) {
|
|
||||||
$configFile = $location;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!$configFile) {
|
|
||||||
die('Config file not found');
|
|
||||||
}
|
|
||||||
self::$configPath = $configFile;
|
|
||||||
return require $configFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string|null
|
|
||||||
*/
|
|
||||||
public static function getConfigPath(): ?string
|
|
||||||
{
|
|
||||||
return self::$configPath;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Core;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
use Feedback;
|
|
||||||
|
|
||||||
class DatabaseConnector
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Connect to the database using given configuration and handle errors.
|
|
||||||
*
|
|
||||||
* @param array $config
|
|
||||||
* @return mixed Database connection
|
|
||||||
*/
|
|
||||||
public static function connect(array $config)
|
|
||||||
{
|
|
||||||
// Load DB classes
|
|
||||||
require_once __DIR__ . '/../classes/database.php';
|
|
||||||
require_once __DIR__ . '/../includes/database.php';
|
|
||||||
|
|
||||||
try {
|
|
||||||
$db = connectDB($config);
|
|
||||||
if (!$db) {
|
|
||||||
throw new Exception('Could not connect to database');
|
|
||||||
}
|
|
||||||
return $db;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
// Show error and exit
|
|
||||||
Feedback::flash('ERROR', 'DEFAULT', getError('Error connecting to the database.', $e->getMessage()));
|
|
||||||
include __DIR__ . '/../templates/page-header.php';
|
|
||||||
include __DIR__ . '/../helpers/feedback.php';
|
|
||||||
include __DIR__ . '/../templates/page-footer.php';
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Core;
|
|
||||||
|
|
||||||
class HookDispatcher
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Stores all registered hooks and their callbacks.
|
|
||||||
* @var array<string, array<callable>>
|
|
||||||
*/
|
|
||||||
private static array $hooks = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a callback for a given hook.
|
|
||||||
*/
|
|
||||||
public static function register(string $hook, callable $callback): void
|
|
||||||
{
|
|
||||||
if (!isset(self::$hooks[$hook])) {
|
|
||||||
self::$hooks[$hook] = [];
|
|
||||||
}
|
|
||||||
self::$hooks[$hook][] = $callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatch all callbacks for the specified hook.
|
|
||||||
*/
|
|
||||||
public static function dispatch(string $hook, array $context = []): void
|
|
||||||
{
|
|
||||||
if (!empty(self::$hooks[$hook])) {
|
|
||||||
foreach (self::$hooks[$hook] as $callback) {
|
|
||||||
call_user_func($callback, $context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply filters for a hook key, passing a value through all callbacks.
|
|
||||||
* Each callback should accept the value and return a modified value.
|
|
||||||
*
|
|
||||||
* @param string $hook
|
|
||||||
* @param mixed $value
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public static function applyFilters(string $hook, $value)
|
|
||||||
{
|
|
||||||
if (!empty(self::$hooks[$hook])) {
|
|
||||||
foreach (self::$hooks[$hook] as $callback) {
|
|
||||||
$value = call_user_func($callback, $value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Core;
|
|
||||||
|
|
||||||
class MiddlewarePipeline {
|
|
||||||
/** @var callable[] */
|
|
||||||
private $middlewares = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a middleware to the pipeline.
|
|
||||||
* @param callable $middleware Should return false to halt execution.
|
|
||||||
*/
|
|
||||||
public function add(callable $middleware): void {
|
|
||||||
$this->middlewares[] = $middleware;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute all middlewares in sequence.
|
|
||||||
* @return bool False if any middleware returns false, true otherwise.
|
|
||||||
*/
|
|
||||||
public function run(): bool {
|
|
||||||
foreach ($this->middlewares as $middleware) {
|
|
||||||
$result = call_user_func($middleware);
|
|
||||||
if ($result === false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Core;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* NullLogger is a fallback for disabling logging when there is no logging plugin enabled.
|
|
||||||
*/
|
|
||||||
class NullLogger
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* PSR-3 compatible log stub.
|
|
||||||
* @param string $level
|
|
||||||
* @param string $message
|
|
||||||
* @param array $context
|
|
||||||
*/
|
|
||||||
public function log(string $level, string $message, array $context = []): void {}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Core;
|
|
||||||
|
|
||||||
class PluginManager
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Loads all enabled plugins from the given directory.
|
|
||||||
*
|
|
||||||
* @param string $pluginsDir
|
|
||||||
* @return array<string, array{path: string, meta: array}>
|
|
||||||
*/
|
|
||||||
public static function load(string $pluginsDir): array
|
|
||||||
{
|
|
||||||
$enabled = [];
|
|
||||||
foreach (glob($pluginsDir . '*', GLOB_ONLYDIR) as $pluginPath) {
|
|
||||||
$manifest = $pluginPath . '/plugin.json';
|
|
||||||
if (!file_exists($manifest)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$meta = json_decode(file_get_contents($manifest), true);
|
|
||||||
if (empty($meta['enabled'])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$name = basename($pluginPath);
|
|
||||||
$enabled[$name] = [
|
|
||||||
'path' => $pluginPath,
|
|
||||||
'meta' => $meta,
|
|
||||||
];
|
|
||||||
$bootstrap = $pluginPath . '/bootstrap.php';
|
|
||||||
if (file_exists($bootstrap)) {
|
|
||||||
include_once $bootstrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $enabled;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Core;
|
|
||||||
|
|
||||||
use Session;
|
|
||||||
use Feedback;
|
|
||||||
|
|
||||||
class Router {
|
|
||||||
/**
|
|
||||||
* Check session validity and handle redirection for protected pages.
|
|
||||||
* Returns current username if session is valid, null otherwise.
|
|
||||||
*/
|
|
||||||
public static function checkAuth(array $config, string $app_root, array $public_pages, string $page): ?string {
|
|
||||||
// Always allow login page to be accessed
|
|
||||||
if ($page === 'login') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this is a public page
|
|
||||||
$isPublicPage = in_array($page, $public_pages, true);
|
|
||||||
|
|
||||||
// For public pages, don't validate session
|
|
||||||
if ($isPublicPage) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For protected pages, check if we have a valid session
|
|
||||||
$validSession = Session::isValidSession(true);
|
|
||||||
|
|
||||||
// If session is valid, return the username
|
|
||||||
if ($validSession) {
|
|
||||||
return Session::getUsername();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we get here, we need to redirect to login
|
|
||||||
// Only show timeout message if we had an active session before
|
|
||||||
if (isset($_SESSION['LAST_ACTIVITY']) && !isset($_SESSION['session_timeout_shown'])) {
|
|
||||||
Feedback::flash('LOGIN', 'SESSION_TIMEOUT');
|
|
||||||
$_SESSION['session_timeout_shown'] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preserve flash messages
|
|
||||||
$flash_messages = $_SESSION['flash_messages'] ?? [];
|
|
||||||
Session::cleanup($config);
|
|
||||||
$_SESSION['flash_messages'] = $flash_messages;
|
|
||||||
|
|
||||||
// Build login URL with redirect if appropriate
|
|
||||||
$loginUrl = $app_root . '?page=login';
|
|
||||||
$trimmed = trim($page, '/?');
|
|
||||||
if (!empty($trimmed) && !in_array($trimmed, INVALID_REDIRECT_PAGES, true)) {
|
|
||||||
$loginUrl .= '&redirect=' . urlencode($_SERVER['REQUEST_URI']);
|
|
||||||
}
|
|
||||||
|
|
||||||
header('Location: ' . $loginUrl);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the user's IP address.
|
|
||||||
* Uses global $user_IP set by Logger plugin if available, else falls back to server variables.
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
function getUserIP() {
|
|
||||||
global $user_IP;
|
|
||||||
if (!empty($user_IP)) {
|
|
||||||
return $user_IP;
|
|
||||||
}
|
|
||||||
// Fallback to HTTP headers or REMOTE_ADDR
|
|
||||||
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
|
||||||
return $_SERVER['HTTP_CLIENT_IP'];
|
|
||||||
}
|
|
||||||
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
|
||||||
// May contain multiple IPs
|
|
||||||
$parts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
|
|
||||||
return trim($parts[0]);
|
|
||||||
}
|
|
||||||
return $_SERVER['REMOTE_ADDR'] ?? '';
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a logger instance: plugin Log if available, otherwise NullLogger.
|
|
||||||
*
|
|
||||||
* @param mixed $database Database or DatabaseConnector instance.
|
|
||||||
* @return mixed Logger instance with PSR-3 log() compatible method.
|
|
||||||
*/
|
|
||||||
function getLoggerInstance($database) {
|
|
||||||
if (class_exists('Log')) {
|
|
||||||
return new Log($database);
|
|
||||||
}
|
|
||||||
require_once __DIR__ . '/../core/NullLogger.php';
|
|
||||||
return new \App\Core\NullLogger();
|
|
||||||
}
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
function getUserIP() {
|
||||||
|
// get directly the user IP
|
||||||
|
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
||||||
|
$ip = $_SERVER['HTTP_CLIENT_IP'];
|
||||||
|
// if user is behind some proxy
|
||||||
|
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||||
|
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
|
||||||
|
} else {
|
||||||
|
$ip = $_SERVER['REMOTE_ADDR'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// get only the first IP if there are more
|
||||||
|
if (strpos($ip, ',') !== false) {
|
||||||
|
$ip = explode(',', $ip)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim($ip);
|
||||||
|
}
|
|
@ -1,109 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Theme Asset handler
|
|
||||||
*
|
|
||||||
* Serves theme assets (images, CSS, JS, etc.) securely by checking if the requested
|
|
||||||
* theme and asset path are valid and accessible.
|
|
||||||
*
|
|
||||||
* This is a standalone handler that doesn't require the full application initialization.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Set error reporting
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
ini_set('display_errors', '1');
|
|
||||||
|
|
||||||
// Define base path if not defined
|
|
||||||
if (!defined('APP_ROOT')) {
|
|
||||||
define('APP_ROOT', dirname(__DIR__));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Basic security checks
|
|
||||||
if (!isset($_GET['theme']) || !preg_match('/^[a-zA-Z0-9_-]+$/', $_GET['theme'])) {
|
|
||||||
http_response_code(400);
|
|
||||||
exit('Invalid theme specified');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isset($_GET['path']) || empty($_GET['path'])) {
|
|
||||||
http_response_code(400);
|
|
||||||
exit('No asset path specified');
|
|
||||||
}
|
|
||||||
|
|
||||||
$themeId = $_GET['theme'];
|
|
||||||
$assetPath = $_GET['path'];
|
|
||||||
|
|
||||||
// Validate asset path (only alphanumeric, hyphen, underscore, dot, and forward slash)
|
|
||||||
if (!preg_match('/^[a-zA-Z0-9_\-\.\/]+$/', $assetPath)) {
|
|
||||||
http_response_code(400);
|
|
||||||
exit('Invalid asset path');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent directory traversal
|
|
||||||
if (strpos($assetPath, '..') !== false) {
|
|
||||||
http_response_code(400);
|
|
||||||
exit('Invalid asset path');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build full path to the asset
|
|
||||||
$themesDir = dirname(dirname(__DIR__)) . '/themes';
|
|
||||||
$fullPath = realpath("$themesDir/$themeId/$assetPath");
|
|
||||||
|
|
||||||
// Additional security check to ensure the path is within the themes directory
|
|
||||||
if ($fullPath === false) {
|
|
||||||
http_response_code(404);
|
|
||||||
header('Content-Type: text/plain');
|
|
||||||
error_log("Asset not found: $themesDir/$themeId/$assetPath");
|
|
||||||
exit("Asset not found: $themesDir/$themeId/$assetPath");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (strpos($fullPath, realpath($themesDir)) !== 0) {
|
|
||||||
http_response_code(400);
|
|
||||||
header('Content-Type: text/plain');
|
|
||||||
error_log("Security violation: Attempted to access path outside themes directory: $fullPath");
|
|
||||||
exit('Invalid asset path');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the file exists and is readable
|
|
||||||
if (!file_exists($fullPath) || !is_readable($fullPath)) {
|
|
||||||
http_response_code(404);
|
|
||||||
header('Content-Type: text/plain');
|
|
||||||
error_log("File not found or not readable: $fullPath");
|
|
||||||
exit("File not found or not readable: " . basename($fullPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear any previous output
|
|
||||||
if (ob_get_level()) {
|
|
||||||
ob_clean();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine content type based on file extension
|
|
||||||
$extension = strtolower(pathinfo($assetPath, PATHINFO_EXTENSION));
|
|
||||||
$contentTypes = [
|
|
||||||
'css' => 'text/css',
|
|
||||||
'js' => 'application/javascript',
|
|
||||||
'json' => 'application/json',
|
|
||||||
'png' => 'image/png',
|
|
||||||
'jpg' => 'image/jpeg',
|
|
||||||
'jpeg' => 'image/jpeg',
|
|
||||||
'gif' => 'image/gif',
|
|
||||||
'svg' => 'image/svg+xml',
|
|
||||||
'webp' => 'image/webp',
|
|
||||||
'woff' => 'font/woff',
|
|
||||||
'woff2' => 'font/woff2',
|
|
||||||
'ttf' => 'font/ttf',
|
|
||||||
'eot' => 'application/vnd.ms-fontobject',
|
|
||||||
];
|
|
||||||
|
|
||||||
$contentType = $contentTypes[$extension] ?? 'application/octet-stream';
|
|
||||||
|
|
||||||
// Set proper headers
|
|
||||||
header('Content-Type: ' . $contentType);
|
|
||||||
header('Content-Length: ' . filesize($fullPath));
|
|
||||||
|
|
||||||
// Cache for 24 hours (86400 seconds)
|
|
||||||
$expires = 86400;
|
|
||||||
header('Cache-Control: public, max-age=' . $expires);
|
|
||||||
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT');
|
|
||||||
header('Pragma: cache');
|
|
||||||
|
|
||||||
// Output the file
|
|
||||||
readfile($fullPath);
|
|
|
@ -1,307 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Theme Helper
|
|
||||||
*
|
|
||||||
* Handles theme management and template/asset loading for the application.
|
|
||||||
* Supports multiple themes with fallback to default theme when needed.
|
|
||||||
* The default theme uses app/templates and public_html/static as fallbacks/
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace App\Helpers;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
|
|
||||||
// Include Session class
|
|
||||||
require_once __DIR__ . '/../classes/session.php';
|
|
||||||
use Session;
|
|
||||||
|
|
||||||
class Theme
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var array Theme configuration
|
|
||||||
*/
|
|
||||||
private static $config;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the theme configuration
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function getConfig()
|
|
||||||
{
|
|
||||||
// Always reload the config to get the latest changes
|
|
||||||
self::$config = require __DIR__ . '/../config/theme.php';
|
|
||||||
return self::$config;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string Current theme name
|
|
||||||
*/
|
|
||||||
private static $currentTheme;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the theme system
|
|
||||||
*/
|
|
||||||
public static function init()
|
|
||||||
{
|
|
||||||
// Only load config if not already loaded
|
|
||||||
if (self::$config === null) {
|
|
||||||
self::$config = require __DIR__ . '/../config/theme.php';
|
|
||||||
}
|
|
||||||
self::$currentTheme = self::getCurrentThemeName();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current theme name
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public static function getCurrentThemeName()
|
|
||||||
{
|
|
||||||
// Ensure session is started
|
|
||||||
if (session_status() === PHP_SESSION_NONE) {
|
|
||||||
Session::startSession();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if already determined
|
|
||||||
if (self::$currentTheme !== null) {
|
|
||||||
return self::$currentTheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to get from session first
|
|
||||||
$sessionTheme = isset($_SESSION['theme']) ? $_SESSION['theme'] : null;
|
|
||||||
if ($sessionTheme && isset(self::$config['available_themes'][$sessionTheme])) {
|
|
||||||
self::$currentTheme = $sessionTheme;
|
|
||||||
} else {
|
|
||||||
// Fall back to default theme
|
|
||||||
self::$currentTheme = self::$config['active_theme'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return self::$currentTheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the URL for a theme asset
|
|
||||||
*
|
|
||||||
* @param string $themeId Theme ID
|
|
||||||
* @param string $assetPath Path to the asset relative to theme directory (e.g., 'css/style.css')
|
|
||||||
* @return string|null URL to the asset or null if not found
|
|
||||||
*/
|
|
||||||
public static function getAssetUrl($themeId, $assetPath = '')
|
|
||||||
{
|
|
||||||
// Clean and validate the asset path
|
|
||||||
$assetPath = ltrim($assetPath, '/');
|
|
||||||
if (empty($assetPath)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only allow alphanumeric, hyphen, underscore, dot, and forward slash
|
|
||||||
if (!preg_match('/^[a-zA-Z0-9_\-\.\/]+$/', $assetPath)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent directory traversal
|
|
||||||
if (strpos($assetPath, '..') !== false) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$fullPath = __DIR__ . "/../../themes/$themeId/$assetPath";
|
|
||||||
if (!file_exists($fullPath) || !is_readable($fullPath)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate URL that goes through index.php
|
|
||||||
global $app_root;
|
|
||||||
// Remove any trailing slash from app_root to avoid double slashes
|
|
||||||
$baseUrl = rtrim($app_root, '/');
|
|
||||||
return "$baseUrl/?page=theme-asset&theme=" . urlencode($themeId) . "&path=" . urlencode($assetPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the current theme for the session
|
|
||||||
*
|
|
||||||
* @param string $themeName
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public static function setCurrentTheme(string $themeName): bool
|
|
||||||
{
|
|
||||||
if (!self::themeExists($themeName)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update session
|
|
||||||
if (Session::isValidSession()) {
|
|
||||||
$_SESSION['theme'] = $themeName;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear the current theme cache
|
|
||||||
self::$currentTheme = null;
|
|
||||||
|
|
||||||
// Update config file
|
|
||||||
$configFile = __DIR__ . '/../config/theme.php';
|
|
||||||
if (file_exists($configFile) && is_writable($configFile)) {
|
|
||||||
$config = file_get_contents($configFile);
|
|
||||||
// Update the active_theme in the config
|
|
||||||
$newConfig = preg_replace(
|
|
||||||
"/'active_theme'\s*=>\s*'[^']*'/",
|
|
||||||
"'active_theme' => '" . addslashes($themeName) . "'",
|
|
||||||
$config
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($newConfig !== $config) {
|
|
||||||
if (file_put_contents($configFile, $newConfig) === false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self::$currentTheme = $themeName;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a theme exists
|
|
||||||
*
|
|
||||||
* @param string $themeName
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public static function themeExists(string $themeName): bool
|
|
||||||
{
|
|
||||||
// Default theme always exists as it uses core templates
|
|
||||||
if ($themeName === 'default') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$themePath = self::getThemePath($themeName);
|
|
||||||
return is_dir($themePath) && file_exists("$themePath/config.php");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the path to a theme
|
|
||||||
*
|
|
||||||
* @param string|null $themeName
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public static function getThemePath(?string $themeName = null): string
|
|
||||||
{
|
|
||||||
$themeName = $themeName ?? self::getCurrentThemeName();
|
|
||||||
$config = self::getConfig();
|
|
||||||
return rtrim($config['paths']['themes'], '/') . "/$themeName";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the URL for a theme asset
|
|
||||||
*
|
|
||||||
* @param string $path
|
|
||||||
* @param bool $includeVersion
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public static function asset($path, $includeVersion = false)
|
|
||||||
{
|
|
||||||
$themeName = self::getCurrentThemeName();
|
|
||||||
$config = self::getConfig();
|
|
||||||
$baseUrl = rtrim($GLOBALS['app_root'] ?? '', '/');
|
|
||||||
|
|
||||||
// For non-default themes, use theme assets
|
|
||||||
if ($themeName !== 'default') {
|
|
||||||
$assetPath = "/themes/{$themeName}/assets/" . ltrim($path, '/');
|
|
||||||
|
|
||||||
// Add version query string for cache busting
|
|
||||||
if ($includeVersion) {
|
|
||||||
$version = self::getThemeVersion($themeName);
|
|
||||||
$assetPath .= (strpos($assetPath, '?') !== false ? '&' : '?') . 'v=' . $version;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For default theme, use public_html directly
|
|
||||||
$assetPath = '/' . ltrim($path, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $baseUrl . $assetPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Include a theme template file
|
|
||||||
*
|
|
||||||
* @param string $template Template name without .php extension
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public static function include($template)
|
|
||||||
{
|
|
||||||
global $config;
|
|
||||||
$config = $config ?? [];
|
|
||||||
|
|
||||||
$themeConfig = self::getConfig();
|
|
||||||
$themeName = self::getCurrentThemeName();
|
|
||||||
|
|
||||||
// We need this here, otherwise because this helper
|
|
||||||
// between index and the views breaks the session vars
|
|
||||||
extract($GLOBALS, EXTR_SKIP | EXTR_REFS);
|
|
||||||
|
|
||||||
// Ensure config is always available in templates
|
|
||||||
$config = array_merge($config, $themeConfig);
|
|
||||||
|
|
||||||
// For non-default themes, look in the theme directory first
|
|
||||||
if ($themeName !== 'default') {
|
|
||||||
$themePath = $config['paths']['themes'] . '/' . $themeName . '/views/' . $template . '.php';
|
|
||||||
if (file_exists($themePath)) {
|
|
||||||
include $themePath;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to default template location
|
|
||||||
$defaultPath = $config['paths']['templates'] . '/' . $template . '.php';
|
|
||||||
if (file_exists($defaultPath)) {
|
|
||||||
include $defaultPath;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log error if template not found
|
|
||||||
error_log("Template not found: {$template} in theme: {$themeName}");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all available themes
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function getAvailableThemes(): array
|
|
||||||
{
|
|
||||||
$config = self::getConfig();
|
|
||||||
$availableThemes = $config['available_themes'] ?? [];
|
|
||||||
$themes = [];
|
|
||||||
|
|
||||||
// Add default theme if not already present
|
|
||||||
if (!isset($availableThemes['default'])) {
|
|
||||||
$availableThemes['default'] = 'Default built-in theme';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify each theme exists and has a config file
|
|
||||||
$themesDir = $config['paths']['themes'] ?? (__DIR__ . '/../../themes');
|
|
||||||
foreach ($availableThemes as $id => $name) {
|
|
||||||
if ($id === 'default' || (is_dir("$themesDir/$id") && file_exists("$themesDir/$id/config.php"))) {
|
|
||||||
$themes[$id] = $name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $themes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the theme system
|
|
||||||
Theme::init();
|
|
|
@ -1,5 +0,0 @@
|
||||||
<?php
|
|
||||||
// Pages that should not be used as redirect targets
|
|
||||||
const INVALID_REDIRECT_PAGES = [
|
|
||||||
'', 'login', 'logout', 'register', 'dashboard', '/'
|
|
||||||
];
|
|
|
@ -1,9 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
require_once __DIR__ . '/../helpers/security.php';
|
require_once __DIR__ . '/../helpers/security.php';
|
||||||
|
require_once __DIR__ . '/../helpers/logs.php';
|
||||||
|
|
||||||
function applyCsrfMiddleware() {
|
function applyCsrfMiddleware() {
|
||||||
global $logObject, $user_IP;
|
global $logObject;
|
||||||
$security = SecurityHelper::getInstance();
|
$security = SecurityHelper::getInstance();
|
||||||
|
|
||||||
// Skip CSRF check for GET requests
|
// Skip CSRF check for GET requests
|
||||||
|
@ -33,14 +34,14 @@ function applyCsrfMiddleware() {
|
||||||
$token = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
$token = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||||
if (!$security->verifyCsrfToken($token)) {
|
if (!$security->verifyCsrfToken($token)) {
|
||||||
// Log CSRF attempt
|
// Log CSRF attempt
|
||||||
$ipAddress = $user_IP;
|
$ipAddress = getUserIP();
|
||||||
$logMessage = sprintf(
|
$logMessage = sprintf(
|
||||||
"CSRF attempt detected - IP: %s, Page: %s, User: %s",
|
"CSRF attempt detected - IP: %s, Page: %s, User: %s",
|
||||||
$ipAddress,
|
$ipAddress,
|
||||||
$_GET['page'] ?? 'unknown',
|
$_GET['page'] ?? 'unknown',
|
||||||
$_SESSION['username'] ?? 'anonymous'
|
$_SESSION['username'] ?? 'anonymous'
|
||||||
);
|
);
|
||||||
$logObject->log('error', $logMessage, ['user_id' => null, 'scope' => 'system']);
|
$logObject->insertLog(null, $logMessage, 'system');
|
||||||
|
|
||||||
// Return error message
|
// Return error message
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
|
|
|
@ -1,63 +1,61 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
// connect to database
|
// connect to database
|
||||||
function connectDB($config) {
|
function connectDB($config, $database = '', $dbFile = '', $platformId = '') {
|
||||||
// sqlite database file
|
|
||||||
if ($config['db_type'] === 'sqlite') {
|
// connecting ti a jilo sqlite database
|
||||||
|
if ($database === 'jilo') {
|
||||||
try {
|
try {
|
||||||
$dbFile = $config['sqlite']['sqlite_file'] ?? null;
|
|
||||||
if (!$dbFile || !file_exists($dbFile)) {
|
if (!$dbFile || !file_exists($dbFile)) {
|
||||||
throw new Exception(getError("Database file \"{$dbFile}\"not found."));
|
throw new Exception(getError("Invalid platform ID \"{$platformId}\", database file \"{$dbFile}\" not found."));
|
||||||
}
|
}
|
||||||
$db = new Database([
|
$db = new Database([
|
||||||
'type' => $config['db_type'],
|
'type' => 'sqlite',
|
||||||
'dbFile' => $dbFile,
|
'dbFile' => $dbFile,
|
||||||
]);
|
]);
|
||||||
$pdo = $db->getConnection();
|
return ['db' => $db, 'error' => null];
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
Feedback::flash('ERROR', 'DEFAULT', getError('Error connecting to DB.', $e->getMessage()));
|
return ['db' => null, 'error' => getError('Error connecting to DB.', $e->getMessage())];
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
return $db;
|
|
||||||
|
|
||||||
// mysql/mariadb database
|
// connecting to a jilo-web database of the web app
|
||||||
} elseif ($config['db_type'] === 'mysql' || $config['db_type'] === 'mariadb') {
|
|
||||||
$db = new Database([
|
|
||||||
'type' => $config['db_type'],
|
|
||||||
'host' => $config['sql']['sql_host'] ?? 'localhost',
|
|
||||||
'port' => $config['sql']['sql_port'] ?? '3306',
|
|
||||||
'dbname' => $config['sql']['sql_database'],
|
|
||||||
'user' => $config['sql']['sql_username'],
|
|
||||||
'password' => $config['sql']['sql_password'],
|
|
||||||
]);
|
|
||||||
try {
|
|
||||||
$pdo = $db->getConnection();
|
|
||||||
} catch (Exception $e) {
|
|
||||||
Feedback::flash('ERROR', 'DEFAULT', getError('Error connecting to DB.', $e->getMessage()));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return $db;
|
|
||||||
|
|
||||||
// unknown database
|
|
||||||
} else {
|
} else {
|
||||||
Feedback::flash('ERROR', 'DEFAULT', getError("Error: unknown database type \"{$config['db_type']}\""));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
// sqlite database file
|
||||||
|
if ($config['db']['db_type'] === 'sqlite') {
|
||||||
// connect to Jilo database
|
try {
|
||||||
function connectJiloDB($config, $dbFile = '', $platformId = '') {
|
$db = new Database([
|
||||||
try {
|
'type' => $config['db']['db_type'],
|
||||||
if (!$dbFile || !file_exists($dbFile)) {
|
'dbFile' => $config['db']['sqlite_file'],
|
||||||
throw new Exception(getError("Invalid platform ID \"{$platformId}\", database file \"{$dbFile}\" not found."));
|
]);
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
return ['db' => $db, 'error' => null];
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return ['db' => null, 'error' => getError('Error connecting to DB.', $e->getMessage())];
|
||||||
|
}
|
||||||
|
// mysql/mariadb database
|
||||||
|
} elseif ($config['db']['db_type'] === 'mysql' || $config['db']['db_type'] === 'mariadb') {
|
||||||
|
try {
|
||||||
|
$db = new Database([
|
||||||
|
'type' => $config['db']['db_type'],
|
||||||
|
'host' => $config['db']['sql_host'] ?? 'localhost',
|
||||||
|
'port' => $config['db']['sql_port'] ?? '3306',
|
||||||
|
'dbname' => $config['db']['sql_database'],
|
||||||
|
'user' => $config['db']['sql_username'],
|
||||||
|
'password' => $config['db']['sql_password'],
|
||||||
|
]);
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
return ['db' => $db, 'error' => null];
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return ['db' => null, 'error' => getError('Error connecting to DB.', $e->getMessage())];
|
||||||
|
}
|
||||||
|
// unknown database
|
||||||
|
} else {
|
||||||
|
$error = "Error: unknow database type \"{$config['db']['db_type']}\"";
|
||||||
|
Feedback::flash('ERROR', 'DEFAULT', $error);
|
||||||
|
exit();
|
||||||
}
|
}
|
||||||
$db = new Database([
|
|
||||||
'type' => 'sqlite',
|
|
||||||
'dbFile' => $dbFile,
|
|
||||||
]);
|
|
||||||
return ['db' => $db, 'error' => null];
|
|
||||||
} catch (Exception $e) {
|
|
||||||
return ['db' => null, 'error' => getError('Error connecting to DB.', $e->getMessage())];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
require_once __DIR__ . '/../classes/ratelimiter.php';
|
require_once __DIR__ . '/../classes/ratelimiter.php';
|
||||||
|
require_once __DIR__ . '/../helpers/logs.php';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rate limit middleware for page requests
|
* Rate limit middleware for page requests
|
||||||
|
@ -12,10 +13,10 @@ require_once __DIR__ . '/../classes/ratelimiter.php';
|
||||||
* @return bool True if request is allowed, false if rate limited
|
* @return bool True if request is allowed, false if rate limited
|
||||||
*/
|
*/
|
||||||
function checkRateLimit($database, $endpoint, $userId = null, $existingRateLimiter = null) {
|
function checkRateLimit($database, $endpoint, $userId = null, $existingRateLimiter = null) {
|
||||||
global $app_root, $user_IP;
|
global $app_root;
|
||||||
$isTest = defined('PHPUNIT_RUNNING');
|
$isTest = defined('PHPUNIT_RUNNING');
|
||||||
$rateLimiter = $existingRateLimiter ?? new RateLimiter($database);
|
$rateLimiter = $existingRateLimiter ?? new RateLimiter($database);
|
||||||
$ipAddress = $user_IP;
|
$ipAddress = getUserIP();
|
||||||
|
|
||||||
// Check if request is allowed
|
// Check if request is allowed
|
||||||
if (!$rateLimiter->isPageRequestAllowed($ipAddress, $endpoint, $userId)) {
|
if (!$rateLimiter->isPageRequestAllowed($ipAddress, $endpoint, $userId)) {
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session Middleware
|
||||||
|
*
|
||||||
|
* Validates session status and handles session timeout.
|
||||||
|
* This middleware should be included in all protected pages.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,16 +11,10 @@ return [
|
||||||
'LOGIN_SUCCESS' => 'Login successful.',
|
'LOGIN_SUCCESS' => 'Login successful.',
|
||||||
'LOGIN_FAILED' => 'Login failed. Please check your credentials.',
|
'LOGIN_FAILED' => 'Login failed. Please check your credentials.',
|
||||||
'LOGOUT_SUCCESS' => 'Logout successful. You can log in again.',
|
'LOGOUT_SUCCESS' => 'Logout successful. You can log in again.',
|
||||||
'SESSION_TIMEOUT' => 'Your session has expired. Please log in again.',
|
|
||||||
'IP_BLACKLISTED' => 'Access denied. Your IP address is blacklisted.',
|
'IP_BLACKLISTED' => 'Access denied. Your IP address is blacklisted.',
|
||||||
'IP_NOT_WHITELISTED' => 'Access denied. Your IP address is not whitelisted.',
|
'IP_NOT_WHITELISTED' => 'Access denied. Your IP address is not whitelisted.',
|
||||||
'TOO_MANY_ATTEMPTS' => 'Too many login attempts. Please try again later.',
|
'TOO_MANY_ATTEMPTS' => 'Too many login attempts. Please try again later.',
|
||||||
],
|
],
|
||||||
'REGISTER' => [
|
|
||||||
'SUCCESS' => 'Registration successful. You can log in now.',
|
|
||||||
'FAILED' => 'Registration failed: %s',
|
|
||||||
'DISABLED' => 'Registration is disabled.',
|
|
||||||
],
|
|
||||||
'SECURITY' => [
|
'SECURITY' => [
|
||||||
'WHITELIST_ADD_SUCCESS' => 'IP address successfully added to whitelist.',
|
'WHITELIST_ADD_SUCCESS' => 'IP address successfully added to whitelist.',
|
||||||
'WHITELIST_ADD_FAILED' => 'Failed to add IP to whitelist.',
|
'WHITELIST_ADD_FAILED' => 'Failed to add IP to whitelist.',
|
||||||
|
@ -36,9 +30,10 @@ return [
|
||||||
'PERMISSION_DENIED' => 'Permission denied. You do not have the required rights.',
|
'PERMISSION_DENIED' => 'Permission denied. You do not have the required rights.',
|
||||||
'IP_REQUIRED' => 'IP address is required.',
|
'IP_REQUIRED' => 'IP address is required.',
|
||||||
],
|
],
|
||||||
'THEME' => [
|
'REGISTER' => [
|
||||||
'THEME_CHANGE_SUCCESS' => 'Theme has been changed successfully.',
|
'SUCCESS' => 'Registration successful. You can log in now.',
|
||||||
'THEME_CHANGE_FAILED' => 'Failed to change theme. The selected theme may not be available.',
|
'FAILED' => 'Registration failed: %s',
|
||||||
|
'DISABLED' => 'Registration is disabled.',
|
||||||
],
|
],
|
||||||
'SYSTEM' => [
|
'SYSTEM' => [
|
||||||
'DB_ERROR' => 'Error connecting to the database: %s',
|
'DB_ERROR' => 'Error connecting to the database: %s',
|
||||||
|
|
|
@ -19,8 +19,8 @@ $agentId = filter_input(INPUT_GET, 'agent', FILTER_VALIDATE_INT);
|
||||||
|
|
||||||
require '../app/classes/agent.php';
|
require '../app/classes/agent.php';
|
||||||
require '../app/classes/host.php';
|
require '../app/classes/host.php';
|
||||||
$agentObject = new Agent($db);
|
$agentObject = new Agent($dbWeb);
|
||||||
$hostObject = new Host($db);
|
$hostObject = new Host($dbWeb);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the cache key for an agent
|
* Get the cache key for an agent
|
||||||
|
@ -50,7 +50,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
|
||||||
// Apply rate limiting for adding new contacts
|
// Apply rate limiting for adding new contacts
|
||||||
require '../app/includes/rate_limit_middleware.php';
|
require '../app/includes/rate_limit_middleware.php';
|
||||||
checkRateLimit($db, 'contact', $userId);
|
checkRateLimit($dbWeb, 'contact', $user_id);
|
||||||
|
|
||||||
// Validate agent ID for POST operations
|
// Validate agent ID for POST operations
|
||||||
if ($agentId === false || $agentId === null) {
|
if ($agentId === false || $agentId === null) {
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// connect to database
|
// connect to database
|
||||||
$response = connectJiloDB($config, $platformDetails[0]['jilo_database'], $platform_id);
|
$response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
|
||||||
|
|
||||||
// if DB connection has error, display it and stop here
|
// if DB connection has error, display it and stop here
|
||||||
if ($response['db'] === null) {
|
if ($response['db'] === null) {
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// connect to database
|
// connect to database
|
||||||
$response = connectJiloDB($config, $platformDetails[0]['jilo_database'], $platform_id);
|
$response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
|
||||||
|
|
||||||
// if DB connection has error, display it and stop here
|
// if DB connection has error, display it and stop here
|
||||||
if ($response['db'] === null) {
|
if ($response['db'] === null) {
|
||||||
|
|
|
@ -13,7 +13,8 @@ require '../app/classes/config.php';
|
||||||
require '../app/classes/api_response.php';
|
require '../app/classes/api_response.php';
|
||||||
|
|
||||||
// Initialize required objects
|
// Initialize required objects
|
||||||
$userObject = new User($db);
|
$userObject = new User($dbWeb);
|
||||||
|
$logObject = new Log($dbWeb);
|
||||||
$configObject = new Config();
|
$configObject = new Config();
|
||||||
|
|
||||||
// For AJAX requests
|
// For AJAX requests
|
||||||
|
@ -50,8 +51,8 @@ if (!$isWritable) {
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
// Check if user has permission to edit config
|
// Check if user has permission to edit config
|
||||||
if (!$userObject->hasRight($userId, 'edit config file')) {
|
if (!$userObject->hasRight($user_id, 'edit config file')) {
|
||||||
$logObject->log('error', "Unauthorized: User \"$currentUser\" tried to edit config file. IP: $user_IP", ['user_id' => $userId, 'scope' => 'system']);
|
$logObject->insertLog($user_id, "Unauthorized: User \"$currentUser\" tried to edit config file. IP: $user_IP", 'system');
|
||||||
if ($isAjax) {
|
if ($isAjax) {
|
||||||
ApiResponse::error('Forbidden: You do not have permission to edit the config file', null, 403);
|
ApiResponse::error('Forbidden: You do not have permission to edit the config file', null, 403);
|
||||||
exit;
|
exit;
|
||||||
|
@ -63,7 +64,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
|
||||||
// Apply rate limiting
|
// Apply rate limiting
|
||||||
require '../app/includes/rate_limit_middleware.php';
|
require '../app/includes/rate_limit_middleware.php';
|
||||||
checkRateLimit($db, 'config', $userId);
|
checkRateLimit($dbWeb, 'config', $user_id);
|
||||||
|
|
||||||
// Ensure no output before this point
|
// Ensure no output before this point
|
||||||
ob_clean();
|
ob_clean();
|
||||||
|
@ -73,7 +74,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
// Get raw input
|
// Get raw input
|
||||||
$jsonData = file_get_contents('php://input');
|
$jsonData = file_get_contents('php://input');
|
||||||
if ($jsonData === false) {
|
if ($jsonData === false) {
|
||||||
$logObject->log('error', "Failed to read request data for config update", ['user_id' => $userId, 'scope' => 'system']);
|
$logObject->insertLog($user_id, "Failed to read request data for config update", 'system');
|
||||||
ApiResponse::error('Failed to read request data');
|
ApiResponse::error('Failed to read request data');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
@ -114,11 +115,10 @@ if (!$isAjax) {
|
||||||
* Handles GET requests to display templates.
|
* Handles GET requests to display templates.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if ($userObject->hasRight($userId, 'superuser') ||
|
if ($userObject->hasRight($user_id, 'view config file')) {
|
||||||
$userObject->hasRight($userId, 'view config file')) {
|
|
||||||
include '../app/templates/config.php';
|
include '../app/templates/config.php';
|
||||||
} else {
|
} else {
|
||||||
$logObject->log('error', "Unauthorized: User \"$currentUser\" tried to access \"config\" page. IP: $user_IP", ['user_id' => $userId, 'scope' => 'system']);
|
$logObject->insertLog($user_id, "Unauthorized: User \"$currentUser\" tried to access \"config\" page. IP: $user_IP", 'system');
|
||||||
include '../app/templates/error-unauthorized.php';
|
include '../app/templates/error-unauthorized.php';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,10 +14,17 @@
|
||||||
* - `password`: Change password
|
* - `password`: Change password
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Initialize user object
|
// Check if user is logged in
|
||||||
$userObject = new User($db);
|
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'] ?? '';
|
$action = $_REQUEST['action'] ?? '';
|
||||||
$item = $_REQUEST['item'] ?? '';
|
$item = $_REQUEST['item'] ?? '';
|
||||||
|
|
||||||
|
@ -33,7 +40,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
|
|
||||||
// Apply rate limiting
|
// Apply rate limiting
|
||||||
require_once '../app/includes/rate_limit_middleware.php';
|
require_once '../app/includes/rate_limit_middleware.php';
|
||||||
checkRateLimit($db, 'credentials', $userId);
|
checkRateLimit($dbWeb, 'credentials', $user_id);
|
||||||
|
|
||||||
switch ($item) {
|
switch ($item) {
|
||||||
case '2fa':
|
case '2fa':
|
||||||
|
@ -43,7 +50,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
$code = $_POST['code'] ?? '';
|
$code = $_POST['code'] ?? '';
|
||||||
$secret = $_POST['secret'] ?? '';
|
$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.');
|
Feedback::flash('NOTICE', 'DEFAULT', 'Two-factor authentication has been enabled successfully.');
|
||||||
header("Location: $app_root?page=credentials");
|
header("Location: $app_root?page=credentials");
|
||||||
exit();
|
exit();
|
||||||
|
@ -60,7 +67,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
case 'verify':
|
case 'verify':
|
||||||
// This is a user-initiated verification
|
// This is a user-initiated verification
|
||||||
$code = $_POST['code'] ?? '';
|
$code = $_POST['code'] ?? '';
|
||||||
if ($userObject->verifyTwoFactor($userId, $code)) {
|
if ($userObject->verifyTwoFactor($user_id, $code)) {
|
||||||
$_SESSION['2fa_verified'] = true;
|
$_SESSION['2fa_verified'] = true;
|
||||||
header("Location: $app_root?page=dashboard");
|
header("Location: $app_root?page=dashboard");
|
||||||
exit();
|
exit();
|
||||||
|
@ -72,7 +79,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'disable':
|
case 'disable':
|
||||||
if ($userObject->disableTwoFactor($userId)) {
|
if ($userObject->disableTwoFactor($user_id)) {
|
||||||
Feedback::flash('NOTICE', 'DEFAULT', 'Two-factor authentication has been disabled.');
|
Feedback::flash('NOTICE', 'DEFAULT', 'Two-factor authentication has been disabled.');
|
||||||
} else {
|
} else {
|
||||||
Feedback::flash('ERROR', 'DEFAULT', 'Failed to disable two-factor authentication.');
|
Feedback::flash('ERROR', 'DEFAULT', 'Failed to disable two-factor authentication.');
|
||||||
|
@ -89,7 +96,8 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
$validator = new Validator($_POST);
|
$validator = new Validator($_POST);
|
||||||
$rules = [
|
$rules = [
|
||||||
'current_password' => [
|
'current_password' => [
|
||||||
'required' => true
|
'required' => true,
|
||||||
|
'min' => 8
|
||||||
],
|
],
|
||||||
'new_password' => [
|
'new_password' => [
|
||||||
'required' => true,
|
'required' => true,
|
||||||
|
@ -107,7 +115,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
exit();
|
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.');
|
Feedback::flash('NOTICE', 'DEFAULT', 'Password has been changed successfully.');
|
||||||
} else {
|
} else {
|
||||||
Feedback::flash('ERROR', 'DEFAULT', 'Failed to change password. Please verify your current password.');
|
Feedback::flash('ERROR', 'DEFAULT', 'Failed to change password. Please verify your current password.');
|
||||||
|
@ -128,12 +136,12 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
$security->generateCsrfToken();
|
$security->generateCsrfToken();
|
||||||
|
|
||||||
// Get 2FA status for the template
|
// Get 2FA status for the template
|
||||||
$has2fa = $userObject->isTwoFactorEnabled($userId);
|
$has2fa = $userObject->isTwoFactorEnabled($user_id);
|
||||||
|
|
||||||
switch ($action) {
|
switch ($action) {
|
||||||
case 'setup':
|
case 'setup':
|
||||||
if (!$has2fa) {
|
if (!$has2fa) {
|
||||||
$result = $userObject->enableTwoFactor($userId);
|
$result = $userObject->enableTwoFactor($user_id);
|
||||||
if ($result['success']) {
|
if ($result['success']) {
|
||||||
$setupData = $result['data'];
|
$setupData = $result['data'];
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -16,7 +16,7 @@ require '../app/classes/conference.php';
|
||||||
require '../app/classes/participant.php';
|
require '../app/classes/participant.php';
|
||||||
|
|
||||||
// connect to database
|
// connect to database
|
||||||
$response = connectJiloDB($config, $platformDetails[0]['jilo_database'], $platform_id);
|
$response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
|
||||||
|
|
||||||
// if DB connection has error, display it and stop here
|
// if DB connection has error, display it and stop here
|
||||||
if ($response['db'] === null) {
|
if ($response['db'] === null) {
|
||||||
|
|
|
@ -7,11 +7,11 @@ require '../app/classes/agent.php';
|
||||||
require '../app/classes/conference.php';
|
require '../app/classes/conference.php';
|
||||||
require '../app/classes/host.php';
|
require '../app/classes/host.php';
|
||||||
|
|
||||||
$agentObject = new Agent($db);
|
$agentObject = new Agent($dbWeb);
|
||||||
$hostObject = new Host($db);
|
$hostObject = new Host($dbWeb);
|
||||||
|
|
||||||
// Connect to Jilo database for log data
|
// Connect to Jilo database for log data
|
||||||
$response = connectJiloDB($config, $platformDetails[0]['jilo_database'], $platform_id);
|
$response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
|
||||||
if ($response['db'] === null) {
|
if ($response['db'] === null) {
|
||||||
Feedback::flash('ERROR', 'DEFAULT', $response['error']);
|
Feedback::flash('ERROR', 'DEFAULT', $response['error']);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
require '../app/classes/agent.php';
|
require '../app/classes/agent.php';
|
||||||
require '../app/classes/host.php';
|
require '../app/classes/host.php';
|
||||||
|
|
||||||
$agentObject = new Agent($db);
|
$agentObject = new Agent($dbWeb);
|
||||||
$hostObject = new Host($db);
|
$hostObject = new Host($dbWeb);
|
||||||
|
|
||||||
// Define metrics to display
|
// Define metrics to display
|
||||||
$metrics = [
|
$metrics = [
|
||||||
|
|
|
@ -19,13 +19,13 @@ unset($error);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// connect to database
|
// connect to database
|
||||||
$db = connectDB($config);
|
$db = connectDB($config)['db'];
|
||||||
|
|
||||||
// Initialize RateLimiter
|
// Initialize RateLimiter
|
||||||
require_once '../app/classes/ratelimiter.php';
|
require_once '../app/classes/ratelimiter.php';
|
||||||
$rateLimiter = new RateLimiter($db);
|
$rateLimiter = new RateLimiter($db);
|
||||||
|
|
||||||
// Get user IP
|
// Get user IP
|
||||||
require_once '../app/helpers/ip_helper.php';
|
|
||||||
$user_IP = getUserIP();
|
$user_IP = getUserIP();
|
||||||
|
|
||||||
$action = $_REQUEST['action'] ?? '';
|
$action = $_REQUEST['action'] ?? '';
|
||||||
|
@ -33,23 +33,21 @@ try {
|
||||||
if ($action === 'verify' && isset($_SESSION['2fa_pending_user_id'])) {
|
if ($action === 'verify' && isset($_SESSION['2fa_pending_user_id'])) {
|
||||||
// Handle 2FA verification
|
// Handle 2FA verification
|
||||||
$code = $_POST['code'] ?? '';
|
$code = $_POST['code'] ?? '';
|
||||||
$pending2FA = Session::get2FAPending();
|
$userId = $_SESSION['2fa_pending_user_id'];
|
||||||
|
$username = $_SESSION['2fa_pending_username'];
|
||||||
if (!$pending2FA) {
|
$rememberMe = isset($_SESSION['2fa_pending_remember']);
|
||||||
header('Location: ' . htmlspecialchars($app_root) . '?page=login');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once '../app/classes/twoFactorAuth.php';
|
require_once '../app/classes/twoFactorAuth.php';
|
||||||
$twoFactorAuth = new TwoFactorAuthentication($db);
|
$twoFactorAuth = new TwoFactorAuthentication($db);
|
||||||
|
|
||||||
if ($twoFactorAuth->verify($pending2FA['user_id'], $code)) {
|
if ($twoFactorAuth->verify($userId, $code)) {
|
||||||
// Complete login
|
// Complete login
|
||||||
handleSuccessfulLogin($pending2FA['user_id'], $pending2FA['username'],
|
handleSuccessfulLogin($userId, $username, $rememberMe, $config, $logObject, $user_IP);
|
||||||
$pending2FA['remember_me'], $config, $app_root, $logObject, $user_IP);
|
|
||||||
|
|
||||||
// Clean up 2FA session data
|
// Clean up 2FA session data
|
||||||
Session::clear2FAPending();
|
unset($_SESSION['2fa_pending_user_id']);
|
||||||
|
unset($_SESSION['2fa_pending_username']);
|
||||||
|
unset($_SESSION['2fa_pending_remember']);
|
||||||
|
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
@ -62,9 +60,6 @@ try {
|
||||||
// Get any new feedback messages
|
// Get any new feedback messages
|
||||||
include '../app/helpers/feedback.php';
|
include '../app/helpers/feedback.php';
|
||||||
|
|
||||||
// Make userId available to template
|
|
||||||
$userId = $pending2FA['user_id'];
|
|
||||||
|
|
||||||
// Load the 2FA verification template
|
// Load the 2FA verification template
|
||||||
include '../app/templates/credentials-2fa-verify.php';
|
include '../app/templates/credentials-2fa-verify.php';
|
||||||
exit();
|
exit();
|
||||||
|
@ -97,7 +92,7 @@ try {
|
||||||
|
|
||||||
// Process reset request
|
// Process reset request
|
||||||
require_once '../app/classes/passwordReset.php';
|
require_once '../app/classes/passwordReset.php';
|
||||||
$resetHandler = new PasswordReset($db, $config);
|
$resetHandler = new PasswordReset($db);
|
||||||
$result = $resetHandler->requestReset($email);
|
$result = $resetHandler->requestReset($email);
|
||||||
|
|
||||||
// Always show same message whether email exists or not for security
|
// Always show same message whether email exists or not for security
|
||||||
|
@ -123,7 +118,7 @@ try {
|
||||||
// Handle password reset
|
// Handle password reset
|
||||||
try {
|
try {
|
||||||
require_once '../app/classes/passwordReset.php';
|
require_once '../app/classes/passwordReset.php';
|
||||||
$resetHandler = new PasswordReset($db, $config);
|
$resetHandler = new PasswordReset($db);
|
||||||
$token = $_GET['token'];
|
$token = $_GET['token'];
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
@ -201,7 +196,8 @@ try {
|
||||||
],
|
],
|
||||||
'password' => [
|
'password' => [
|
||||||
'type' => 'string',
|
'type' => 'string',
|
||||||
'required' => true
|
'required' => true,
|
||||||
|
'min' => 5
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -224,6 +220,9 @@ try {
|
||||||
if ($rateLimiter->tooManyAttempts($username, $user_IP)) {
|
if ($rateLimiter->tooManyAttempts($username, $user_IP)) {
|
||||||
throw new Exception(Feedback::get('LOGIN', 'TOO_MANY_ATTEMPTS')['message']);
|
throw new Exception(Feedback::get('LOGIN', 'TOO_MANY_ATTEMPTS')['message']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Record this attempt
|
||||||
|
$rateLimiter->attempt($username, $user_IP);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt login
|
// Attempt login
|
||||||
|
@ -233,8 +232,11 @@ try {
|
||||||
switch ($loginResult['status']) {
|
switch ($loginResult['status']) {
|
||||||
case 'requires_2fa':
|
case 'requires_2fa':
|
||||||
// Store pending 2FA info
|
// Store pending 2FA info
|
||||||
Session::store2FAPending($loginResult['user_id'], $loginResult['username'],
|
$_SESSION['2fa_pending_user_id'] = $loginResult['user_id'];
|
||||||
isset($formData['remember_me']));
|
$_SESSION['2fa_pending_username'] = $loginResult['username'];
|
||||||
|
if (isset($formData['remember_me'])) {
|
||||||
|
$_SESSION['2fa_pending_remember'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Redirect to 2FA verification
|
// Redirect to 2FA verification
|
||||||
header('Location: ?page=login&action=verify');
|
header('Location: ?page=login&action=verify');
|
||||||
|
@ -243,7 +245,7 @@ try {
|
||||||
case 'success':
|
case 'success':
|
||||||
// Complete login
|
// Complete login
|
||||||
handleSuccessfulLogin($loginResult['user_id'], $loginResult['username'],
|
handleSuccessfulLogin($loginResult['user_id'], $loginResult['username'],
|
||||||
isset($formData['remember_me']), $config, $app_root, $logObject, $user_IP);
|
isset($formData['remember_me']), $config, $logObject, $user_IP);
|
||||||
exit();
|
exit();
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -256,9 +258,8 @@ try {
|
||||||
// Log the failed attempt
|
// Log the failed attempt
|
||||||
Feedback::flash('ERROR', 'DEFAULT', $e->getMessage());
|
Feedback::flash('ERROR', 'DEFAULT', $e->getMessage());
|
||||||
if (isset($username)) {
|
if (isset($username)) {
|
||||||
$userId = $userObject->getUserId($username)[0]['id'] ?? 0;
|
$user_id = $userObject->getUserId($username)[0]['id'] ?? 0;
|
||||||
$logObject->log('error', "Login: Failed login attempt for user \"$username\". IP: $user_IP. Reason: {$e->getMessage()}", ['user_id' => $userId, 'scope' => 'user']);
|
$logObject->insertLog($user_id, "Login: Failed login attempt for user \"$username\". IP: $user_IP. Reason: {$e->getMessage()}", 'user');
|
||||||
$rateLimiter->attempt($username, $user_IP);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -268,7 +269,7 @@ try {
|
||||||
|
|
||||||
// Show configured login message if any
|
// Show configured login message if any
|
||||||
if (!empty($config['login_message'])) {
|
if (!empty($config['login_message'])) {
|
||||||
echo Feedback::render('NOTICE', 'DEFAULT', $config['login_message'], false, false, false);
|
echo Feedback::render('NOTICE', 'DEFAULT', $config['login_message'], false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get any new feedback messages
|
// Get any new feedback messages
|
||||||
|
@ -280,26 +281,42 @@ include '../app/templates/form-login.php';
|
||||||
/**
|
/**
|
||||||
* Handle successful login by setting up session and cookies
|
* Handle successful login by setting up session and cookies
|
||||||
*/
|
*/
|
||||||
function handleSuccessfulLogin($userId, $username, $rememberMe, $config, $app_root, $logObject, $userIP) {
|
function handleSuccessfulLogin($userId, $username, $rememberMe, $config, $logObject, $userIP) {
|
||||||
// Create authenticated session
|
if ($rememberMe) {
|
||||||
Session::createAuthSession($userId, $username, $rememberMe, $config);
|
// 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
|
// Log successful login
|
||||||
$logObject->log('info', "Login: User \"$username\" logged in. IP: $userIP", ['user_id' => $userId, 'scope' => 'user']);
|
$logObject->insertLog($userId, "Login: User \"$username\" logged in. IP: $userIP", 'user');
|
||||||
|
|
||||||
// Set success message
|
// Set success message and redirect
|
||||||
Feedback::flash('LOGIN', 'LOGIN_SUCCESS');
|
Feedback::flash('LOGIN', 'LOGIN_SUCCESS');
|
||||||
|
header('Location: ' . htmlspecialchars($app_root));
|
||||||
// After successful login, redirect to original page if provided in URL param or POST
|
|
||||||
$redirect = $app_root;
|
|
||||||
$candidate = $_POST['redirect'] ?? $_GET['redirect'] ?? '';
|
|
||||||
$trimmed = trim($candidate, '/?');
|
|
||||||
if (
|
|
||||||
(strpos($candidate, '/') === 0 || strpos($candidate, '?') === 0)
|
|
||||||
&& !in_array($trimmed, INVALID_REDIRECT_PAGES, true)
|
|
||||||
) {
|
|
||||||
$redirect = $candidate;
|
|
||||||
}
|
|
||||||
header('Location: ' . htmlspecialchars($redirect));
|
|
||||||
exit();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,17 +8,12 @@
|
||||||
* It supports pagination and filtering.
|
* It supports pagination and filtering.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Define plugin base path if not already defined
|
// Get any new feedback messages
|
||||||
if (!defined('PLUGIN_LOGS_PATH')) {
|
include '../app/helpers/feedback.php';
|
||||||
define('PLUGIN_LOGS_PATH', dirname(__FILE__, 2) . '/');
|
|
||||||
}
|
|
||||||
require_once PLUGIN_LOGS_PATH . 'models/Log.php';
|
|
||||||
require_once PLUGIN_LOGS_PATH . 'models/LoggerFactory.php';
|
|
||||||
require_once dirname(__FILE__, 4) . '/app/classes/user.php';
|
|
||||||
|
|
||||||
// Check for rights; user or system
|
// Check for rights; user or system
|
||||||
$has_system_access = ($userObject->hasRight($userId, 'superuser') ||
|
$has_system_access = ($userObject->hasRight($user_id, 'superuser') ||
|
||||||
$userObject->hasRight($userId, 'view app logs'));
|
$userObject->hasRight($user_id, 'view app logs'));
|
||||||
|
|
||||||
// Get current page for pagination
|
// Get current page for pagination
|
||||||
$currentPage = $_REQUEST['page_num'] ?? 1;
|
$currentPage = $_REQUEST['page_num'] ?? 1;
|
||||||
|
@ -74,8 +69,8 @@ if (isset($_REQUEST['tab'])) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepare the result
|
// prepare the result
|
||||||
$search = $logObject->readLog($userId, $scope, $offset, $items_per_page, $filters);
|
$search = $logObject->readLog($user_id, $scope, $offset, $items_per_page, $filters);
|
||||||
$search_all = $logObject->readLog($userId, $scope, 0, 0, $filters);
|
$search_all = $logObject->readLog($user_id, $scope, 0, 0, $filters);
|
||||||
|
|
||||||
if (!empty($search)) {
|
if (!empty($search)) {
|
||||||
// we get total items and number of pages
|
// we get total items and number of pages
|
||||||
|
@ -91,7 +86,6 @@ if (!empty($search)) {
|
||||||
$log_record = array(
|
$log_record = array(
|
||||||
// assign title to the field in the array record
|
// assign title to the field in the array record
|
||||||
'time' => $item['time'],
|
'time' => $item['time'],
|
||||||
'log level' => $item['level'],
|
|
||||||
'log message' => $item['message']
|
'log message' => $item['message']
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -100,7 +94,6 @@ if (!empty($search)) {
|
||||||
'userID' => $item['user_id'],
|
'userID' => $item['user_id'],
|
||||||
'username' => $item['username'],
|
'username' => $item['username'],
|
||||||
'time' => $item['time'],
|
'time' => $item['time'],
|
||||||
'log level' => $item['level'],
|
|
||||||
'log message' => $item['message']
|
'log message' => $item['message']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -110,13 +103,7 @@ if (!empty($search)) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$username = $userObject->getUserDetails($userId)[0]['username'];
|
$username = $userObject->getUserDetails($user_id)[0]['username'];
|
||||||
|
|
||||||
// Get any new feedback messages
|
// Load the template
|
||||||
include dirname(__FILE__, 4) . '/app/helpers/feedback.php';
|
include '../app/templates/logs.php';
|
||||||
|
|
||||||
// Load plugin helpers
|
|
||||||
include PLUGIN_LOGS_PATH . 'helpers/logs_view_helper.php';
|
|
||||||
|
|
||||||
// Display messages list
|
|
||||||
include PLUGIN_LOGS_PATH . 'views/logs.php';
|
|
|
@ -9,7 +9,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// connect to database
|
// connect to database
|
||||||
$response = connectJiloDB($config, $platformDetails[0]['jilo_database'], $platform_id);
|
$response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
|
||||||
|
|
||||||
// if DB connection has error, display it and stop here
|
// if DB connection has error, display it and stop here
|
||||||
if ($response['db'] === null) {
|
if ($response['db'] === null) {
|
||||||
|
|
|
@ -30,11 +30,11 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
|
|
||||||
// Apply rate limiting for profile operations
|
// Apply rate limiting for profile operations
|
||||||
require_once '../app/includes/rate_limit_middleware.php';
|
require_once '../app/includes/rate_limit_middleware.php';
|
||||||
checkRateLimit($db, 'profile', $userId);
|
checkRateLimit($dbWeb, 'profile', $user_id);
|
||||||
|
|
||||||
// avatar removal
|
// avatar removal
|
||||||
if ($item === 'avatar' && $action === 'remove') {
|
if ($item === 'avatar' && $action === 'remove') {
|
||||||
$validator = new Validator(['user_id' => $userId]);
|
$validator = new Validator(['user_id' => $user_id]);
|
||||||
$rules = [
|
$rules = [
|
||||||
'user_id' => [
|
'user_id' => [
|
||||||
'required' => true,
|
'required' => true,
|
||||||
|
@ -48,7 +48,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
exit();
|
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) {
|
if ($result === true) {
|
||||||
Feedback::flash('NOTICE', 'DEFAULT', "Avatar for user \"{$userDetails[0]['username']}\" is removed.");
|
Feedback::flash('NOTICE', 'DEFAULT', "Avatar for user \"{$userDetails[0]['username']}\" is removed.");
|
||||||
} else {
|
} else {
|
||||||
|
@ -89,54 +89,50 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
'timezone' => htmlspecialchars($_POST['timezone'] ?? ''),
|
'timezone' => htmlspecialchars($_POST['timezone'] ?? ''),
|
||||||
'bio' => htmlspecialchars($_POST['bio'] ?? ''),
|
'bio' => htmlspecialchars($_POST['bio'] ?? ''),
|
||||||
];
|
];
|
||||||
$result = $userObject->editUser($userId, $updatedUser);
|
$result = $userObject->editUser($user_id, $updatedUser);
|
||||||
if ($result === true) {
|
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 {
|
} else {
|
||||||
Feedback::flash('ERROR', 'DEFAULT', "Editing the user details failed. Error: $result");
|
Feedback::flash('ERROR', 'DEFAULT', "Editing the user details failed. Error: $result");
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the rights
|
// update the rights
|
||||||
// Get current rights IDs
|
if (isset($_POST['rights'])) {
|
||||||
$userRightsIds = array_column($userRights, 'right_id');
|
$validator = new Validator(['rights' => $_POST['rights']]);
|
||||||
|
$rules = [
|
||||||
|
'rights' => [
|
||||||
|
'array' => true
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
// If no rights are selected, remove all rights
|
if (!$validator->validate($rules)) {
|
||||||
if (!isset($_POST['rights'])) {
|
Feedback::flash('ERROR', 'DEFAULT', $validator->getFirstError());
|
||||||
$_POST['rights'] = [];
|
header("Location: $app_root?page=profile");
|
||||||
}
|
exit();
|
||||||
|
|
||||||
$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);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// what rights we need to remove
|
$newRights = $_POST['rights'];
|
||||||
$rightsToRemove = array_diff($userRightsIds, $newRights);
|
// extract the new right_ids
|
||||||
if (!empty($rightsToRemove)) {
|
$userRightsIds = array_column($userRights, 'right_id');
|
||||||
foreach ($rightsToRemove as $rightId) {
|
// what rights we need to add
|
||||||
$userObject->removeUserRight($userId, $rightId);
|
$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
|
// update the avatar
|
||||||
if (!empty($_FILES['avatar_file']['tmp_name'])) {
|
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");
|
header("Location: $app_root?page=profile");
|
||||||
|
|
|
@ -8,31 +8,24 @@
|
||||||
* and redirects to the login page on success or displays an error message on failure.
|
* and redirects to the login page on success or displays an error message on failure.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Define plugin base path if not already defined
|
|
||||||
if (!defined('PLUGIN_REGISTER_PATH')) {
|
|
||||||
define('PLUGIN_REGISTER_PATH', dirname(__FILE__, 2) . '/');
|
|
||||||
}
|
|
||||||
require_once PLUGIN_REGISTER_PATH . 'models/register.php';
|
|
||||||
require_once dirname(__FILE__, 4) . '/app/classes/user.php';
|
|
||||||
require_once dirname(__FILE__, 4) . '/app/classes/validator.php';
|
|
||||||
require_once dirname(__FILE__, 4) . '/app/helpers/security.php';
|
|
||||||
|
|
||||||
// registration is allowed, go on
|
// registration is allowed, go on
|
||||||
if ($config['registration_enabled'] == true) {
|
if ($config['registration_enabled'] == true) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
global $db, $logObject, $userObject;
|
global $dbWeb, $logObject, $userObject;
|
||||||
|
|
||||||
if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {
|
if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {
|
||||||
|
|
||||||
// Apply rate limiting
|
// Apply rate limiting
|
||||||
require_once dirname(__FILE__, 4) . '/app/includes/rate_limit_middleware.php';
|
require '../app/includes/rate_limit_middleware.php';
|
||||||
checkRateLimit($db, 'register');
|
checkRateLimit($dbWeb, 'register');
|
||||||
|
|
||||||
|
require_once '../app/classes/validator.php';
|
||||||
|
require_once '../app/helpers/security.php';
|
||||||
$security = SecurityHelper::getInstance();
|
$security = SecurityHelper::getInstance();
|
||||||
|
|
||||||
// Sanitize input
|
// Sanitize input
|
||||||
$formData = $security->sanitizeArray($_POST, ['username', 'password', 'confirm_password', 'csrf_token', 'terms']);
|
$formData = $security->sanitizeArray($_POST, ['username', 'password', 'confirm_password', 'csrf_token']);
|
||||||
|
|
||||||
// Validate CSRF token
|
// Validate CSRF token
|
||||||
if (!$security->verifyCsrfToken($formData['csrf_token'] ?? '')) {
|
if (!$security->verifyCsrfToken($formData['csrf_token'] ?? '')) {
|
||||||
|
@ -54,10 +47,6 @@ if ($config['registration_enabled'] == true) {
|
||||||
'confirm_password' => [
|
'confirm_password' => [
|
||||||
'required' => true,
|
'required' => true,
|
||||||
'matches' => 'password'
|
'matches' => 'password'
|
||||||
],
|
|
||||||
'terms' => [
|
|
||||||
'required' => true,
|
|
||||||
'equals' => 'on'
|
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -67,42 +56,41 @@ if ($config['registration_enabled'] == true) {
|
||||||
$password = $formData['password'];
|
$password = $formData['password'];
|
||||||
|
|
||||||
// registering
|
// registering
|
||||||
$register = new Register($db);
|
$result = $userObject->register($username, $password);
|
||||||
$result = $register->register($username, $password);
|
|
||||||
|
|
||||||
// redirect to login
|
// redirect to login
|
||||||
if ($result === true) {
|
if ($result === true) {
|
||||||
// Get the new user's ID for logging
|
// Get the new user's ID for logging
|
||||||
$userId = $userObject->getUserId($username)[0]['id'];
|
$user_id = $userObject->getUserId($username)[0]['id'];
|
||||||
$logObject->log('info', "Registration: New user \"$username\" registered successfully. IP: $user_IP", ['user_id' => $userId, 'scope' => 'user']);
|
$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.");
|
Feedback::flash('NOTICE', 'DEFAULT', "Registration successful. You can log in now.");
|
||||||
header('Location: ' . htmlspecialchars($app_root . '?page=login'));
|
header('Location: ' . htmlspecialchars($app_root));
|
||||||
exit();
|
exit();
|
||||||
// registration fail, redirect to login
|
// registration fail, redirect to login
|
||||||
} else {
|
} else {
|
||||||
$logObject->log('error', "Registration: Failed registration attempt for user \"$username\". IP: $user_IP. Reason: $result", ['user_id' => null, 'scope' => 'system']);
|
$logObject->insertLog(0, "Registration: Failed registration attempt for user \"$username\". IP: $user_IP. Reason: $result", 'system');
|
||||||
Feedback::flash('ERROR', 'DEFAULT', "Registration failed. $result");
|
Feedback::flash('ERROR', 'DEFAULT', "Registration failed. $result");
|
||||||
header('Location: ' . htmlspecialchars($app_root . '?page=register'));
|
header('Location: ' . htmlspecialchars($app_root));
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$error = $validator->getFirstError();
|
$error = $validator->getFirstError();
|
||||||
$logObject->log('error', "Registration: Failed validation for user \"" . ($username ?? 'unknown') . "\". IP: $user_IP. Reason: $error", ['user_id' => null, 'scope' => 'system']);
|
$logObject->insertLog(0, "Registration: Failed validation for user \"" . ($username ?? 'unknown') . "\". IP: $user_IP. Reason: $error", 'system');
|
||||||
Feedback::flash('ERROR', 'DEFAULT', $error);
|
Feedback::flash('ERROR', 'DEFAULT', $error);
|
||||||
header('Location: ' . htmlspecialchars($app_root . '?page=register'));
|
header('Location: ' . htmlspecialchars($app_root . '?page=register'));
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$logObject->log('error', "Registration: System error. IP: $user_IP. Error: " . $e->getMessage(), ['user_id' => null, 'scope' => 'system']);
|
$logObject->insertLog(0, "Registration: System error. IP: $user_IP. Error: " . $e->getMessage(), 'system');
|
||||||
Feedback::flash('ERROR', 'DEFAULT', $e->getMessage());
|
Feedback::flash('ERROR', 'DEFAULT', $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get any new feedback messages
|
// Get any new feedback messages
|
||||||
include dirname(__FILE__, 4) . '/app/helpers/feedback.php';
|
include '../app/helpers/feedback.php';
|
||||||
|
|
||||||
// Load the template
|
// Load the template
|
||||||
include PLUGIN_REGISTER_PATH . 'views/form-register.php';
|
include '../app/templates/form-register.php';
|
||||||
|
|
||||||
// registration disabled
|
// registration disabled
|
||||||
} else {
|
} else {
|
|
@ -1,10 +1,15 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
// Check if user has any of the required rights
|
// Check if user has any of the required rights
|
||||||
if (!($userObject->hasRight($userId, 'superuser') ||
|
if (!($userObject->hasRight($user_id, 'superuser') ||
|
||||||
$userObject->hasRight($userId, 'edit whitelist') ||
|
$userObject->hasRight($user_id, 'edit whitelist') ||
|
||||||
$userObject->hasRight($userId, 'edit blacklist') ||
|
$userObject->hasRight($user_id, 'edit blacklist') ||
|
||||||
$userObject->hasRight($userId, 'edit ratelimiting'))) {
|
$userObject->hasRight($user_id, 'edit ratelimiting'))) {
|
||||||
|
include '../app/templates/error-unauthorized.php';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($currentUser)) {
|
||||||
include '../app/templates/error-unauthorized.php';
|
include '../app/templates/error-unauthorized.php';
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
@ -14,7 +19,7 @@ $section = isset($_POST['section']) ? $_POST['section'] : (isset($_GET['section'
|
||||||
|
|
||||||
// Initialize RateLimiter
|
// Initialize RateLimiter
|
||||||
require_once '../app/classes/ratelimiter.php';
|
require_once '../app/classes/ratelimiter.php';
|
||||||
$rateLimiter = new RateLimiter($db);
|
$rateLimiter = new RateLimiter($dbWeb);
|
||||||
|
|
||||||
// Handle form submissions
|
// Handle form submissions
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||||
|
@ -22,7 +27,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||||
|
|
||||||
// Apply rate limiting for security operations
|
// Apply rate limiting for security operations
|
||||||
require_once '../app/includes/rate_limit_middleware.php';
|
require_once '../app/includes/rate_limit_middleware.php';
|
||||||
checkRateLimit($db, 'security', $userId);
|
checkRateLimit($dbWeb, 'security', $user_id);
|
||||||
|
|
||||||
$action = $_POST['action'];
|
$action = $_POST['action'];
|
||||||
$validator = new Validator($_POST);
|
$validator = new Validator($_POST);
|
||||||
|
@ -30,7 +35,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||||
try {
|
try {
|
||||||
switch ($action) {
|
switch ($action) {
|
||||||
case 'add_whitelist':
|
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');
|
Feedback::flash('SECURITY', 'PERMISSION_DENIED');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -49,7 +54,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||||
|
|
||||||
if ($validator->validate($rules)) {
|
if ($validator->validate($rules)) {
|
||||||
$is_network = isset($_POST['is_network']) && $_POST['is_network'] === 'on';
|
$is_network = isset($_POST['is_network']) && $_POST['is_network'] === 'on';
|
||||||
if (!$rateLimiter->addToWhitelist($_POST['ip_address'], $is_network, $_POST['description'] ?? '', $currentUser, $userId)) {
|
if (!$rateLimiter->addToWhitelist($_POST['ip_address'], $is_network, $_POST['description'] ?? '', $currentUser, $user_id)) {
|
||||||
Feedback::flash('SECURITY', 'WHITELIST_ADD_FAILED');
|
Feedback::flash('SECURITY', 'WHITELIST_ADD_FAILED');
|
||||||
} else {
|
} else {
|
||||||
Feedback::flash('SECURITY', 'WHITELIST_ADD_SUCCESS');
|
Feedback::flash('SECURITY', 'WHITELIST_ADD_SUCCESS');
|
||||||
|
@ -60,7 +65,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'remove_whitelist':
|
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');
|
Feedback::flash('SECURITY', 'PERMISSION_DENIED');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -74,7 +79,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($validator->validate($rules)) {
|
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');
|
Feedback::flash('SECURITY', 'WHITELIST_REMOVE_FAILED');
|
||||||
} else {
|
} else {
|
||||||
Feedback::flash('SECURITY', 'WHITELIST_REMOVE_SUCCESS');
|
Feedback::flash('SECURITY', 'WHITELIST_REMOVE_SUCCESS');
|
||||||
|
@ -85,7 +90,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'add_blacklist':
|
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');
|
Feedback::flash('SECURITY', 'PERMISSION_DENIED');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -111,7 +116,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||||
$is_network = isset($_POST['is_network']) && $_POST['is_network'] === 'on';
|
$is_network = isset($_POST['is_network']) && $_POST['is_network'] === 'on';
|
||||||
$expiry_hours = !empty($_POST['expiry_hours']) ? (int)$_POST['expiry_hours'] : null;
|
$expiry_hours = !empty($_POST['expiry_hours']) ? (int)$_POST['expiry_hours'] : null;
|
||||||
|
|
||||||
if (!$rateLimiter->addToBlacklist($_POST['ip_address'], $is_network, $_POST['reason'], $currentUser, $userId, $expiry_hours)) {
|
if (!$rateLimiter->addToBlacklist($_POST['ip_address'], $is_network, $_POST['reason'], $currentUser, $user_id, $expiry_hours)) {
|
||||||
Feedback::flash('SECURITY', 'BLACKLIST_ADD_FAILED');
|
Feedback::flash('SECURITY', 'BLACKLIST_ADD_FAILED');
|
||||||
} else {
|
} else {
|
||||||
Feedback::flash('SECURITY', 'BLACKLIST_ADD_SUCCESS');
|
Feedback::flash('SECURITY', 'BLACKLIST_ADD_SUCCESS');
|
||||||
|
@ -122,7 +127,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'remove_blacklist':
|
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');
|
Feedback::flash('SECURITY', 'PERMISSION_DENIED');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -136,7 +141,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($validator->validate($rules)) {
|
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');
|
Feedback::flash('SECURITY', 'BLACKLIST_REMOVE_FAILED');
|
||||||
} else {
|
} else {
|
||||||
Feedback::flash('SECURITY', 'BLACKLIST_REMOVE_SUCCESS');
|
Feedback::flash('SECURITY', 'BLACKLIST_REMOVE_SUCCESS');
|
||||||
|
|
|
@ -21,8 +21,8 @@ $host = $_REQUEST['host'] ?? '';
|
||||||
require '../app/classes/host.php';
|
require '../app/classes/host.php';
|
||||||
require '../app/classes/agent.php';
|
require '../app/classes/agent.php';
|
||||||
|
|
||||||
$hostObject = new Host($db);
|
$hostObject = new Host($dbWeb);
|
||||||
$agentObject = new Agent($db);
|
$agentObject = new Agent($dbWeb);
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
/**
|
/**
|
||||||
|
@ -31,7 +31,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
|
|
||||||
// Apply rate limiting for profile operations
|
// Apply rate limiting for profile operations
|
||||||
require_once '../app/includes/rate_limit_middleware.php';
|
require_once '../app/includes/rate_limit_middleware.php';
|
||||||
checkRateLimit($db, 'profile', $userId);
|
checkRateLimit($dbWeb, 'profile', $user_id);
|
||||||
|
|
||||||
// Get hash from URL if present
|
// Get hash from URL if present
|
||||||
$hash = parse_url($_SERVER['REQUEST_URI'], PHP_URL_FRAGMENT) ?? '';
|
$hash = parse_url($_SERVER['REQUEST_URI'], PHP_URL_FRAGMENT) ?? '';
|
||||||
|
@ -170,7 +170,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
* Handles GET requests to display templates.
|
* Handles GET requests to display templates.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if ($userObject->hasRight($userId, 'view settings') || $userObject->hasRight($userId, 'superuser')) {
|
if ($userObject->hasRight($user_id, 'view settings')) {
|
||||||
$jilo_agent_types = $agentObject->getAgentTypes();
|
$jilo_agent_types = $agentObject->getAgentTypes();
|
||||||
include '../app/templates/settings.php';
|
include '../app/templates/settings.php';
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -13,8 +13,8 @@ include '../app/helpers/feedback.php';
|
||||||
|
|
||||||
require '../app/classes/agent.php';
|
require '../app/classes/agent.php';
|
||||||
require '../app/classes/host.php';
|
require '../app/classes/host.php';
|
||||||
$agentObject = new Agent($db);
|
$agentObject = new Agent($dbWeb);
|
||||||
$hostObject = new Host($db);
|
$hostObject = new Host($dbWeb);
|
||||||
|
|
||||||
include '../app/templates/status-server.php';
|
include '../app/templates/status-server.php';
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ include '../app/templates/status-server.php';
|
||||||
foreach ($platformsAll as $platform) {
|
foreach ($platformsAll as $platform) {
|
||||||
|
|
||||||
// check if we can connect to the jilo database
|
// check if we can connect to the jilo database
|
||||||
$response = connectJiloDB($config, $platform['jilo_database'], $platform['id']);
|
$response = connectDB($config, 'jilo', $platform['jilo_database'], $platform['id']);
|
||||||
if ($response['error'] !== null) {
|
if ($response['error'] !== null) {
|
||||||
$jilo_database_status = $response['error'];
|
$jilo_database_status = $response['error'];
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Theme Asset handler
|
|
||||||
*
|
|
||||||
* Serves theme assets through the main application router.
|
|
||||||
* This provides a secure way to serve theme files that are outside the web root.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Include the theme asset handler
|
|
||||||
require_once __DIR__ . '/../helpers/theme-asset.php';
|
|
|
@ -1,71 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Theme Management Controller
|
|
||||||
*
|
|
||||||
* Handles theme switching and management functionality.
|
|
||||||
* Allows users to view available themes and change the active theme.
|
|
||||||
*
|
|
||||||
* Actions:
|
|
||||||
* - switch_to: Changes the active theme for the current user
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Initialize security
|
|
||||||
require_once '../app/helpers/security.php';
|
|
||||||
$security = SecurityHelper::getInstance();
|
|
||||||
|
|
||||||
// Only allow access to logged-in users
|
|
||||||
if (!Session::isValidSession()) {
|
|
||||||
header('Location: ' . $app_root . '?page=login');
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle theme switching
|
|
||||||
if (isset($_GET['switch_to'])) {
|
|
||||||
$themeName = $_GET['switch_to'];
|
|
||||||
|
|
||||||
// Validate CSRF token for state-changing operations
|
|
||||||
if (!$security->verifyCsrfToken($_GET['csrf_token'] ?? '')) {
|
|
||||||
Feedback::flash('SECURITY', 'CSRF_INVALID');
|
|
||||||
header("Location: $app_root?page=theme");
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (\App\Helpers\Theme::setCurrentTheme($themeName)) {
|
|
||||||
// Set success message
|
|
||||||
Feedback::flash('THEME', 'THEME_CHANGED');
|
|
||||||
} else {
|
|
||||||
// Set error message
|
|
||||||
Feedback::flash('THEME', 'THEME_CHANGE_FAILED');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect back to prevent form resubmission
|
|
||||||
$redirect = $app_root . '?page=theme';
|
|
||||||
header("Location: $redirect");
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get available themes and current theme for the view
|
|
||||||
$themes = \App\Helpers\Theme::getAvailableThemes();
|
|
||||||
$currentTheme = \App\Helpers\Theme::getCurrentThemeName();
|
|
||||||
|
|
||||||
// Prepare theme data with screenshot URLs for the view
|
|
||||||
$themeData = [];
|
|
||||||
foreach ($themes as $id => $name) {
|
|
||||||
$themeData[$id] = [
|
|
||||||
'name' => $name,
|
|
||||||
'screenshotUrl' => \App\Helpers\Theme::getAssetUrl($id, 'screenshot.png'),
|
|
||||||
'isActive' => $id === $currentTheme
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make theme data available to the view
|
|
||||||
$themes = $themeData;
|
|
||||||
|
|
||||||
// Generate CSRF token for the form
|
|
||||||
$csrf_token = $security->generateCsrfToken();
|
|
||||||
|
|
||||||
// Get any new feedback messages
|
|
||||||
include '../app/helpers/feedback.php';
|
|
||||||
|
|
||||||
// Load the template
|
|
||||||
include '../app/templates/theme.php';
|
|
|
@ -17,8 +17,7 @@
|
||||||
<i class="fas fa-wrench me-2 text-secondary"></i>
|
<i class="fas fa-wrench me-2 text-secondary"></i>
|
||||||
<?= htmlspecialchars($config['site_name']) ?> app configuration
|
<?= htmlspecialchars($config['site_name']) ?> app configuration
|
||||||
</h5>
|
</h5>
|
||||||
<?php if ($userObject->hasRight($userId, 'superuser') ||
|
<?php if ($userObject->hasRight($user_id, 'edit config file')) { ?>
|
||||||
$userObject->hasRight($userId, 'edit config file')) { ?>
|
|
||||||
<div>
|
<div>
|
||||||
<button type="button" class="btn btn-outline-primary btn-sm toggle-edit" <?= !$isWritable ? 'disabled' : '' ?>>
|
<button type="button" class="btn btn-outline-primary btn-sm toggle-edit" <?= !$isWritable ? 'disabled' : '' ?>>
|
||||||
<i class="fas fa-edit me-2"></i>Edit
|
<i class="fas fa-edit me-2"></i>Edit
|
||||||
|
@ -38,7 +37,7 @@
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
<form id="configForm">
|
<form id="configForm">
|
||||||
<?php
|
<?php
|
||||||
include CSRF_TOKEN_INCLUDE;
|
include 'csrf_token.php';
|
||||||
|
|
||||||
function renderConfigItem($key, $value, $path = '') {
|
function renderConfigItem($key, $value, $path = '') {
|
||||||
$fullPath = $path ? $path . '[' . $key . ']' : $key;
|
$fullPath = $path ? $path . '[' . $key . ']' : $key;
|
||||||
|
|
|
@ -4,15 +4,15 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text"><strong>Welcome to <?= htmlspecialchars($config['site_name']); ?>!</strong><br />Please enter login credentials:</p>
|
<p class="card-text"><strong>Welcome to <?= htmlspecialchars($config['site_name']); ?>!</strong><br />Please enter login credentials:</p>
|
||||||
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=login">
|
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=login">
|
||||||
<?php include CSRF_TOKEN_INCLUDE; ?>
|
<?php include 'csrf_token.php'; ?>
|
||||||
<div class="form-group mb-3">
|
<div class="form-group mb-3">
|
||||||
<input type="text" class="form-control w-50 mx-auto" name="username" placeholder="Username"
|
<input type="text" class="form-control w-50 mx-auto" name="username" placeholder="Username"
|
||||||
pattern="[A-Za-z0-9_\-]{3,20}" title="3-20 characters, letters, numbers, - and _"
|
pattern="[A-Za-z0-9_\-]{3,20}" title="3-20 characters, letters, numbers, - and _"
|
||||||
required />
|
required autofocus />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group mb-3">
|
<div class="form-group mb-3">
|
||||||
<input type="password" class="form-control w-50 mx-auto" name="password" placeholder="Password"
|
<input type="password" class="form-control w-50 mx-auto" name="password" placeholder="Password"
|
||||||
pattern=".{8,}" title="Eight or more characters"
|
pattern=".{5,}" title="Eight or more characters"
|
||||||
required />
|
required />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group mb-3">
|
<div class="form-group mb-3">
|
||||||
|
@ -21,9 +21,6 @@
|
||||||
remember me
|
remember me
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<?php if (isset($_GET['redirect'])): ?>
|
|
||||||
<input type="hidden" name="redirect" value="<?php echo htmlspecialchars($_GET['redirect']); ?>">
|
|
||||||
<?php endif; ?>
|
|
||||||
<input type="submit" class="btn btn-primary" value="Login" />
|
<input type="submit" class="btn btn-primary" value="Login" />
|
||||||
</form>
|
</form>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<p>Enter your email address and we will send you<br />
|
<p>Enter your email address and we will send you<br />
|
||||||
instructions to reset your password.</p>
|
instructions to reset your password.</p>
|
||||||
<form method="post" action="?page=login&action=forgot">
|
<form method="post" action="?page=login&action=forgot">
|
||||||
<?php include CSRF_TOKEN_INCLUDE; ?>
|
<?php include 'csrf_token.php'; ?>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="email">email address:</label>
|
<label for="email">email address:</label>
|
||||||
<input type="email"
|
<input type="email"
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h3 class="card-title mb-4">Set new password</h3>
|
<h3 class="card-title mb-4">Set new password</h3>
|
||||||
<form method="post" action="?page=login&action=reset&token=<?= htmlspecialchars(urlencode($token)) ?>">
|
<form method="post" action="?page=login&action=reset&token=<?= htmlspecialchars(urlencode($token)) ?>">
|
||||||
<?php include CSRF_TOKEN_INCLUDE; ?>
|
<?php include 'csrf_token.php'; ?>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="new_password">new password:</label>
|
<label for="new_password">new password:</label>
|
||||||
<input type="password"
|
<input type="password"
|
||||||
|
|
|
@ -4,11 +4,11 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text">Enter credentials for registration:</p>
|
<p class="card-text">Enter credentials for registration:</p>
|
||||||
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=register">
|
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=register">
|
||||||
<?php include CSRF_TOKEN_INCLUDE; ?>
|
<?php include 'csrf_token.php'; ?>
|
||||||
<div class="form-group mb-3">
|
<div class="form-group mb-3">
|
||||||
<input type="text" class="form-control w-50 mx-auto" name="username" placeholder="Username"
|
<input type="text" class="form-control w-50 mx-auto" name="username" placeholder="Username"
|
||||||
pattern="[A-Za-z0-9_\-]{3,20}" title="3-20 characters, letters, numbers, - and _"
|
pattern="[A-Za-z0-9_\-]{3,20}" title="3-20 characters, letters, numbers, - and _"
|
||||||
required />
|
required autofocus />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group mb-3">
|
<div class="form-group mb-3">
|
||||||
<input type="password" class="form-control w-50 mx-auto" name="password" placeholder="Password"
|
<input type="password" class="form-control w-50 mx-auto" name="password" placeholder="Password"
|
||||||
|
@ -20,17 +20,6 @@
|
||||||
pattern=".{8,}" title="Eight or more characters"
|
pattern=".{8,}" title="Eight or more characters"
|
||||||
required />
|
required />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group mb-3">
|
|
||||||
<div class="form-check">
|
|
||||||
<label class="form-check-label" for="terms">
|
|
||||||
<input type="checkbox" class="form-check-input" id="terms" name="terms" required>
|
|
||||||
I agree to the <a href="<?= htmlspecialchars($app_root) ?>?page=terms" target="_blank">terms & conditions</a> and <a href="<?= htmlspecialchars($app_root) ?>?page=privacy" target="_blank">privacy policy</a>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<small class="text-muted mt-2">
|
|
||||||
We use cookies to improve your experience. See our <a href="<?= htmlspecialchars($app_root) ?>?page=cookies" target="_blank">cookies policy</a>
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<input type="submit" class="btn btn-primary" value="Register" />
|
<input type="submit" class="btn btn-primary" value="Register" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
|
@ -0,0 +1,109 @@
|
||||||
|
|
||||||
|
<!-- log events -->
|
||||||
|
<div class="container-fluid mt-4">
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h2 class="mb-0">Log events</h2>
|
||||||
|
<small>events recorded in the Jilo monitoring platform</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabs navigation -->
|
||||||
|
<ul class="nav nav-tabs mb-4">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link <?= $scope === 'user' ? 'active' : '' ?>" href="?page=logs&tab=user">
|
||||||
|
Logs for current user
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php if ($has_system_access) { ?>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link <?= $scope === 'system' ? 'active' : '' ?>" href="?page=logs&tab=system">
|
||||||
|
Logs for all users
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php } ?>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- logs filter -->
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="GET" action="" class="row g-3 align-items-end">
|
||||||
|
<input type="hidden" name="page" value="logs">
|
||||||
|
<input type="hidden" name="tab" value="<?= htmlspecialchars($scope) ?>">
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="from_time" class="form-label">From date</label>
|
||||||
|
<input type="date" class="form-control" id="from_time" name="from_time" value="<?= htmlspecialchars($_REQUEST['from_time'] ?? '') ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="until_time" class="form-label">Until date</label>
|
||||||
|
<input type="date" class="form-control" id="until_time" name="until_time" value="<?= htmlspecialchars($_REQUEST['until_time'] ?? '') ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($scope === 'system') { ?>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label for="id" class="form-label">User ID</label>
|
||||||
|
<input type="text" class="form-control" id="id" name="id" value="<?= htmlspecialchars($_REQUEST['id'] ?? '') ?>" placeholder="Enter user ID">
|
||||||
|
</div>
|
||||||
|
<?php } ?>
|
||||||
|
|
||||||
|
<div class="col-md">
|
||||||
|
<label for="message" class="form-label">Message</label>
|
||||||
|
<input type="text" class="form-control" id="message" name="message" value="<?= htmlspecialchars($_REQUEST['message'] ?? '') ?>" placeholder="Search in log messages">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-auto">
|
||||||
|
<button type="submit" class="btn btn-primary me-2">
|
||||||
|
<i class="fas fa-search me-2"></i>Search
|
||||||
|
</button>
|
||||||
|
<a href="?page=logs&tab=<?= htmlspecialchars($scope) ?>" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-times me-2"></i>Clear
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- /logs filter -->
|
||||||
|
|
||||||
|
<!-- logs -->
|
||||||
|
<?php if ($time_range_specified) { ?>
|
||||||
|
<div class="alert alert-info m-3">
|
||||||
|
<i class="fas fa-calendar-alt me-2"></i>Time period: <strong><?= htmlspecialchars($from_time) ?> - <?= htmlspecialchars($until_time) ?></strong>
|
||||||
|
</div>
|
||||||
|
<?php } ?>
|
||||||
|
<div class="mb-5">
|
||||||
|
<?php if (!empty($logs['records'])) { ?>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<?php if ($scope === 'system') { ?>
|
||||||
|
<th>Username (id)</th>
|
||||||
|
<?php } ?>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>Log message</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($logs['records'] as $row) { ?>
|
||||||
|
<tr>
|
||||||
|
<?php if ($scope === 'system') { ?>
|
||||||
|
<td><strong><?= htmlspecialchars($row['username']) ?> (<?= htmlspecialchars($row['userID']) ?>)</strong></td>
|
||||||
|
<?php } ?>
|
||||||
|
<td><span class="text-muted"><?= date('d M Y H:i', strtotime($row['time'])) ?></span></td>
|
||||||
|
<td><?= htmlspecialchars($row['log message']) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php } ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php include '../app/templates/pagination.php'; ?>
|
||||||
|
<?php } else { ?>
|
||||||
|
<div class="alert alert-info m-3">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>No log entries found for the specified criteria.
|
||||||
|
</div>
|
||||||
|
<?php } ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- /log events -->
|
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if (Session::getUsername() && $page !== 'logout') { ?>
|
<?php if (isset($currentUser) && $page !== 'logout') { ?>
|
||||||
<script src="static/js/sidebar.js"></script>
|
<script src="static/js/sidebar.js"></script>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<link rel="stylesheet" type="text/css" href="<?= htmlspecialchars($app_root) ?>static/css/main.css">
|
<link rel="stylesheet" type="text/css" href="<?= htmlspecialchars($app_root) ?>static/css/main.css">
|
||||||
<link rel="stylesheet" type="text/css" href="<?= htmlspecialchars($app_root) ?>static/css/messages.css">
|
<link rel="stylesheet" type="text/css" href="<?= htmlspecialchars($app_root) ?>static/css/messages.css">
|
||||||
<script src="<?= htmlspecialchars($app_root) ?>static/js/messages.js"></script>
|
<script src="<?= htmlspecialchars($app_root) ?>static/js/messages.js"></script>
|
||||||
<?php if (Session::getUsername()) { ?>
|
<?php if (isset($currentUser)) { ?>
|
||||||
<script>
|
<script>
|
||||||
// restore sidebar state before the page is rendered
|
// restore sidebar state before the page is rendered
|
||||||
(function () {
|
(function () {
|
||||||
|
|
|
@ -15,10 +15,10 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<li class="font-weight-light text-uppercase" style="font-size: 0.5em; color: whitesmoke; margin-right: 70px; align-content: center;">
|
<li class="font-weight-light text-uppercase" style="font-size: 0.5em; color: whitesmoke; margin-right: 70px; align-content: center;">
|
||||||
version <?= htmlspecialchars($config['version'] ?? '1.0.0') ?>
|
version <?= htmlspecialchars($config['version']) ?>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<?php if (Session::isValidSession()) { ?>
|
<?php if (isset($_SESSION['username']) && isset($_SESSION['user_id'])) { ?>
|
||||||
|
|
||||||
<?php foreach ($platformsAll as $platform) {
|
<?php foreach ($platformsAll as $platform) {
|
||||||
$platform_switch_url = switchPlatform($platform['id']);
|
$platform_switch_url = switchPlatform($platform['id']);
|
||||||
|
@ -40,17 +40,13 @@
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<ul class="menu-right">
|
<ul class="menu-right">
|
||||||
<?php if (Session::isValidSession()) { ?>
|
<?php if (isset($_SESSION['username']) && isset($_SESSION['user_id'])) { ?>
|
||||||
<li class="dropdown">
|
<li class="dropdown">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
|
||||||
<i class="fas fa-user"></i>
|
<i class="fas fa-user"></i>
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu dropdown-menu-right">
|
<div class="dropdown-menu dropdown-menu-right">
|
||||||
<h6 class="dropdown-header"><?= htmlspecialchars($currentUser) ?></h6>
|
<h6 class="dropdown-header"><?= htmlspecialchars($currentUser) ?></h6>
|
||||||
<a class="dropdown-item" href="<?= htmlspecialchars($app_root) ?>?page=theme">
|
|
||||||
<i class="fas fa-paint-brush"></i>Change theme
|
|
||||||
</a>
|
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
<a class="dropdown-item" href="<?= htmlspecialchars($app_root) ?>?page=profile">
|
<a class="dropdown-item" href="<?= htmlspecialchars($app_root) ?>?page=profile">
|
||||||
<i class="fas fa-id-card"></i>Profile details
|
<i class="fas fa-id-card"></i>Profile details
|
||||||
</a>
|
</a>
|
||||||
|
@ -63,49 +59,10 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown">
|
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
|
|
||||||
<i class="fas fa-cog"></i>
|
|
||||||
</a>
|
|
||||||
<div class="dropdown-menu dropdown-menu-right">
|
|
||||||
<h6 class="dropdown-header">system</h6>
|
|
||||||
<?php if ($userObject->hasRight($userId, 'superuser') ||
|
|
||||||
$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, 'view config file') ||
|
|
||||||
$userObject->hasRight($userId, 'edit config file') ||
|
|
||||||
$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')) {?>
|
|
||||||
<?php do_hook('main_menu', ['app_root' => $app_root, 'section' => 'main', 'position' => 100]); ?>
|
|
||||||
<?php } ?>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<?php } else { ?>
|
<?php } else { ?>
|
||||||
<li><a href="<?= htmlspecialchars($app_root) ?>?page=login">login</a></li>
|
<li><a href="<?= htmlspecialchars($app_root) ?>?page=login">login</a></li>
|
||||||
<?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 } ?>
|
<?php } ?>
|
||||||
<li class="dropdown">
|
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
|
|
||||||
<i class="fas fa-info-circle"></i>
|
|
||||||
</a>
|
|
||||||
<div class="dropdown-menu dropdown-menu-right">
|
|
||||||
<h6 class="dropdown-header">resources</h6>
|
|
||||||
<a class="dropdown-item" href="<?= htmlspecialchars($app_root) ?>?page=help">
|
|
||||||
<i class="fas fa-question-circle"></i>Help
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<!-- /Menu -->
|
<!-- /Menu -->
|
||||||
|
|
|
@ -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
|
<i class="fas fa-cog" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="jilo settings"></i>settings
|
||||||
</li>
|
</li>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<li class="list-group-item bg-light" style="border: none;"><p class="text-end mb-0"><small>system</small></p></li>
|
||||||
|
|
||||||
|
<?php if ($userObject->hasRight($user_id, 'view config file')) {?>
|
||||||
|
<a href="<?= htmlspecialchars($app_root) ?>?page=config">
|
||||||
|
<li class="list-group-item<?php if ($page === 'config') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
|
||||||
|
<i class="fas fa-wrench" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="app config"></i>config
|
||||||
|
</li>
|
||||||
|
</a>
|
||||||
|
<?php } ?>
|
||||||
|
<?php if ($userObject->hasRight($user_id, 'superuser') ||
|
||||||
|
$userObject->hasRight($user_id, 'edit whitelist') ||
|
||||||
|
$userObject->hasRight($user_id, 'edit blacklist') ||
|
||||||
|
$userObject->hasRight($user_id, 'edit ratelimiting')) { ?>
|
||||||
|
<a href="<?= htmlspecialchars($app_root) ?>?page=security">
|
||||||
|
<li class="list-group-item<?php if ($page === 'security') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
|
||||||
|
<i class="fas fa-shield-alt" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="security"></i>security
|
||||||
|
</li>
|
||||||
|
</a>
|
||||||
|
<?php } ?>
|
||||||
<a href="<?= htmlspecialchars($app_root) ?>?page=status">
|
<a href="<?= htmlspecialchars($app_root) ?>?page=status">
|
||||||
<li class="list-group-item<?php if ($page === 'status' && $item === '') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
|
<li class="list-group-item<?php if ($page === 'status' && $item === '') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
|
||||||
<i class="fas fa-heartbeat" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="status"></i>status
|
<i class="fas fa-heartbeat" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="status"></i>status
|
||||||
</li>
|
</li>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<?php if ($userObject->hasRight($user_id, 'view app logs')) {?>
|
||||||
|
<a href="<?= htmlspecialchars($app_root) ?>?page=logs">
|
||||||
|
<li class="list-group-item<?php if ($page === 'logs') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
|
||||||
|
<i class="fas fa-shoe-prints" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="logs"></i>logs
|
||||||
|
</li>
|
||||||
|
</a>
|
||||||
|
<?php } ?>
|
||||||
|
<a href="<?= htmlspecialchars($app_root) ?>?page=help">
|
||||||
|
<li class="list-group-item<?php if ($page === 'help') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
|
||||||
|
<i class="fas fa-question-circle" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="help"></i>help
|
||||||
|
</li>
|
||||||
|
</a>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,14 +6,11 @@
|
||||||
* $totalPages - Total number of pages
|
* $totalPages - Total number of pages
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Validate required pagination variables
|
// Ensure required variables are set
|
||||||
if (!isset($currentPage) || !isset($totalPages)) {
|
if (!isset($currentPage) || !isset($totalPages)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure valid values
|
|
||||||
$currentPage = max(1, min($currentPage, $totalPages));
|
|
||||||
|
|
||||||
// Number of page links to show before and after current page
|
// Number of page links to show before and after current page
|
||||||
$range = 2;
|
$range = 2;
|
||||||
?>
|
?>
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=profile" enctype="multipart/form-data">
|
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=profile" enctype="multipart/form-data">
|
||||||
<?php include CSRF_TOKEN_INCLUDE; ?>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p class="border rounded bg-light mb-4"><small>edit the profile fields</small></p>
|
<p class="border rounded bg-light mb-4"><small>edit the profile fields</small></p>
|
||||||
<div class="col-md-4 avatar-container">
|
<div class="col-md-4 avatar-container">
|
||||||
|
@ -133,7 +132,6 @@
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
|
||||||
<form id="remove-avatar-form" data-action="remove-avatar" method="POST" action="<?= htmlspecialchars($app_root) ?>?page=profile&action=remove&item=avatar">
|
<form id="remove-avatar-form" data-action="remove-avatar" method="POST" action="<?= htmlspecialchars($app_root) ?>?page=profile&action=remove&item=avatar">
|
||||||
<?php include CSRF_TOKEN_INCLUDE; ?>
|
|
||||||
<button type="button" class="btn btn-danger" id="confirm-delete">Delete Avatar</button>
|
<button type="button" class="btn btn-danger" id="confirm-delete">Delete Avatar</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,17 +5,17 @@
|
||||||
<h2 class="mb-0">Security settings</h2>
|
<h2 class="mb-0">Security settings</h2>
|
||||||
<small>network restrictions to control flooding and brute force attacks</small>
|
<small>network restrictions to control flooding and brute force attacks</small>
|
||||||
<ul class="nav nav-tabs mt-5">
|
<ul class="nav nav-tabs mt-5">
|
||||||
<?php if ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, 'edit whitelist')) { ?>
|
<?php if ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit whitelist')) { ?>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link <?= $section === 'whitelist' ? 'active' : '' ?>" href="?page=security§ion=whitelist">IP whitelist</a>
|
<a class="nav-link <?= $section === 'whitelist' ? 'active' : '' ?>" href="?page=security§ion=whitelist">IP whitelist</a>
|
||||||
</li>
|
</li>
|
||||||
<?php } ?>
|
<?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">
|
<li class="nav-item">
|
||||||
<a class="nav-link <?= $section === 'blacklist' ? 'active' : '' ?>" href="?page=security§ion=blacklist">IP blacklist</a>
|
<a class="nav-link <?= $section === 'blacklist' ? 'active' : '' ?>" href="?page=security§ion=blacklist">IP blacklist</a>
|
||||||
</li>
|
</li>
|
||||||
<?php } ?>
|
<?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">
|
<li class="nav-item">
|
||||||
<a class="nav-link <?= $section === 'ratelimit' ? 'active' : '' ?>" href="?page=security§ion=ratelimit">Rate limiting</a>
|
<a class="nav-link <?= $section === 'ratelimit' ? 'active' : '' ?>" href="?page=security§ion=ratelimit">Rate limiting</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- whitelist section -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
@ -35,7 +35,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST" class="mb-4">
|
<form method="POST" class="mb-4">
|
||||||
<?php include CSRF_TOKEN_INCLUDE; ?>
|
<?php include 'csrf_token.php'; ?>
|
||||||
<input type="hidden" name="action" value="add_whitelist">
|
<input type="hidden" name="action" value="add_whitelist">
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
|
@ -77,7 +77,7 @@
|
||||||
<td><?= htmlspecialchars($ip['created_at']) ?></td>
|
<td><?= htmlspecialchars($ip['created_at']) ?></td>
|
||||||
<td>
|
<td>
|
||||||
<form method="POST" style="display: inline;">
|
<form method="POST" style="display: inline;">
|
||||||
<?php include CSRF_TOKEN_INCLUDE; ?>
|
<?php include 'csrf_token.php'; ?>
|
||||||
<input type="hidden" name="action" value="remove_whitelist">
|
<input type="hidden" name="action" value="remove_whitelist">
|
||||||
<input type="hidden" name="ip_address" value="<?= htmlspecialchars($ip['ip_address']) ?>">
|
<input type="hidden" name="ip_address" value="<?= htmlspecialchars($ip['ip_address']) ?>">
|
||||||
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to remove this IP from whitelist?')">Remove</button>
|
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to remove this IP from whitelist?')">Remove</button>
|
||||||
|
@ -93,7 +93,7 @@
|
||||||
</div>
|
</div>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
|
|
||||||
<?php if ($section === 'blacklist' && ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, 'edit blacklist'))) { ?>
|
<?php if ($section === 'blacklist' && ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit blacklist'))) { ?>
|
||||||
<!-- blacklist section -->
|
<!-- blacklist section -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
@ -104,7 +104,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST" class="mb-4">
|
<form method="POST" class="mb-4">
|
||||||
<?php include CSRF_TOKEN_INCLUDE; ?>
|
<?php include 'csrf_token.php'; ?>
|
||||||
<input type="hidden" name="action" value="add_blacklist">
|
<input type="hidden" name="action" value="add_blacklist">
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
|
@ -151,7 +151,7 @@
|
||||||
<td><?= $ip['expiry_time'] ? htmlspecialchars($ip['expiry_time']) : 'Never' ?></td>
|
<td><?= $ip['expiry_time'] ? htmlspecialchars($ip['expiry_time']) : 'Never' ?></td>
|
||||||
<td>
|
<td>
|
||||||
<form method="POST" style="display: inline;">
|
<form method="POST" style="display: inline;">
|
||||||
<?php include CSRF_TOKEN_INCLUDE; ?>
|
<?php include 'csrf_token.php'; ?>
|
||||||
<input type="hidden" name="action" value="remove_blacklist">
|
<input type="hidden" name="action" value="remove_blacklist">
|
||||||
<input type="hidden" name="ip_address" value="<?= htmlspecialchars($ip['ip_address']) ?>">
|
<input type="hidden" name="ip_address" value="<?= htmlspecialchars($ip['ip_address']) ?>">
|
||||||
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to remove this IP from blacklist?')">Remove</button>
|
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to remove this IP from blacklist?')">Remove</button>
|
||||||
|
@ -167,7 +167,7 @@
|
||||||
</div>
|
</div>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
|
|
||||||
<?php if ($section === 'ratelimit' && ($userObject->hasRight($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 -->
|
<!-- rate limiting section -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
<button type="button" class="btn btn-outline-secondary cancel-edit platform-edit-mode" style="display: none;">
|
<button type="button" class="btn btn-outline-secondary cancel-edit platform-edit-mode" style="display: none;">
|
||||||
<i class="fas fa-times me-1"></i>Cancel
|
<i class="fas fa-times me-1"></i>Cancel
|
||||||
</button>
|
</button>
|
||||||
<?php if ($userObject->hasRight($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'])) ?>')">
|
<button type="button" class="btn btn-outline-danger platform-view-mode" onclick="showDeletePlatformModal(<?= htmlspecialchars($platform['id']) ?>, '<?= htmlspecialchars(addslashes($platform['name'])) ?>', '<?= htmlspecialchars(addslashes($platform['jitsi_url'])) ?>', '<?= htmlspecialchars(addslashes($platform['jilo_database'])) ?>')">
|
||||||
<i class="fas fa-trash me-1"></i>Delete platform
|
<i class="fas fa-trash me-1"></i>Delete platform
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Theme switcher template
|
|
||||||
*
|
|
||||||
* Displays available themes and allows the user to switch between them.
|
|
||||||
*
|
|
||||||
* @var array $themes List of available themes with their data
|
|
||||||
* - name: Display name
|
|
||||||
* - screenshotUrl: URL to the screenshot (or null if not available)
|
|
||||||
* - isActive: Whether this is the current theme
|
|
||||||
*/
|
|
||||||
?>
|
|
||||||
<div class="container mt-4">
|
|
||||||
<h2>Theme switcher</h2>
|
|
||||||
<p class="text-muted">Select a theme to change the appearance of the application.</p>
|
|
||||||
<div class="row mt-4">
|
|
||||||
<?php foreach ($themes as $themeId => $theme): ?>
|
|
||||||
<div class="col-md-4 mb-4">
|
|
||||||
<div class="card h-100 <?= $theme['isActive'] ? 'border-primary' : '' ?>">
|
|
||||||
<!-- Theme screenshot -->
|
|
||||||
<div class="theme-screenshot" style="height: 150px; background-size: cover; background-position: center; background-color: #f8f9fa; <?= $theme['screenshotUrl'] ? 'background-image: url(' . htmlspecialchars($theme['screenshotUrl']) . ')' : '' ?>">
|
|
||||||
<?php if (!$theme['screenshotUrl']): ?>
|
|
||||||
<div class="h-100 d-flex align-items-center justify-content-center text-muted">No preview available</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<?php if ($theme['isActive']): ?>
|
|
||||||
<div class="card-header bg-primary text-white">Current theme</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<div class="card-body d-flex flex-column">
|
|
||||||
<h5 class="card-title"><?= htmlspecialchars($theme['name']) ?></h5>
|
|
||||||
<p class="card-text text-muted">Theme ID: <code><?= htmlspecialchars($themeId) ?></code></p>
|
|
||||||
<div class="mt-auto">
|
|
||||||
<?php if (!$theme['isActive']): ?>
|
|
||||||
<a href="?page=theme&switch_to=<?= urlencode($themeId) ?>&csrf_token=<?= $csrf_token ?>" class="btn btn-primary">Switch to this theme</a>
|
|
||||||
<?php else: ?>
|
|
||||||
<button class="btn btn-outline-secondary" disabled>Currently active</button>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,243 +0,0 @@
|
||||||
-- Time: 13.03.2025, 15:52
|
|
||||||
-- Server: 11.4.5-MariaDB-1
|
|
||||||
|
|
||||||
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
|
||||||
START TRANSACTION;
|
|
||||||
SET time_zone = "+00:00";
|
|
||||||
SET NAMES utf8mb4;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- User profiles
|
|
||||||
--
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `user` (
|
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
`username` varchar(50) NOT NULL,
|
|
||||||
`password` varchar(100) NOT NULL,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
UNIQUE KEY `username` (`username`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
|
||||||
|
|
||||||
INSERT INTO `user` (`id`, `username`, `password`) VALUES
|
|
||||||
(1,'demo','$2y$12$AtIKs3eVxD4wTT1IWwJujuuHyGhhmfBJYqSfIrPFFPMDfKu3Rcsx6'),
|
|
||||||
(2,'demo1','$2y$12$ELwYyhQ8XDkVvX9Xsb0mlORqeQHNFaBOvaBuPQym4n4IomA/DgvLC');
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `user_meta` (
|
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
`user_id` int(11) NOT NULL,
|
|
||||||
`name` varchar(255) DEFAULT NULL,
|
|
||||||
`email` varchar(256) DEFAULT NULL,
|
|
||||||
`timezone` varchar(255) DEFAULT NULL,
|
|
||||||
`avatar` varchar(255) DEFAULT NULL,
|
|
||||||
`bio` text DEFAULT NULL,
|
|
||||||
PRIMARY KEY (`id`,`user_id`) USING BTREE,
|
|
||||||
KEY `user_id` (`user_id`),
|
|
||||||
CONSTRAINT `user_meta_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
|
||||||
|
|
||||||
INSERT INTO `user_meta` (`id`, `user_id`, `name`, `email`, `timezone`, `avatar`, `bio`) VALUES
|
|
||||||
(1,1,'demo admin user','admin@example.com',NULL,NULL,'This is a demo user of the demo install of Jilo Web'),
|
|
||||||
(2,2,'demo user','demo@example.com',NULL,NULL,'This is a demo user of the demo install of Jilo Web');
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `right` (
|
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
`name` varchar(255) NOT NULL,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
UNIQUE KEY `name` (`name`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
|
||||||
|
|
||||||
INSERT INTO `right` (`id`, `name`) VALUES
|
|
||||||
(1, 'superuser'),
|
|
||||||
(2, 'edit users'),
|
|
||||||
(3, 'view config file'),
|
|
||||||
(4, 'edit config file'),
|
|
||||||
(5, 'view own profile'),
|
|
||||||
(6, 'edit own profile'),
|
|
||||||
(7, 'view all profiles'),
|
|
||||||
(8, 'edit all profiles'),
|
|
||||||
(9, 'view app logs'),
|
|
||||||
(10, 'manage plugins'),
|
|
||||||
(11,'view all platforms'),
|
|
||||||
(12,'edit all platforms'),
|
|
||||||
(13,'view all agents'),
|
|
||||||
(14,'edit all agents'),
|
|
||||||
(15,'view jilo config');
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `user_right` (
|
|
||||||
`user_id` int(11) NOT NULL,
|
|
||||||
`right_id` int(11) NOT NULL,
|
|
||||||
PRIMARY KEY (`user_id`,`right_id`),
|
|
||||||
KEY `fk_right_id` (`right_id`),
|
|
||||||
CONSTRAINT `fk_right_id` FOREIGN KEY (`right_id`) REFERENCES `right` (`id`),
|
|
||||||
CONSTRAINT `fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `user_2fa` (
|
|
||||||
`user_id` int(11) NOT NULL,
|
|
||||||
`secret_key` varchar(64) NOT NULL,
|
|
||||||
`backup_codes` text,
|
|
||||||
`enabled` tinyint(1) NOT NULL DEFAULT 0,
|
|
||||||
`created_at` datetime NOT NULL,
|
|
||||||
`last_used` datetime DEFAULT NULL,
|
|
||||||
PRIMARY KEY (`user_id`),
|
|
||||||
CONSTRAINT `fk_user_2fa_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `user_2fa_temp` (
|
|
||||||
`user_id` int(11) NOT NULL,
|
|
||||||
`code` varchar(6) NOT NULL,
|
|
||||||
`created_at` datetime NOT NULL,
|
|
||||||
`expires_at` datetime NOT NULL,
|
|
||||||
PRIMARY KEY (`user_id`, `code`),
|
|
||||||
CONSTRAINT `fk_user_2fa_temp_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `user_password_reset` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`user_id` int(11) NOT NULL,
|
|
||||||
`token` varchar(64) NOT NULL,
|
|
||||||
`expires` int(11) NOT NULL,
|
|
||||||
`used` TINYINT(1) NOT NULL DEFAULT 0,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT `fk_user_password_reset` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`),
|
|
||||||
UNIQUE KEY `token_idx` (`token`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Login security
|
|
||||||
--
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `security_rate_auth` (
|
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
`ip_address` varchar(45) NOT NULL,
|
|
||||||
`username` varchar(255) NOT NULL,
|
|
||||||
`attempted_at` datetime DEFAULT current_timestamp(),
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `idx_ip_username` (`ip_address`,`username`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `security_rate_page` (
|
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
`ip_address` varchar(45) NOT NULL,
|
|
||||||
`endpoint` varchar(255) NOT NULL,
|
|
||||||
`request_time` datetime DEFAULT current_timestamp(),
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `idx_ip_endpoint` (`ip_address`,`endpoint`),
|
|
||||||
KEY `idx_request_time` (`request_time`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `security_ip_blacklist` (
|
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
`ip_address` varchar(45) NOT NULL,
|
|
||||||
`is_network` tinyint(1) DEFAULT 0,
|
|
||||||
`reason` varchar(255) DEFAULT NULL,
|
|
||||||
`expiry_time` timestamp NULL DEFAULT NULL,
|
|
||||||
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
|
||||||
`created_by` varchar(255) DEFAULT NULL,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
UNIQUE KEY `unique_ip` (`ip_address`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
|
||||||
|
|
||||||
INSERT INTO `security_ip_blacklist` (`id`, `ip_address`, `is_network`, `reason`, `expiry_time`, `created_at`, `created_by`) VALUES
|
|
||||||
(1, '0.0.0.0/8', 1, 'Reserved address space - RFC 1122', NULL, '2025-01-03 16:40:15', 'system'),
|
|
||||||
(2, '100.64.0.0/10', 1, 'Carrier-grade NAT space - RFC 6598', NULL, '2025-01-03 16:40:15', 'system'),
|
|
||||||
(3, '192.0.2.0/24', 1, 'TEST-NET-1 Documentation space - RFC 5737', NULL, '2025-01-03 16:40:15', 'system'),
|
|
||||||
(4, '198.51.100.0/24', 1, 'TEST-NET-2 Documentation space - RFC 5737', NULL, '2025-01-03 16:40:15', 'system'),
|
|
||||||
(5, '203.0.113.0/24', 1, 'TEST-NET-3 Documentation space - RFC 5737', NULL, '2025-01-03 16:40:15', 'system');
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `security_ip_whitelist` (
|
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
`ip_address` varchar(45) NOT NULL,
|
|
||||||
`is_network` tinyint(1) DEFAULT 0,
|
|
||||||
`description` varchar(255) DEFAULT NULL,
|
|
||||||
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
|
||||||
`created_by` varchar(255) DEFAULT NULL,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
UNIQUE KEY `unique_ip` (`ip_address`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
|
||||||
|
|
||||||
INSERT INTO `security_ip_whitelist` (`id`, `ip_address`, `is_network`, `description`, `created_at`, `created_by`) VALUES
|
|
||||||
(1, '127.0.0.1', 0, 'localhost IPv4', '2025-01-03 16:40:15', 'system'),
|
|
||||||
(2, '::1', 0, 'localhost IPv6', '2025-01-03 16:40:15', 'system'),
|
|
||||||
(3, '10.0.0.0/8', 1, 'Private network (Class A)', '2025-01-03 16:40:15', 'system'),
|
|
||||||
(4, '172.16.0.0/12', 1, 'Private network (Class B)', '2025-01-03 16:40:15', 'system'),
|
|
||||||
(5, '192.168.0.0/16', 1, 'Private network (Class C)', '2025-01-03 16:40:15', 'system');
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Jilo
|
|
||||||
--
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `jilo_agent_type` (
|
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
`description` varchar(255),
|
|
||||||
`endpoint` varchar(255),
|
|
||||||
PRIMARY KEY (`id`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
|
||||||
INSERT INTO `jilo_agent_type` (`id`, `description`, `endpoint`) VALUES
|
|
||||||
(1,'jvb','/jvb'),
|
|
||||||
(2,'jicofo','/jicofo'),
|
|
||||||
(3,'prosody','/prosody'),
|
|
||||||
(4,'nginx','/nginx'),
|
|
||||||
(5,'jibri','/jibri');
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `platform` (
|
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
`name` varchar(255) NOT NULL,
|
|
||||||
`jitsi_url` varchar(255) NOT NULL,
|
|
||||||
`jilo_database` varchar(255) NOT NULL,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
UNIQUE KEY `name` (`name`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
|
||||||
INSERT INTO `platforms` (`id`, `name`, `jitsi_url`, `jilo_database`) VALUES
|
|
||||||
(1,'example.com','https://meet.example.com','../../jilo/jilo.db');
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `host` (
|
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
`address` varchar(255) NOT NULL,
|
|
||||||
`platform_id` int(11) NOT NULL,
|
|
||||||
`name` varchar(255),
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
CONSTRAINT `host_ibfk_1` FOREIGN KEY (`platform_id`) REFERENCES `platform` (`id`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `jilo_agent` (
|
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
`host_id` int(11) NOT NULL,
|
|
||||||
`agent_type_id` int(11) NOT NULL,
|
|
||||||
`url` varchar(255) NOT NULL,
|
|
||||||
`secret_key` varchar(255),
|
|
||||||
`check_period` int(11) DEFAULT 0,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
CONSTRAINT `jilo_agent_ibfk_1` FOREIGN KEY (`agent_type_id`) REFERENCES `jilo_agent_type` (`id`),
|
|
||||||
CONSTRAINT `jilo_agent_ibfk_2` FOREIGN KEY (`host_id`) REFERENCES `host` (`id`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `jilo_agent_check` (
|
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
`agent_id` int(11),
|
|
||||||
`timestamp` datetime DEFAULT current_timestamp(),
|
|
||||||
`status_code` int(11),
|
|
||||||
`response_time_ms` int(11),
|
|
||||||
`response_content` varchar(255),
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
CONSTRAINT `jilo_agent_check_ibfk_1` FOREIGN KEY (`agent_id`) REFERENCES `jilo_agent` (`id`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
|
||||||
|
|
||||||
|
|
||||||
COMMIT;
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
|
||||||
CREATE TABLE users (
|
CREATE TABLE users (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
username TEXT NOT NULL UNIQUE,
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
@ -24,95 +25,43 @@ CREATE TABLE rights (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT NOT NULL UNIQUE
|
name TEXT NOT NULL UNIQUE
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS "jilo_agent_types" (
|
INSERT INTO rights VALUES(1,'superuser');
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
INSERT INTO rights VALUES(2,'edit users');
|
||||||
description TEXT,
|
INSERT INTO rights VALUES(3,'view settings');
|
||||||
endpoint TEXT
|
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 (
|
CREATE TABLE platforms (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT NOT NULL UNIQUE,
|
name TEXT NOT NULL UNIQUE,
|
||||||
jitsi_url TEXT NOT NULL,
|
jitsi_url TEXT NOT NULL,
|
||||||
jilo_database TEXT NOT NULL
|
jilo_database TEXT NOT NULL
|
||||||
);
|
);
|
||||||
CREATE TABLE hosts (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
address TEXT NOT NULL,
|
|
||||||
platform_id INTEGER NOT NULL,
|
|
||||||
name TEXT,
|
|
||||||
FOREIGN KEY(platform_id) REFERENCES platforms(id)
|
|
||||||
);
|
|
||||||
CREATE TABLE jilo_agents (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
host_id INTEGER NOT NULL,
|
|
||||||
agent_type_id INTEGER NOT NULL,
|
|
||||||
url TEXT NOT NULL,
|
|
||||||
secret_key TEXT,
|
|
||||||
check_period INTEGER DEFAULT 0,
|
|
||||||
FOREIGN KEY(agent_type_id) REFERENCES jilo_agent_types(id),
|
|
||||||
FOREIGN KEY(host_id) REFERENCES hosts(id)
|
|
||||||
);
|
|
||||||
CREATE TABLE jilo_agent_checks (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
agent_id INTEGER,
|
|
||||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
status_code INTEGER,
|
|
||||||
response_time_ms INTEGER,
|
|
||||||
response_content TEXT,
|
|
||||||
FOREIGN KEY(agent_id) REFERENCES jilo_agents(id)
|
|
||||||
);
|
|
||||||
CREATE TABLE ip_whitelist (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
ip_address TEXT NOT NULL UNIQUE,
|
|
||||||
is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)),
|
|
||||||
description TEXT,
|
|
||||||
created_at TEXT DEFAULT (DATETIME('now')),
|
|
||||||
created_by TEXT
|
|
||||||
);
|
|
||||||
CREATE TABLE ip_blacklist (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
ip_address TEXT NOT NULL UNIQUE,
|
|
||||||
is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)),
|
|
||||||
reason TEXT,
|
|
||||||
expiry_time TEXT NULL,
|
|
||||||
created_at TEXT DEFAULT (DATETIME('now')),
|
|
||||||
created_by TEXT
|
|
||||||
);
|
|
||||||
CREATE TABLE logs (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
time TEXT DEFAULT (DATETIME('now')),
|
|
||||||
scope TEXT NOT NULL,
|
|
||||||
message TEXT NOT NULL,
|
|
||||||
FOREIGN KEY(user_id) REFERENCES users(id)
|
|
||||||
);
|
|
||||||
CREATE TABLE pages_rate_limits (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
ip_address TEXT NOT NULL,
|
|
||||||
endpoint TEXT NOT NULL,
|
|
||||||
request_time DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
CREATE TABLE login_attempts (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
ip_address TEXT NOT NULL,
|
|
||||||
username TEXT NOT NULL,
|
|
||||||
attempted_at TEXT DEFAULT (DATETIME('now'))
|
|
||||||
);
|
|
||||||
CREATE TABLE user_2fa (
|
CREATE TABLE user_2fa (
|
||||||
user_id INTEGER NOT NULL PRIMARY KEY,
|
user_id INTEGER NOT NULL PRIMARY KEY,
|
||||||
secret_key TEXT NOT NULL,
|
secret_key TEXT NOT NULL,
|
||||||
backup_codes TEXT,
|
backup_codes TEXT,
|
||||||
enabled INTEGER NOT NULL DEFAULT 0,
|
enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
last_used TEXT,
|
last_used TEXT,
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
);
|
);
|
||||||
CREATE TABLE user_2fa_temp (
|
CREATE TABLE user_2fa_temp (
|
||||||
user_id INTEGER NOT NULL PRIMARY KEY,
|
user_id INTEGER NOT NULL,
|
||||||
code TEXT NOT NULL,
|
code TEXT NOT NULL,
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
expires_at TEXT NOT NULL,
|
expires_at TEXT NOT NULL,
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
PRIMARY KEY (user_id, code),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
);
|
);
|
||||||
CREATE TABLE user_password_reset (
|
CREATE TABLE user_password_reset (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
@ -121,5 +70,85 @@ CREATE TABLE user_password_reset (
|
||||||
expires INTEGER NOT NULL,
|
expires INTEGER NOT NULL,
|
||||||
used INTEGER NOT NULL DEFAULT 0,
|
used INTEGER NOT NULL DEFAULT 0,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (user_id) REFERENCES 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');
|
||||||
|
|
|
@ -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(1,1,'demo admin user','admin@example.com',NULL,NULL,'This is a demo user of the demo install of Jilo Web');
|
||||||
INSERT INTO users_meta VALUES(2,2,'demo user','demo@example.com',NULL,NULL,'This is a demo user of the demo install of Jilo Web');
|
INSERT INTO users_meta VALUES(2,2,'demo user','demo@example.com',NULL,NULL,'This is a demo user of the demo install of Jilo Web');
|
||||||
|
|
||||||
INSERT INTO rights VALUES(1,'superuser');
|
INSERT INTO platforms VALUES(1,'meet.lindeas.com','https://meet.lindeas.com','../jilo-meet.lindeas.db');
|
||||||
INSERT INTO rights VALUES(2,'edit users');
|
INSERT INTO platforms VALUES(2,'example.com','https://meet.example.com','../jilo.db');
|
||||||
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 jilo_agent_types VALUES(1,'jvb','/jvb');
|
INSERT INTO logs VALUES(1,2,'2024-09-30 09:54:50','user','Logout: User "demo" logged out. IP: 151.237.101.43');
|
||||||
INSERT INTO jilo_agent_types VALUES(2,'jicofo','/jicofo');
|
INSERT INTO logs VALUES(2,2,'2024-09-30 09:54:54','user','Login: User "demo" logged in. IP: 151.237.101.43');
|
||||||
INSERT INTO jilo_agent_types VALUES(3,'prosody','/prosody');
|
INSERT INTO logs VALUES(3,2,'2024-10-03 16:34:49','user','Logout: User "demo" logged out. IP: 151.237.101.43');
|
||||||
INSERT INTO jilo_agent_types VALUES(4,'nginx','/nginx');
|
INSERT INTO logs VALUES(4,2,'2024-10-03 16:34:56','user','Login: User "demo" logged in. IP: 151.237.101.43');
|
||||||
INSERT INTO jilo_agent_types VALUES(5,'jibri','/jibri');
|
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 hosts VALUES(1,'meet.lindeas.com',2,'main machine');
|
||||||
INSERT INTO ip_whitelist VALUES(2,'::1',0,'localhost IPv6','2025-01-04 11:39:08','system');
|
INSERT INTO hosts VALUES(2,'meet.example.com',2,'test');
|
||||||
INSERT INTO ip_whitelist VALUES(3,'10.0.0.0/8',1,'Private network (Class A)','2025-01-04 11:39:08','system');
|
|
||||||
INSERT INTO ip_whitelist VALUES(4,'172.16.0.0/12',1,'Private network (Class B)','2025-01-04 11:39:08','system');
|
|
||||||
INSERT INTO ip_whitelist VALUES(5,'192.168.0.0/16',1,'Private network (Class C)','2025-01-04 11:39:08','system');
|
|
||||||
|
|
||||||
INSERT INTO ip_blacklist VALUES(1,'0.0.0.0/8',1,'Reserved address space - RFC 1122',NULL,'2025-01-04 11:39:08','system');
|
|
||||||
INSERT INTO ip_blacklist VALUES(2,'100.64.0.0/10',1,'Carrier-grade NAT space - RFC 6598',NULL,'2025-01-04 11:39:08','system');
|
|
||||||
INSERT INTO ip_blacklist VALUES(3,'192.0.2.0/24',1,'TEST-NET-1 Documentation space - RFC 5737',NULL,'2025-01-04 11:39:08','system');
|
|
||||||
INSERT INTO ip_blacklist VALUES(4,'198.51.100.0/24',1,'TEST-NET-2 Documentation space - RFC 5737',NULL,'2025-01-04 11:39:08','system');
|
|
||||||
INSERT INTO ip_blacklist VALUES(5,'203.0.113.0/24',1,'TEST-NET-3 Documentation space - RFC 5737',NULL,'2025-01-04 11:39:08','system');
|
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
# Logger plugin
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
The Logger plugin provides a modular, pluggable logging system for the application.
|
|
||||||
It logs user and system events to a MySQL table named `log`.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
1. Copy the entire `logger` folder into your project's `plugins/` directory.
|
|
||||||
2. Ensure `"enabled": true` in `plugins/logger/plugin.json`.
|
|
||||||
3. On first initialization, the plugin will create the `log` table if it does not already exist.
|
|
||||||
|
|
||||||
## Database Schema
|
|
||||||
The plugin defines the following table (auto-created):
|
|
||||||
```sql
|
|
||||||
CREATE TABLE IF NOT EXISTS `log` (
|
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
`user_id` int(11) NOT NULL,
|
|
||||||
`time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
`scope` SET('user','system') NOT NULL,
|
|
||||||
`message` VARCHAR(255) NOT NULL,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `user_id` (`user_id`),
|
|
||||||
CONSTRAINT `log_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Hook API
|
|
||||||
Core must call:
|
|
||||||
```php
|
|
||||||
// After DB connect:
|
|
||||||
do_hook('logger.system_init', ['db' => $db]);
|
|
||||||
```
|
|
||||||
The plugin listens on `logger.system_init`, runs auto-migration, then sets:
|
|
||||||
```php
|
|
||||||
$GLOBALS['logObject']; // instance of Log
|
|
||||||
$GLOBALS['user_IP']; // current user IP
|
|
||||||
```
|
|
||||||
|
|
||||||
Then in the code use:
|
|
||||||
```php
|
|
||||||
$logObject->insertLog($userId, 'Your message', 'user');
|
|
||||||
$data = $logObject->readLog($userId, 'user', $offset, $limit, $filters);
|
|
||||||
```
|
|
||||||
|
|
||||||
## File Structure
|
|
||||||
```
|
|
||||||
plugins/logger/
|
|
||||||
├─ bootstrap.php # registers hook
|
|
||||||
├─ plugin.json # metadata & enabled flag
|
|
||||||
├─ README.md # this documentation
|
|
||||||
├─ models/
|
|
||||||
│ ├─ Log.php # main Log class
|
|
||||||
│ └─ LoggerFactory.php# migration + factory
|
|
||||||
├─ helpers/
|
|
||||||
│ └─ logs.php # user IP helper
|
|
||||||
└─ migrations/
|
|
||||||
└─ create_log_table.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
## Uninstall / Disable
|
|
||||||
- Set `"enabled": false` in `plugin.json` or delete the `plugins/logger/` folder.
|
|
||||||
- Core code will default to `NullLogger` and no logs will be written.
|
|
|
@ -1,33 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
// Logs plugin bootstrap
|
|
||||||
// (here we add any plugin autoloader, if needed)
|
|
||||||
|
|
||||||
// List here all the controllers in "/controllers/" that we need as pages
|
|
||||||
$GLOBALS['plugin_controllers']['logs'] = [
|
|
||||||
'logs'
|
|
||||||
];
|
|
||||||
|
|
||||||
// Logger plugin bootstrap
|
|
||||||
register_hook('logger.system_init', function(array $context) {
|
|
||||||
// Load plugin-specific LoggerFactory class
|
|
||||||
require_once __DIR__ . '/models/LoggerFactory.php';
|
|
||||||
[$logger, $userIP] = LoggerFactory::create($context['db']);
|
|
||||||
|
|
||||||
// Expose to globals for routing logic
|
|
||||||
$GLOBALS['logObject'] = $logger;
|
|
||||||
$GLOBALS['user_IP'] = $userIP;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configuration for top menu injection
|
|
||||||
define('LOGS_MAIN_MENU_SECTION', 'main'); // section of the top menu
|
|
||||||
define('LOGS_MAIN_MENU_POSITION', 20); // lower = earlier in menu
|
|
||||||
register_hook('main_menu', function($ctx) {
|
|
||||||
$section = defined('LOGS_MAIN_MENU_SECTION') ? LOGS_MAIN_MENU_SECTION : 'main';
|
|
||||||
$position = defined('LOGS_MAIN_MENU_POSITION') ? LOGS_MAIN_MENU_POSITION : 100;
|
|
||||||
// We use $section/$position for sorting/insertion logic in the menu template
|
|
||||||
echo '
|
|
||||||
<a class="dropdown-item" href="?page=logs">
|
|
||||||
<i class="fas fa-list"></i>Logs
|
|
||||||
</a>';
|
|
||||||
});
|
|
|
@ -1,15 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
function getLogLevelClass($level) {
|
|
||||||
switch (strtolower($level)) {
|
|
||||||
case 'emergency': return 'text-danger fw-bold';
|
|
||||||
case 'alert': return 'text-danger';
|
|
||||||
case 'critical': return 'text-warning fw-bold';
|
|
||||||
case 'error': return 'text-warning';
|
|
||||||
case 'warning': return 'text-warning';
|
|
||||||
case 'notice': return 'text-primary';
|
|
||||||
case 'info': return 'text-info';
|
|
||||||
case 'debug': return 'text-muted';
|
|
||||||
default: return 'text-body'; // fallback normal text
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
-- --------------------------------------------------------
|
|
||||||
-- Logger Plugin: Create `log` table if it does not exist
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE IF NOT EXISTS `log` (
|
|
||||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
`user_id` INT(11) NOT NULL,
|
|
||||||
`time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
`level` set('emergency','alert','critical','error','warning','notice','info','debug') NOT NULL DEFAULT 'info',
|
|
||||||
`scope` set('user','system') NOT NULL,
|
|
||||||
`message` VARCHAR(255) NOT NULL,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `user_id` (`user_id`),
|
|
||||||
CONSTRAINT `log_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
|
|
@ -1,119 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* class Log
|
|
||||||
*
|
|
||||||
* Handles logging events into a database and reading log entries.
|
|
||||||
*/
|
|
||||||
class Log {
|
|
||||||
/**
|
|
||||||
* @var PDO|null $db The database connection instance.
|
|
||||||
*/
|
|
||||||
private $db;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs constructor.
|
|
||||||
* Initializes the database connection.
|
|
||||||
*
|
|
||||||
* @param object $database The database object to initialize the connection.
|
|
||||||
*/
|
|
||||||
public function __construct($database) {
|
|
||||||
$this->db = $database->getConnection();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve log entries from the database.
|
|
||||||
*
|
|
||||||
* @param int $userId The ID of the user whose logs are being retrieved.
|
|
||||||
* @param string $scope The scope of the logs ('user' or 'system').
|
|
||||||
* @param int $offset The offset for pagination. Default is 0.
|
|
||||||
* @param int $items_per_page The number of log entries to retrieve per page. Default is no limit.
|
|
||||||
* @param array $filters Optional array of filters (from_time, until_time, message, id)
|
|
||||||
*
|
|
||||||
* @return array An array of log entries.
|
|
||||||
*/
|
|
||||||
public function readLog($userId, $scope, $offset = 0, $items_per_page = '', $filters = []) {
|
|
||||||
$params = [];
|
|
||||||
$where_clauses = [];
|
|
||||||
|
|
||||||
// Base query with user join
|
|
||||||
$base_sql = 'SELECT l.*, u.username
|
|
||||||
FROM log l
|
|
||||||
LEFT JOIN user u ON l.user_id = u.id';
|
|
||||||
|
|
||||||
// Add scope condition
|
|
||||||
if ($scope === 'user') {
|
|
||||||
$where_clauses[] = 'l.user_id = :user_id';
|
|
||||||
$params[':user_id'] = $userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add time range filters if specified
|
|
||||||
if (!empty($filters['from_time'])) {
|
|
||||||
$where_clauses[] = 'l.time >= :from_time';
|
|
||||||
$params[':from_time'] = $filters['from_time'] . ' 00:00:00';
|
|
||||||
}
|
|
||||||
if (!empty($filters['until_time'])) {
|
|
||||||
$where_clauses[] = 'l.time <= :until_time';
|
|
||||||
$params[':until_time'] = $filters['until_time'] . ' 23:59:59';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add message search if specified
|
|
||||||
if (!empty($filters['message'])) {
|
|
||||||
$where_clauses[] = 'l.message LIKE :message';
|
|
||||||
$params[':message'] = '%' . $filters['message'] . '%';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add user ID search if specified
|
|
||||||
if (!empty($filters['id'])) {
|
|
||||||
$where_clauses[] = 'l.user_id = :search_user_id';
|
|
||||||
$params[':search_user_id'] = $filters['id'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combine WHERE clauses
|
|
||||||
$sql = $base_sql;
|
|
||||||
if (!empty($where_clauses)) {
|
|
||||||
$sql .= ' WHERE ' . implode(' AND ', $where_clauses);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add ordering
|
|
||||||
$sql .= ' ORDER BY l.time DESC';
|
|
||||||
|
|
||||||
// Add pagination
|
|
||||||
if ($items_per_page) {
|
|
||||||
$items_per_page = (int)$items_per_page;
|
|
||||||
$sql .= ' LIMIT ' . $offset . ',' . $items_per_page;
|
|
||||||
}
|
|
||||||
|
|
||||||
$query = $this->db->prepare($sql);
|
|
||||||
$query->execute($params);
|
|
||||||
|
|
||||||
return $query->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PSR-3 style log method - inserts a log event into the database.
|
|
||||||
*
|
|
||||||
* @param string $level The log level (emergency, alert, critical, error, warning, notice, info, debug).
|
|
||||||
* @param string $message The log message to insert.
|
|
||||||
* @param string $scope The scope of the log event (e.g., 'user', 'system'). Default is 'system'.
|
|
||||||
*/
|
|
||||||
public function log(string $level, string $message, array $context = []): void {
|
|
||||||
$userId = $context['user_id'] ?? null;
|
|
||||||
$scope = $context['scope'] ?? 'system';
|
|
||||||
try {
|
|
||||||
$sql = 'INSERT INTO log
|
|
||||||
(user_id, level, scope, message)
|
|
||||||
VALUES
|
|
||||||
(:user_id, :level, :scope, :message)';
|
|
||||||
$query = $this->db->prepare($sql);
|
|
||||||
$query->execute([
|
|
||||||
':user_id' => $userId,
|
|
||||||
':level' => $level,
|
|
||||||
':scope' => $scope,
|
|
||||||
':message' => $message,
|
|
||||||
]);
|
|
||||||
} catch (Exception $e) {
|
|
||||||
// swallowing exceptions or here we could log to error log for testing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LoggerFactory for Logger Plugin.
|
|
||||||
*
|
|
||||||
* Responsible for auto-migration and creating the Log instance.
|
|
||||||
*/
|
|
||||||
class LoggerFactory
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param object $db Database connector instance.
|
|
||||||
* @return array [Log $logger, string $userIP]
|
|
||||||
*/
|
|
||||||
public static function create($db): array
|
|
||||||
{
|
|
||||||
// Auto-migration: ensure log table exists
|
|
||||||
$pdo = $db->getConnection();
|
|
||||||
$migrationFile = __DIR__ . '/../migrations/create_log_table.sql';
|
|
||||||
if (file_exists($migrationFile)) {
|
|
||||||
$sql = file_get_contents($migrationFile);
|
|
||||||
$pdo->exec($sql);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load models and core IP helper
|
|
||||||
require_once __DIR__ . '/Log.php';
|
|
||||||
require_once __DIR__ . '/../../../app/helpers/ip_helper.php';
|
|
||||||
|
|
||||||
// Instantiate logger and retrieve user IP
|
|
||||||
$logger = new \Log($db);
|
|
||||||
$userIP = getUserIP();
|
|
||||||
|
|
||||||
return [$logger, $userIP];
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Logger Plugin",
|
|
||||||
"version": "1.0.1",
|
|
||||||
"enabled": true,
|
|
||||||
"description": "Initializes logging system via LoggerFactory"
|
|
||||||
}
|
|
|
@ -1,111 +0,0 @@
|
||||||
|
|
||||||
<!-- log events -->
|
|
||||||
<div class="container-fluid mt-4">
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<h2 class="mb-0">Log events</h2>
|
|
||||||
<small>events recorded in the platform</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tabs navigation -->
|
|
||||||
<ul class="nav nav-tabs mb-4">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link <?= $scope === 'user' ? 'active' : '' ?>" href="?page=logs&tab=user">
|
|
||||||
Logs for current user
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<?php if ($has_system_access) { ?>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link <?= $scope === 'system' ? 'active' : '' ?>" href="?page=logs&tab=system">
|
|
||||||
Logs for all users
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<?php } ?>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<!-- logs filter -->
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-body">
|
|
||||||
<form method="GET" action="" class="row g-3 align-items-end">
|
|
||||||
<input type="hidden" name="page" value="logs">
|
|
||||||
<input type="hidden" name="tab" value="<?= htmlspecialchars($scope) ?>">
|
|
||||||
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label for="from_time" class="form-label">From date</label>
|
|
||||||
<input type="date" class="form-control" id="from_time" name="from_time" value="<?= htmlspecialchars($_REQUEST['from_time'] ?? '') ?>">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label for="until_time" class="form-label">Until date</label>
|
|
||||||
<input type="date" class="form-control" id="until_time" name="until_time" value="<?= htmlspecialchars($_REQUEST['until_time'] ?? '') ?>">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($scope === 'system') { ?>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label for="id" class="form-label">User ID</label>
|
|
||||||
<input type="text" class="form-control" id="id" name="id" value="<?= htmlspecialchars($_REQUEST['id'] ?? '') ?>" placeholder="Enter user ID">
|
|
||||||
</div>
|
|
||||||
<?php } ?>
|
|
||||||
|
|
||||||
<div class="col-md">
|
|
||||||
<label for="message" class="form-label">Message</label>
|
|
||||||
<input type="text" class="form-control" id="message" name="message" value="<?= htmlspecialchars($_REQUEST['message'] ?? '') ?>" placeholder="Search in log messages">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-auto">
|
|
||||||
<button type="submit" class="btn btn-primary me-2">
|
|
||||||
<i class="fas fa-search me-2"></i>Search
|
|
||||||
</button>
|
|
||||||
<a href="?page=logs&tab=<?= htmlspecialchars($scope) ?>" class="btn btn-outline-secondary">
|
|
||||||
<i class="fas fa-times me-2"></i>Clear
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- /logs filter -->
|
|
||||||
|
|
||||||
<!-- logs -->
|
|
||||||
<?php if ($time_range_specified) { ?>
|
|
||||||
<div class="alert alert-info m-3">
|
|
||||||
<i class="fas fa-calendar-alt me-2"></i>Time period: <strong><?= htmlspecialchars($from_time) ?> - <?= htmlspecialchars($until_time) ?></strong>
|
|
||||||
</div>
|
|
||||||
<?php } ?>
|
|
||||||
<div class="mb-5">
|
|
||||||
<?php if (!empty($logs['records'])) { ?>
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-hover align-middle mb-0" style="width: 100%;">
|
|
||||||
<thead class="table-light">
|
|
||||||
<tr>
|
|
||||||
<?php if ($scope === 'system') { ?>
|
|
||||||
<th>Username (id)</th>
|
|
||||||
<?php } ?>
|
|
||||||
<th style="white-space: nowrap;">Time</th>
|
|
||||||
<th style="white-space: nowrap;">Log level</th>
|
|
||||||
<th>Log message</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($logs['records'] as $row) { ?>
|
|
||||||
<tr>
|
|
||||||
<?php if ($scope === 'system') { ?>
|
|
||||||
<td style="white-space: nowrap;"><?= $row['userID'] ? '<strong>' . htmlspecialchars($row['username'] . " ({$row['userID']})") . '</strong>' : '<span class="text-muted font-weight-normal small">SYSTEM</span>' ?></td>
|
|
||||||
<?php } ?>
|
|
||||||
<td style="white-space: nowrap;"><span class="text-muted"><?= date('d M Y H:i', strtotime($row['time'])) ?></span></td>
|
|
||||||
<td style="white-space: nowrap;"><span class="<?= getLogLevelClass($row['log level']) ?>"><?= htmlspecialchars($row['log level']) ?></span></td>
|
|
||||||
<td style="width: 100%; word-break: break-word;"><?= htmlspecialchars($row['log message']) ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php } ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<?php include '../app/templates/pagination.php'; ?>
|
|
||||||
<?php } else { ?>
|
|
||||||
<div class="alert alert-info m-3">
|
|
||||||
<i class="fas fa-info-circle me-2"></i>No log entries found for the specified criteria.
|
|
||||||
</div>
|
|
||||||
<?php } ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- /log events -->
|
|
|
@ -1,24 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
// Register plugin bootstrap
|
|
||||||
// (here we add any plugin autoloader, if needed)
|
|
||||||
|
|
||||||
// List here all the controllers in "/controllers/" that we need as pages
|
|
||||||
$GLOBALS['plugin_controllers']['register'] = [
|
|
||||||
'register'
|
|
||||||
];
|
|
||||||
|
|
||||||
// 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>';
|
|
||||||
});
|
|
|
@ -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 user table
|
|
||||||
$sql = 'INSERT
|
|
||||||
INTO user (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 user_meta table
|
|
||||||
$sql2 = 'INSERT
|
|
||||||
INTO user_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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Registration Plugin",
|
|
||||||
"version": "1.0.1",
|
|
||||||
"enabled": true,
|
|
||||||
"description": "Provides registration functionality as a plugin."
|
|
||||||
}
|
|
|
@ -11,126 +11,18 @@
|
||||||
* Version: 0.4
|
* Version: 0.4
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// error reporting, comment out in production
|
|
||||||
//ini_set('display_errors', 1);
|
|
||||||
//ini_set('display_startup_errors', 1);
|
|
||||||
//error_reporting(E_ALL);
|
|
||||||
|
|
||||||
// Prepare config loader
|
|
||||||
require_once __DIR__ . '/../app/core/ConfigLoader.php';
|
|
||||||
use App\Core\ConfigLoader;
|
|
||||||
|
|
||||||
// Load configuration
|
|
||||||
$config = ConfigLoader::loadConfig([
|
|
||||||
__DIR__ . '/../app/config/jilo-web.conf.php',
|
|
||||||
__DIR__ . '/../jilo-web.conf.php',
|
|
||||||
'/srv/jilo-web/jilo-web.conf.php',
|
|
||||||
'/opt/jilo-web/jilo-web.conf.php',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Make config available globally
|
|
||||||
$GLOBALS['config'] = $config;
|
|
||||||
|
|
||||||
// Expose config file path for pages
|
|
||||||
$config_file = ConfigLoader::getConfigPath();
|
|
||||||
$localConfigPath = str_replace(__DIR__ . '/..', '', $config_file);
|
|
||||||
|
|
||||||
// Set app root with default
|
|
||||||
$app_root = $config['folder'] ?? '/';
|
|
||||||
|
|
||||||
// Preparing plugins and hooks
|
|
||||||
// Initialize HookDispatcher and plugin system
|
|
||||||
require_once __DIR__ . '/../app/core/HookDispatcher.php';
|
|
||||||
require_once __DIR__ . '/../app/core/PluginManager.php';
|
|
||||||
use App\Core\HookDispatcher;
|
|
||||||
use App\Core\PluginManager;
|
|
||||||
|
|
||||||
// Global allowed URLs registration
|
|
||||||
register_hook('filter_allowed_urls', function($urls) {
|
|
||||||
if (isset($GLOBALS['plugin_controllers']) && is_array($GLOBALS['plugin_controllers'])) {
|
|
||||||
foreach ($GLOBALS['plugin_controllers'] as $controllers) {
|
|
||||||
foreach ($controllers as $ctrl) {
|
|
||||||
$urls[] = $ctrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $urls;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Hook registration and dispatch helpers
|
|
||||||
function register_hook(string $hook, callable $callback): void {
|
|
||||||
HookDispatcher::register($hook, $callback);
|
|
||||||
}
|
|
||||||
function do_hook(string $hook, array $context = []): void {
|
|
||||||
HookDispatcher::dispatch($hook, $context);
|
|
||||||
}
|
|
||||||
function filter_public_pages(array $pages): array {
|
|
||||||
return HookDispatcher::applyFilters('filter_public_pages', $pages);
|
|
||||||
}
|
|
||||||
function filter_allowed_urls(array $urls): array {
|
|
||||||
return HookDispatcher::applyFilters('filter_allowed_urls', $urls);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load enabled plugins
|
|
||||||
$plugins_dir = dirname(__DIR__) . '/plugins/';
|
|
||||||
$enabled_plugins = PluginManager::load($plugins_dir);
|
|
||||||
$GLOBALS['enabled_plugins'] = $enabled_plugins;
|
|
||||||
|
|
||||||
// Define CSRF token include path globally
|
|
||||||
if (!defined('CSRF_TOKEN_INCLUDE')) {
|
|
||||||
define('CSRF_TOKEN_INCLUDE', dirname(__DIR__) . '/app/includes/csrf_token.php');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global cnstants
|
|
||||||
require_once '../app/includes/constants.php';
|
|
||||||
|
|
||||||
// we start output buffering and
|
// we start output buffering and
|
||||||
// flush it later only when there is no redirect
|
// flush it later only when there is no redirect
|
||||||
ob_start();
|
ob_start();
|
||||||
|
|
||||||
// Start session before any session-dependent code
|
// Apply security headers
|
||||||
require_once '../app/classes/session.php';
|
require_once '../app/includes/security_headers_middleware.php';
|
||||||
|
|
||||||
// Initialize themes system after session is started
|
// sanitize all input vars that may end up in URLs or forms
|
||||||
require_once __DIR__ . '/../app/helpers/theme.php';
|
require '../app/includes/sanitize.php';
|
||||||
use app\Helpers\Theme;
|
|
||||||
|
|
||||||
Session::startSession();
|
session_name('jilo');
|
||||||
|
session_start();
|
||||||
// Define page variable early via sanitize
|
|
||||||
require_once __DIR__ . '/../app/includes/sanitize.php';
|
|
||||||
// Ensure $page is defined to avoid undefined variable
|
|
||||||
if (!isset($page)) {
|
|
||||||
$page = 'dashboard';
|
|
||||||
}
|
|
||||||
|
|
||||||
// List of pages that don't require authentication
|
|
||||||
$public_pages = ['login', 'help', 'about'];
|
|
||||||
|
|
||||||
// Let plugins filter/extend public_pages
|
|
||||||
$public_pages = filter_public_pages($public_pages);
|
|
||||||
|
|
||||||
// Middleware pipeline for security, sanitization & CSRF
|
|
||||||
require_once __DIR__ . '/../app/core/MiddlewarePipeline.php';
|
|
||||||
$pipeline = new \App\Core\MiddlewarePipeline();
|
|
||||||
$pipeline->add(function() {
|
|
||||||
// Apply security headers
|
|
||||||
require_once __DIR__ . '/../app/includes/security_headers_middleware.php';
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// For public pages, we don't need to validate the session
|
|
||||||
// The Router will handle authentication for protected pages
|
|
||||||
$validSession = false;
|
|
||||||
$userId = null;
|
|
||||||
|
|
||||||
// Only check session for non-public pages
|
|
||||||
if (!in_array($page, $public_pages)) {
|
|
||||||
$validSession = Session::isValidSession(true);
|
|
||||||
if ($validSession) {
|
|
||||||
$userId = Session::getUserId();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize feedback message system
|
// Initialize feedback message system
|
||||||
require_once '../app/classes/feedback.php';
|
require_once '../app/classes/feedback.php';
|
||||||
|
@ -138,81 +30,120 @@ $system_messages = [];
|
||||||
|
|
||||||
require '../app/includes/errors.php';
|
require '../app/includes/errors.php';
|
||||||
|
|
||||||
|
// error reporting, comment out in production
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
// list of available pages
|
// list of available pages
|
||||||
// edit accordingly, add 'pages/PAGE.php'
|
// edit accordingly, add 'pages/PAGE.php'
|
||||||
$allowed_urls = [
|
$allowed_urls = [
|
||||||
'dashboard',
|
'dashboard',
|
||||||
'conferences','participants','components',
|
|
||||||
'graphs','latest','livejs','agents',
|
'conferences',
|
||||||
'profile','credentials','config','security',
|
'participants',
|
||||||
'settings','theme','theme-asset',
|
'components',
|
||||||
|
|
||||||
|
'graphs',
|
||||||
|
'latest',
|
||||||
|
'livejs',
|
||||||
|
|
||||||
|
'agents',
|
||||||
|
|
||||||
|
'config',
|
||||||
|
|
||||||
|
'profile',
|
||||||
|
'credentials',
|
||||||
|
|
||||||
|
'settings',
|
||||||
|
'security',
|
||||||
'status',
|
'status',
|
||||||
'help','about',
|
'logs',
|
||||||
'login','logout',
|
'help',
|
||||||
|
|
||||||
|
'login',
|
||||||
|
'logout',
|
||||||
|
'register',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Let plugins filter/extend allowed_urls
|
// cnfig file
|
||||||
$allowed_urls = filter_allowed_urls($allowed_urls);
|
// possible locations, in order of preference
|
||||||
|
$config_file_locations = [
|
||||||
// Dispatch routing and auth
|
__DIR__ . '/../app/config/jilo-web.conf.php',
|
||||||
require_once __DIR__ . '/../app/core/Router.php';
|
__DIR__ . '/../jilo-web.conf.php',
|
||||||
use App\Core\Router;
|
'/srv/jilo-web/jilo-web.conf.php',
|
||||||
$currentUser = Router::checkAuth($config, $app_root, $public_pages, $page);
|
'/opt/jilo-web/jilo-web.conf.php'
|
||||||
|
];
|
||||||
// Connect to DB via DatabaseConnector
|
$config_file = null;
|
||||||
require_once __DIR__ . '/../app/core/DatabaseConnector.php';
|
// try to find the config file
|
||||||
use App\Core\DatabaseConnector;
|
foreach ($config_file_locations as $location) {
|
||||||
$db = DatabaseConnector::connect($config);
|
if (file_exists($location)) {
|
||||||
|
$config_file = $location;
|
||||||
// Logging: default to NullLogger, plugin can override
|
break;
|
||||||
require_once __DIR__ . '/../app/core/NullLogger.php';
|
}
|
||||||
use App\Core\NullLogger;
|
|
||||||
$logObject = new NullLogger();
|
|
||||||
// Get the user IP
|
|
||||||
require_once __DIR__ . '/../app/helpers/ip_helper.php';
|
|
||||||
$user_IP = '';
|
|
||||||
|
|
||||||
// Plugin: initialize logging system plugin if available
|
|
||||||
do_hook('logger.system_init', ['db' => $db]);
|
|
||||||
|
|
||||||
// Override defaults if plugin provided real logger
|
|
||||||
if (isset($GLOBALS['logObject'])) {
|
|
||||||
$logObject = $GLOBALS['logObject'];
|
|
||||||
}
|
}
|
||||||
if (isset($GLOBALS['user_IP'])) {
|
// if found, use it
|
||||||
$user_IP = $GLOBALS['user_IP'];
|
if ($config_file) {
|
||||||
|
$localConfigPath = str_replace(__DIR__ . '/..', '', $config_file);
|
||||||
|
$config = require $config_file;
|
||||||
|
} else {
|
||||||
|
die('Config file not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// CSRF middleware and run pipeline
|
$app_root = $config['folder'];
|
||||||
$pipeline->add(function() {
|
|
||||||
// Initialize security middleware
|
// check if logged in
|
||||||
require_once __DIR__ . '/../app/includes/csrf_middleware.php';
|
unset($currentUser);
|
||||||
require_once __DIR__ . '/../app/helpers/security.php';
|
if (isset($_COOKIE['username'])) {
|
||||||
$security = SecurityHelper::getInstance();
|
if ( !isset($_SESSION['username']) ) {
|
||||||
// Verify CSRF token for POST requests
|
$_SESSION['username'] = $_COOKIE['username'];
|
||||||
return applyCsrfMiddleware();
|
}
|
||||||
});
|
$currentUser = htmlspecialchars($_SESSION['username']);
|
||||||
$pipeline->add(function() {
|
|
||||||
// Init rate limiter
|
|
||||||
global $db, $page, $userId;
|
|
||||||
require_once __DIR__ . '/../app/includes/rate_limit_middleware.php';
|
|
||||||
return checkRateLimit($db, $page, $userId);
|
|
||||||
});
|
|
||||||
$pipeline->add(function() {
|
|
||||||
// Init user functions
|
|
||||||
global $db, $userObject;
|
|
||||||
require_once __DIR__ . '/../app/classes/user.php';
|
|
||||||
include __DIR__ . '/../app/helpers/profile.php';
|
|
||||||
$userObject = new User($db);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
if (!$pipeline->run()) {
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// redirect to login
|
||||||
|
if ( !isset($_COOKIE['username']) && ($page !== 'login' && $page !== 'register') ) {
|
||||||
|
header('Location: ' . htmlspecialchars($app_root) . '?page=login');
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect to db of Jilo Web
|
||||||
|
require '../app/classes/database.php';
|
||||||
|
require '../app/includes/database.php';
|
||||||
|
try {
|
||||||
|
$response = connectDB($config);
|
||||||
|
if (!$response['db']) {
|
||||||
|
throw new Exception('Could not connect to database: ' . $response['error']);
|
||||||
|
}
|
||||||
|
$dbWeb = $response['db'];
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Feedback::flash('ERROR', 'DEFAULT', getError('Error connecting to the database.', $e->getMessage()));
|
||||||
|
include '../app/templates/page-header.php';
|
||||||
|
include '../app/helpers/feedback.php';
|
||||||
|
include '../app/templates/page-footer.php';
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// start logging
|
||||||
|
require '../app/classes/log.php';
|
||||||
|
include '../app/helpers/logs.php';
|
||||||
|
$logObject = new Log($dbWeb);
|
||||||
|
$user_IP = getUserIP();
|
||||||
|
|
||||||
|
// Initialize security middleware
|
||||||
|
require_once '../app/includes/csrf_middleware.php';
|
||||||
|
require_once '../app/helpers/security.php';
|
||||||
|
$security = SecurityHelper::getInstance();
|
||||||
|
|
||||||
|
// Verify CSRF token for POST requests
|
||||||
|
applyCsrfMiddleware();
|
||||||
|
|
||||||
|
// init rate limiter
|
||||||
|
require '../app/classes/ratelimiter.php';
|
||||||
|
|
||||||
// get platforms details
|
// get platforms details
|
||||||
require '../app/classes/platform.php';
|
require '../app/classes/platform.php';
|
||||||
$platformObject = new Platform($db);
|
$platformObject = new Platform($dbWeb);
|
||||||
$platformsAll = $platformObject->getPlatformDetails();
|
$platformsAll = $platformObject->getPlatformDetails();
|
||||||
|
|
||||||
// by default we connect ot the first configured platform
|
// by default we connect ot the first configured platform
|
||||||
|
@ -222,50 +153,54 @@ if ($platform_id == '') {
|
||||||
|
|
||||||
$platformDetails = $platformObject->getPlatformDetails($platform_id);
|
$platformDetails = $platformObject->getPlatformDetails($platform_id);
|
||||||
|
|
||||||
|
// init user functions
|
||||||
|
require '../app/classes/user.php';
|
||||||
|
include '../app/helpers/profile.php';
|
||||||
|
$userObject = new User($dbWeb);
|
||||||
|
|
||||||
// logout is a special case, as we can't use session vars for notices
|
// logout is a special case, as we can't use session vars for notices
|
||||||
if ($page == 'logout') {
|
if ($page == 'logout') {
|
||||||
// Save config before destroying session
|
// get user info before destroying session
|
||||||
$savedConfig = $config;
|
$user_id = $userObject->getUserId($currentUser)[0]['id'];
|
||||||
|
|
||||||
// clean up session
|
// clean up session
|
||||||
Session::destroySession();
|
session_unset();
|
||||||
|
session_destroy();
|
||||||
|
|
||||||
// start new session for the login page
|
// start new session for the login page
|
||||||
Session::startSession();
|
session_start();
|
||||||
|
|
||||||
// Restore config to global scope
|
|
||||||
$config = $savedConfig;
|
|
||||||
$GLOBALS['config'] = $config;
|
|
||||||
|
|
||||||
setcookie('username', "", time() - 100, $config['folder'], $config['domain'], isset($_SERVER['HTTPS']), true);
|
setcookie('username', "", time() - 100, $config['folder'], $config['domain'], isset($_SERVER['HTTPS']), true);
|
||||||
|
|
||||||
// Log successful logout
|
// Log successful logout
|
||||||
$logObject->log('info', "Logout: User \"$currentUser\" logged out. IP: $user_IP", ['user_id' => $userId, 'scope' => 'user']);
|
$logObject->insertLog($user_id, "Logout: User \"$currentUser\" logged out. IP: $user_IP", 'user');
|
||||||
|
|
||||||
// Set success message
|
// Set success message
|
||||||
Feedback::flash('LOGIN', 'LOGOUT_SUCCESS');
|
Feedback::flash('LOGIN', 'LOGOUT_SUCCESS');
|
||||||
|
|
||||||
// Use theme helper to include templates
|
include '../app/templates/page-header.php';
|
||||||
\App\Helpers\Theme::include('page-header');
|
include '../app/templates/page-menu.php';
|
||||||
\App\Helpers\Theme::include('page-menu');
|
|
||||||
include '../app/pages/login.php';
|
include '../app/pages/login.php';
|
||||||
\App\Helpers\Theme::include('page-footer');
|
include '../app/templates/page-footer.php';
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// if user is logged in, we need user details and rights
|
// if user is logged in, we need user details and rights
|
||||||
if ($validSession) {
|
if (isset($currentUser)) {
|
||||||
|
|
||||||
// If by error a logged in user requests the login page
|
// If by error a logged in user requests the login page
|
||||||
if ($page === 'login') {
|
if ($page === 'login') {
|
||||||
header('Location: ' . htmlspecialchars($app_root));
|
header('Location: ' . htmlspecialchars($app_root));
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
$userDetails = $userObject->getUserDetails($userId);
|
$user_id = $userObject->getUserId($currentUser)[0]['id'];
|
||||||
$userRights = $userObject->getUserRights($userId);
|
$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)
|
$userTimezone = (!empty($userDetails[0]['timezone'])) ? $userDetails[0]['timezone'] : 'UTC'; // Default to UTC if no timezone is set (or is missing)
|
||||||
|
|
||||||
// check if the Jilo Server is running
|
// check if the Jilo Server is running
|
||||||
require '../app/classes/server.php';
|
require '../app/classes/server.php';
|
||||||
$serverObject = new Server($db);
|
$serverObject = new Server($dbWeb);
|
||||||
|
|
||||||
$server_host = '127.0.0.1';
|
$server_host = '127.0.0.1';
|
||||||
$server_port = '8080';
|
$server_port = '8080';
|
||||||
|
@ -276,70 +211,28 @@ if ($page == 'logout') {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Plugin loading logic for all enabled plugins ---
|
// List of pages that don't require authentication
|
||||||
// Ensure all enabled plugin bootstraps are loaded before mapping controllers
|
$public_pages = ['login', 'register'];
|
||||||
foreach ($GLOBALS['enabled_plugins'] as $plugin_name => $plugin_info) {
|
|
||||||
$bootstrap_path = $plugin_info['path'] . '/bootstrap.php';
|
// Check if the requested page requires authentication
|
||||||
if (file_exists($bootstrap_path)) {
|
if (!in_array($page, $public_pages)) {
|
||||||
require_once $bootstrap_path;
|
require_once '../app/includes/session_middleware.php';
|
||||||
}
|
|
||||||
}
|
|
||||||
// Plugin controller mapping logic (we add each controller listed in bootstrap as a page)
|
|
||||||
$mapped_plugin_controllers = [];
|
|
||||||
foreach ($GLOBALS['enabled_plugins'] as $plugin_name => $plugin_info) {
|
|
||||||
if (isset($GLOBALS['plugin_controllers'][$plugin_name])) {
|
|
||||||
foreach ($GLOBALS['plugin_controllers'][$plugin_name] as $plugin_page) {
|
|
||||||
$controller_path = $plugin_info['path'] . '/controllers/' . $plugin_page . '.php';
|
|
||||||
if (file_exists($controller_path)) {
|
|
||||||
$mapped_plugin_controllers[$plugin_page] = $controller_path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// page building
|
// page building
|
||||||
if (in_array($page, $allowed_urls)) {
|
include '../app/templates/page-header.php';
|
||||||
// The page is in allowed URLs
|
include '../app/templates/page-menu.php';
|
||||||
if (isset($mapped_plugin_controllers[$page]) && file_exists($mapped_plugin_controllers[$page])) {
|
if (isset($currentUser)) {
|
||||||
// The page is from a plugin controller
|
include '../app/templates/page-sidebar.php';
|
||||||
if (defined('PLUGIN_PAGE_DIRECT_OUTPUT') && PLUGIN_PAGE_DIRECT_OUTPUT === true) {
|
|
||||||
// Barebone page controller, we don't output anything extra
|
|
||||||
include $mapped_plugin_controllers[$page];
|
|
||||||
ob_end_flush();
|
|
||||||
exit;
|
|
||||||
} else {
|
|
||||||
\App\Helpers\Theme::include('page-header');
|
|
||||||
\App\Helpers\Theme::include('page-menu');
|
|
||||||
if ($validSession) {
|
|
||||||
\App\Helpers\Theme::include('page-sidebar');
|
|
||||||
}
|
|
||||||
include $mapped_plugin_controllers[$page];
|
|
||||||
\App\Helpers\Theme::include('page-footer');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// The page is from a core controller
|
|
||||||
\App\Helpers\Theme::include('page-header');
|
|
||||||
\App\Helpers\Theme::include('page-menu');
|
|
||||||
if ($validSession) {
|
|
||||||
\App\Helpers\Theme::include('page-sidebar');
|
|
||||||
}
|
|
||||||
if (file_exists("../app/pages/{$page}.php")) {
|
|
||||||
include "../app/pages/{$page}.php";
|
|
||||||
} else {
|
|
||||||
include '../app/templates/error-notfound.php';
|
|
||||||
}
|
|
||||||
\App\Helpers\Theme::include('page-footer');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// The page is not in allowed URLs
|
|
||||||
\App\Helpers\Theme::include('page-header');
|
|
||||||
\App\Helpers\Theme::include('page-menu');
|
|
||||||
if ($validSession) {
|
|
||||||
\App\Helpers\Theme::include('page-sidebar');
|
|
||||||
}
|
|
||||||
include '../app/templates/error-notfound.php';
|
|
||||||
\App\Helpers\Theme::include('page-footer');
|
|
||||||
}
|
}
|
||||||
|
if (in_array($page, $allowed_urls)) {
|
||||||
|
// all normal pages
|
||||||
|
include "../app/pages/{$page}.php";
|
||||||
|
} else {
|
||||||
|
// the page is not in allowed urls, loading "not found" page
|
||||||
|
include '../app/templates/error-notfound.php';
|
||||||
|
}
|
||||||
|
include '../app/templates/page-footer.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
// flush the output buffer and show the page
|
// flush the output buffer and show the page
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Middleware\Mock;
|
|
||||||
|
|
||||||
class Feedback {
|
|
||||||
public static function flash($type, $message) {}
|
|
||||||
}
|
|
|
@ -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 = [];
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -16,235 +16,137 @@ class RateLimitMiddlewareTest extends TestCase
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
// Set global IP for rate limiting
|
|
||||||
global $user_IP;
|
|
||||||
$user_IP = '8.8.8.8';
|
|
||||||
|
|
||||||
// Prepare DB for Github CI
|
|
||||||
$host = defined('CI_DB_HOST') ? CI_DB_HOST : '127.0.0.1';
|
|
||||||
$password = defined('CI_DB_PASSWORD') ? CI_DB_PASSWORD : '';
|
|
||||||
|
|
||||||
// Set up test database
|
// Set up test database
|
||||||
$this->db = new Database([
|
$this->db = new Database([
|
||||||
'type' => 'mariadb',
|
'type' => 'sqlite',
|
||||||
'host' => $host,
|
'dbFile' => ':memory:'
|
||||||
'port' => '3306',
|
|
||||||
'dbname' => 'jilo_test',
|
|
||||||
'user' => 'test_jilo',
|
|
||||||
'password' => $password
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Create rate limiter instance
|
// Create rate limiter table
|
||||||
|
$this->db->getConnection()->exec("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 ip_whitelist table
|
||||||
|
$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
|
||||||
|
)");
|
||||||
|
|
||||||
|
// Create ip_blacklist table
|
||||||
|
$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->rateLimiter = new RateLimiter($this->db);
|
$this->rateLimiter = new RateLimiter($this->db);
|
||||||
|
|
||||||
// Drop tables if they exist
|
// Mock $_SERVER variables
|
||||||
$this->db->getConnection()->exec("DROP TABLE IF EXISTS security_rate_auth");
|
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
|
||||||
$this->db->getConnection()->exec("DROP TABLE IF EXISTS security_rate_page");
|
$_SERVER['REQUEST_URI'] = '/login';
|
||||||
$this->db->getConnection()->exec("DROP TABLE IF EXISTS security_ip_blacklist");
|
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||||
$this->db->getConnection()->exec("DROP TABLE IF EXISTS security_ip_whitelist");
|
|
||||||
$this->db->getConnection()->exec("DROP TABLE IF EXISTS log");
|
|
||||||
|
|
||||||
// Create required tables with correct names from RateLimiter class
|
// Define testing constant
|
||||||
$this->db->getConnection()->exec("
|
if (!defined('TESTING')) {
|
||||||
CREATE TABLE IF NOT EXISTS security_rate_auth (
|
define('TESTING', true);
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
ip_address VARCHAR(45) NOT NULL,
|
|
||||||
username VARCHAR(255) NOT NULL,
|
|
||||||
attempted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_ip_username (ip_address, username)
|
|
||||||
)
|
|
||||||
");
|
|
||||||
|
|
||||||
$this->db->getConnection()->exec("
|
|
||||||
CREATE TABLE IF NOT EXISTS security_rate_page (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
ip_address VARCHAR(45) NOT NULL,
|
|
||||||
endpoint VARCHAR(255) NOT NULL,
|
|
||||||
request_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_ip_endpoint (ip_address, endpoint),
|
|
||||||
INDEX idx_request_time (request_time)
|
|
||||||
)
|
|
||||||
");
|
|
||||||
|
|
||||||
$this->db->getConnection()->exec("
|
|
||||||
CREATE TABLE IF NOT EXISTS security_ip_blacklist (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
ip_address VARCHAR(45) NOT NULL,
|
|
||||||
is_network BOOLEAN DEFAULT FALSE,
|
|
||||||
reason VARCHAR(255),
|
|
||||||
expiry_time TIMESTAMP NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
created_by VARCHAR(255),
|
|
||||||
UNIQUE KEY unique_ip (ip_address)
|
|
||||||
)
|
|
||||||
");
|
|
||||||
|
|
||||||
$this->db->getConnection()->exec("
|
|
||||||
CREATE TABLE IF NOT EXISTS security_ip_whitelist (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
ip_address VARCHAR(45) NOT NULL,
|
|
||||||
is_network BOOLEAN DEFAULT FALSE,
|
|
||||||
description VARCHAR(255),
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
created_by VARCHAR(255),
|
|
||||||
UNIQUE KEY unique_ip (ip_address)
|
|
||||||
)
|
|
||||||
");
|
|
||||||
|
|
||||||
// Create log table
|
|
||||||
$this->db->getConnection()->exec("
|
|
||||||
CREATE TABLE IF NOT EXISTS log (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
user_id INT,
|
|
||||||
scope VARCHAR(50) NOT NULL,
|
|
||||||
message TEXT NOT NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)
|
|
||||||
");
|
|
||||||
|
|
||||||
// Mock $_SERVER['REMOTE_ADDR'] with a non-whitelisted IP
|
|
||||||
$_SERVER['REMOTE_ADDR'] = '8.8.8.8';
|
|
||||||
|
|
||||||
// Define PHPUNIT_RUNNING constant
|
|
||||||
if (!defined('PHPUNIT_RUNNING')) {
|
|
||||||
define('PHPUNIT_RUNNING', true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function tearDown(): void
|
protected function tearDown(): void
|
||||||
{
|
{
|
||||||
// Clean up all rate limit records
|
// Clean up rate limit records
|
||||||
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page");
|
$this->db->getConnection()->exec('DELETE FROM pages_rate_limits');
|
||||||
$this->db->getConnection()->exec("TRUNCATE TABLE security_ip_blacklist");
|
|
||||||
$this->db->getConnection()->exec("TRUNCATE TABLE security_ip_whitelist");
|
|
||||||
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_auth");
|
|
||||||
$this->db->getConnection()->exec("TRUNCATE TABLE log");
|
|
||||||
parent::tearDown();
|
parent::tearDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRateLimitMiddleware()
|
public function testRateLimitMiddleware()
|
||||||
{
|
{
|
||||||
// Clean any existing rate limit records
|
// Test multiple requests
|
||||||
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page");
|
for ($i = 1; $i <= 5; $i++) {
|
||||||
|
|
||||||
// Make 60 requests to reach the limit
|
|
||||||
for ($i = 0; $i < 60; $i++) {
|
|
||||||
$result = checkRateLimit($this->db, '/login');
|
$result = checkRateLimit($this->db, '/login');
|
||||||
$this->assertTrue($result, "Request $i should be allowed");
|
|
||||||
|
|
||||||
// Verify request was recorded
|
if ($i <= 5) {
|
||||||
$stmt = $this->db->getConnection()->prepare("
|
// First 5 requests should pass
|
||||||
SELECT COUNT(*) as count
|
$this->assertTrue($result);
|
||||||
FROM security_rate_page
|
} else {
|
||||||
WHERE ip_address = ?
|
// 6th and subsequent requests should be blocked
|
||||||
AND endpoint = ?
|
$this->assertFalse($result);
|
||||||
AND request_time >= DATE_SUB(NOW(), INTERVAL 1 MINUTE)
|
}
|
||||||
");
|
|
||||||
$stmt->execute(['8.8.8.8', '/login']);
|
|
||||||
$count = $stmt->fetch(PDO::FETCH_ASSOC)['count'];
|
|
||||||
$this->assertEquals($i + 1, $count, "Expected " . ($i + 1) . " requests to be recorded, got {$count}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The 61st request should be blocked
|
|
||||||
$result = checkRateLimit($this->db, '/login');
|
|
||||||
$this->assertFalse($result, "Request should be blocked after 60 requests");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRateLimitBypass()
|
public function testRateLimitBypass()
|
||||||
{
|
{
|
||||||
// Clean any existing rate limit records and lists
|
// Test AJAX request bypass
|
||||||
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page");
|
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest';
|
||||||
$this->db->getConnection()->exec("TRUNCATE TABLE security_ip_whitelist");
|
$result = checkRateLimit($this->db, '/login');
|
||||||
$this->db->getConnection()->exec("TRUNCATE TABLE security_ip_blacklist");
|
$this->assertTrue($result);
|
||||||
|
|
||||||
// Add IP to whitelist and verify it was added
|
|
||||||
$stmt = $this->db->getConnection()->prepare("INSERT INTO security_ip_whitelist (ip_address, is_network, description, created_by) VALUES (?, 0, ?, 'PHPUnit')");
|
|
||||||
$stmt->execute(['8.8.8.8', 'Test whitelist']);
|
|
||||||
|
|
||||||
// Verify IP is in whitelist
|
|
||||||
$stmt = $this->db->getConnection()->prepare("SELECT COUNT(*) as count FROM security_ip_whitelist WHERE ip_address = ?");
|
|
||||||
$stmt->execute(['8.8.8.8']);
|
|
||||||
$count = $stmt->fetch(PDO::FETCH_ASSOC)['count'];
|
|
||||||
$this->assertEquals(1, $count, "IP should be in whitelist");
|
|
||||||
|
|
||||||
// Should be able to make more requests than limit
|
|
||||||
for ($i = 0; $i < 100; $i++) {
|
|
||||||
$result = checkRateLimit($this->db, '/login');
|
|
||||||
$this->assertTrue($result, "Request $i should be allowed for whitelisted IP");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRateLimitReset()
|
public function testRateLimitReset()
|
||||||
{
|
{
|
||||||
// Clean any existing rate limit records
|
// Use up the rate limit
|
||||||
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page");
|
for ($i = 0; $i < 5; $i++) {
|
||||||
|
checkRateLimit($this->db, '/login');
|
||||||
// Make some requests
|
|
||||||
for ($i = 0; $i < 15; $i++) {
|
|
||||||
$result = checkRateLimit($this->db, '/login');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manually expire old requests
|
// Wait for rate limit to reset (use a short window for testing)
|
||||||
$this->db->getConnection()->exec("
|
sleep(2);
|
||||||
DELETE FROM security_rate_page
|
|
||||||
WHERE request_time < DATE_SUB(NOW(), INTERVAL 1 MINUTE)
|
|
||||||
");
|
|
||||||
|
|
||||||
// Should be able to make requests again
|
// Should be able to make request again
|
||||||
$result = checkRateLimit($this->db, '/login');
|
$result = checkRateLimit($this->db, '/login');
|
||||||
$this->assertTrue($result);
|
$this->assertTrue($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDifferentEndpoints()
|
public function testDifferentEndpoints()
|
||||||
{
|
{
|
||||||
// Clean any existing rate limit records
|
// Use up rate limit for login
|
||||||
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page");
|
|
||||||
|
|
||||||
// Make requests to login endpoint (default limit: 60)
|
|
||||||
for ($i = 0; $i < 30; $i++) {
|
|
||||||
$result = checkRateLimit($this->db, '/login');
|
|
||||||
$this->assertTrue($result, "Request $i to /login should be allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up between endpoint tests
|
|
||||||
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page");
|
|
||||||
|
|
||||||
// Make requests to register endpoint (limit: 5)
|
|
||||||
for ($i = 0; $i < 5; $i++) {
|
for ($i = 0; $i < 5; $i++) {
|
||||||
$result = checkRateLimit($this->db, '/register');
|
checkRateLimit($this->db, '/login');
|
||||||
$this->assertTrue($result, "Request $i to /register should be allowed");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The 6th request to register should be blocked
|
// Should still be able to access different endpoint
|
||||||
$result = checkRateLimit($this->db, '/register');
|
$result = checkRateLimit($this->db, '/dashboard');
|
||||||
$this->assertFalse($result, "Request should be blocked after 5 requests to /register");
|
$this->assertTrue($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDifferentIpAddresses()
|
public function testDifferentIpAddresses()
|
||||||
{
|
{
|
||||||
// Make requests from first IP
|
// Use up rate limit for first IP
|
||||||
for ($i = 0; $i < 30; $i++) {
|
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
|
||||||
$result = checkRateLimit($this->db, '/login');
|
for ($i = 0; $i < 5; $i++) {
|
||||||
$this->assertTrue($result);
|
checkRateLimit($this->db, '/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change IP
|
// Different IP should not be affected
|
||||||
$_SERVER['REMOTE_ADDR'] = '8.8.4.4';
|
$_SERVER['REMOTE_ADDR'] = '127.0.0.2';
|
||||||
|
$result = checkRateLimit($this->db, '/login');
|
||||||
// Should be able to make requests from different IP
|
$this->assertTrue($result);
|
||||||
for ($i = 0; $i < 30; $i++) {
|
|
||||||
$result = checkRateLimit($this->db, '/login');
|
|
||||||
$this->assertTrue($result);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testWhitelistedIp()
|
public function testWhitelistedIp()
|
||||||
{
|
{
|
||||||
// Add IP to whitelist
|
// Add IP to whitelist
|
||||||
$this->rateLimiter->addToWhitelist('8.8.8.8', false, 'Test whitelist', 'PHPUnit');
|
$this->db->execute(
|
||||||
|
'INSERT INTO ip_whitelist (ip_address, description, created_by) VALUES (?, ?, ?)',
|
||||||
|
['127.0.0.1', 'Test whitelist', 'PHPUnit']
|
||||||
|
);
|
||||||
|
|
||||||
// Should be able to make more requests than limit
|
// Should be able to make more requests than limit
|
||||||
for ($i = 0; $i < 50; $i++) {
|
for ($i = 0; $i < 10; $i++) {
|
||||||
$result = checkRateLimit($this->db, '/login');
|
$result = checkRateLimit($this->db, '/login');
|
||||||
$this->assertTrue($result);
|
$this->assertTrue($result);
|
||||||
}
|
}
|
||||||
|
@ -252,39 +154,30 @@ class RateLimitMiddlewareTest extends TestCase
|
||||||
|
|
||||||
public function testBlacklistedIp()
|
public function testBlacklistedIp()
|
||||||
{
|
{
|
||||||
// Add IP to blacklist and verify it was added
|
// Add IP to blacklist
|
||||||
$this->db->getConnection()->exec("INSERT INTO security_ip_blacklist (ip_address, is_network, reason, created_by) VALUES ('8.8.8.8', 0, 'Test blacklist', 'system')");
|
$this->db->execute(
|
||||||
|
'INSERT INTO ip_blacklist (ip_address, reason, created_by) VALUES (?, ?, ?)',
|
||||||
|
['127.0.0.1', 'Test blacklist', 'PHPUnit']
|
||||||
|
);
|
||||||
|
|
||||||
// Request should be blocked
|
// Should be blocked immediately
|
||||||
$result = checkRateLimit($this->db, '/login');
|
$result = checkRateLimit($this->db, '/login');
|
||||||
$this->assertFalse($result, "Blacklisted IP should be blocked");
|
$this->assertFalse($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRateLimitPersistence()
|
public function testRateLimitPersistence()
|
||||||
{
|
{
|
||||||
// Clean any existing rate limit records
|
// Use up some of the rate limit
|
||||||
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page");
|
for ($i = 0; $i < 2; $i++) {
|
||||||
|
checkRateLimit($this->db, '/login');
|
||||||
// Make 60 requests to reach the limit
|
|
||||||
for ($i = 0; $i < 60; $i++) {
|
|
||||||
$result = checkRateLimit($this->db, '/login');
|
|
||||||
$this->assertTrue($result, "Request $i should be allowed");
|
|
||||||
|
|
||||||
// Verify request was recorded
|
|
||||||
$stmt = $this->db->getConnection()->prepare("
|
|
||||||
SELECT COUNT(*) as count
|
|
||||||
FROM security_rate_page
|
|
||||||
WHERE ip_address = ?
|
|
||||||
AND endpoint = ?
|
|
||||||
AND request_time >= DATE_SUB(NOW(), INTERVAL 1 MINUTE)
|
|
||||||
");
|
|
||||||
$stmt->execute(['8.8.8.8', '/login']);
|
|
||||||
$count = $stmt->fetch(PDO::FETCH_ASSOC)['count'];
|
|
||||||
$this->assertEquals($i + 1, $count, "Expected " . ($i + 1) . " requests to be recorded, got {$count}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The 61st request should be blocked
|
// Destroy and restart session
|
||||||
|
//session_destroy();
|
||||||
|
//session_start();
|
||||||
|
|
||||||
|
// Should still count previous requests
|
||||||
$result = checkRateLimit($this->db, '/login');
|
$result = checkRateLimit($this->db, '/login');
|
||||||
$this->assertFalse($result, "Request should be blocked after 60 requests");
|
$this->assertTrue($result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
require_once dirname(__DIR__, 3) . '/app/includes/session_middleware.php';
|
||||||
use Tests\Feature\Middleware\Mock\Session;
|
|
||||||
use Tests\Feature\Middleware\Mock\Feedback;
|
|
||||||
|
|
||||||
require_once __DIR__ . '/MockSession.php';
|
use PHPUnit\Framework\TestCase;
|
||||||
require_once __DIR__ . '/MockFeedback.php';
|
|
||||||
|
|
||||||
class SessionMiddlewareTest extends TestCase
|
class SessionMiddlewareTest extends TestCase
|
||||||
{
|
{
|
||||||
|
@ -41,24 +38,11 @@ class SessionMiddlewareTest extends TestCase
|
||||||
protected function tearDown(): void
|
protected function tearDown(): void
|
||||||
{
|
{
|
||||||
parent::tearDown();
|
parent::tearDown();
|
||||||
$_SESSION = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function applyMiddleware()
|
public function testSessionStart()
|
||||||
{
|
{
|
||||||
// Check session validity
|
$result = applySessionMiddleware($this->config, $this->app_root);
|
||||||
if (!Session::isValidSession()) {
|
|
||||||
// Session invalid, clean up
|
|
||||||
Session::cleanup($this->config);
|
|
||||||
Feedback::flash("LOGIN", "SESSION_TIMEOUT");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testValidSession()
|
|
||||||
{
|
|
||||||
$result = $this->applyMiddleware();
|
|
||||||
|
|
||||||
$this->assertTrue($result);
|
$this->assertTrue($result);
|
||||||
$this->assertArrayHasKey('LAST_ACTIVITY', $_SESSION);
|
$this->assertArrayHasKey('LAST_ACTIVITY', $_SESSION);
|
||||||
|
@ -70,10 +54,24 @@ class SessionMiddlewareTest extends TestCase
|
||||||
public function testSessionTimeout()
|
public function testSessionTimeout()
|
||||||
{
|
{
|
||||||
$_SESSION['LAST_ACTIVITY'] = time() - (self::SESSION_TIMEOUT + 60); // 2 hours + 1 minute ago
|
$_SESSION['LAST_ACTIVITY'] = time() - (self::SESSION_TIMEOUT + 60); // 2 hours + 1 minute ago
|
||||||
$result = $this->applyMiddleware();
|
|
||||||
|
$result = applySessionMiddleware($this->config, $this->app_root);
|
||||||
|
|
||||||
$this->assertFalse($result);
|
$this->assertFalse($result);
|
||||||
$this->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()
|
public function testRememberMe()
|
||||||
|
@ -81,7 +79,7 @@ class SessionMiddlewareTest extends TestCase
|
||||||
$_SESSION['REMEMBER_ME'] = true;
|
$_SESSION['REMEMBER_ME'] = true;
|
||||||
$_SESSION['LAST_ACTIVITY'] = time() - (self::SESSION_TIMEOUT + 60); // More than 2 hours ago
|
$_SESSION['LAST_ACTIVITY'] = time() - (self::SESSION_TIMEOUT + 60); // More than 2 hours ago
|
||||||
|
|
||||||
$result = $this->applyMiddleware();
|
$result = applySessionMiddleware($this->config, $this->app_root);
|
||||||
|
|
||||||
$this->assertTrue($result);
|
$this->assertTrue($result);
|
||||||
$this->assertArrayHasKey('user_id', $_SESSION);
|
$this->assertArrayHasKey('user_id', $_SESSION);
|
||||||
|
@ -90,19 +88,19 @@ class SessionMiddlewareTest extends TestCase
|
||||||
public function testNoUserSession()
|
public function testNoUserSession()
|
||||||
{
|
{
|
||||||
unset($_SESSION['user_id']);
|
unset($_SESSION['user_id']);
|
||||||
$result = $this->applyMiddleware();
|
$result = applySessionMiddleware($this->config, $this->app_root);
|
||||||
|
|
||||||
$this->assertFalse($result);
|
$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
|
$_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->assertFalse($result);
|
||||||
$this->assertEmpty($_SESSION);
|
$this->assertArrayNotHasKey('user_id', $_SESSION, 'Session should be cleared after timeout');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ require_once dirname(__DIR__, 3) . '/app/helpers/security.php';
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
class TestLogger {
|
class TestLogger {
|
||||||
public static function insertLog($userId, $message, $scope = 'user') {
|
public static function insertLog($user_id, $message, $scope = 'user') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,9 +24,9 @@ class AgentTest extends TestCase
|
||||||
'dbFile' => ':memory:'
|
'dbFile' => ':memory:'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Create jilo_agent table
|
// Create jilo_agents table
|
||||||
$this->db->getConnection()->exec("
|
$this->db->getConnection()->exec("
|
||||||
CREATE TABLE jilo_agent (
|
CREATE TABLE jilo_agents (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
host_id INTEGER NOT NULL,
|
host_id INTEGER NOT NULL,
|
||||||
agent_type_id INTEGER NOT NULL,
|
agent_type_id INTEGER NOT NULL,
|
||||||
|
@ -38,18 +38,18 @@ class AgentTest extends TestCase
|
||||||
)
|
)
|
||||||
");
|
");
|
||||||
|
|
||||||
// Create jilo_agent_type table
|
// Create jilo_agent_types table
|
||||||
$this->db->getConnection()->exec("
|
$this->db->getConnection()->exec("
|
||||||
CREATE TABLE jilo_agent_type (
|
CREATE TABLE jilo_agent_types (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
description TEXT NOT NULL,
|
description TEXT NOT NULL,
|
||||||
endpoint TEXT NOT NULL
|
endpoint TEXT NOT NULL
|
||||||
)
|
)
|
||||||
");
|
");
|
||||||
|
|
||||||
// Create host table
|
// Create hosts table
|
||||||
$this->db->getConnection()->exec("
|
$this->db->getConnection()->exec("
|
||||||
CREATE TABLE host (
|
CREATE TABLE hosts (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
platform_id INTEGER NOT NULL,
|
platform_id INTEGER NOT NULL,
|
||||||
name TEXT NOT NULL
|
name TEXT NOT NULL
|
||||||
|
@ -58,12 +58,12 @@ class AgentTest extends TestCase
|
||||||
|
|
||||||
// Insert test host
|
// Insert test host
|
||||||
$this->db->getConnection()->exec("
|
$this->db->getConnection()->exec("
|
||||||
INSERT INTO host (id, platform_id, name) VALUES (1, 1, 'Test Host')
|
INSERT INTO hosts (id, platform_id, name) VALUES (1, 1, 'Test Host')
|
||||||
");
|
");
|
||||||
|
|
||||||
// Insert test agent type
|
// Insert test agent type
|
||||||
$this->db->getConnection()->exec("
|
$this->db->getConnection()->exec("
|
||||||
INSERT INTO jilo_agent_type (id, description, endpoint)
|
INSERT INTO jilo_agent_types (id, description, endpoint)
|
||||||
VALUES (1, 'Test Agent Type', '/api/test')
|
VALUES (1, 'Test Agent Type', '/api/test')
|
||||||
");
|
");
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ class AgentTest extends TestCase
|
||||||
$this->assertTrue($result);
|
$this->assertTrue($result);
|
||||||
|
|
||||||
// Verify agent was created
|
// Verify agent was created
|
||||||
$stmt = $this->db->getConnection()->prepare('SELECT * FROM jilo_agent WHERE host_id = ?');
|
$stmt = $this->db->getConnection()->prepare('SELECT * FROM jilo_agents WHERE host_id = ?');
|
||||||
$stmt->execute([$hostId]);
|
$stmt->execute([$hostId]);
|
||||||
$agent = $stmt->fetch(PDO::FETCH_ASSOC);
|
$agent = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
@ -131,7 +131,7 @@ class AgentTest extends TestCase
|
||||||
$this->agent->addAgent($hostId, $data);
|
$this->agent->addAgent($hostId, $data);
|
||||||
|
|
||||||
// Get agent ID
|
// Get agent ID
|
||||||
$stmt = $this->db->getConnection()->prepare('SELECT id FROM jilo_agent WHERE host_id = ? LIMIT 1');
|
$stmt = $this->db->getConnection()->prepare('SELECT id FROM jilo_agents WHERE host_id = ? LIMIT 1');
|
||||||
$stmt->execute([$hostId]);
|
$stmt->execute([$hostId]);
|
||||||
$agentId = $stmt->fetch(PDO::FETCH_COLUMN);
|
$agentId = $stmt->fetch(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
@ -148,7 +148,7 @@ class AgentTest extends TestCase
|
||||||
$this->assertTrue($result);
|
$this->assertTrue($result);
|
||||||
|
|
||||||
// Verify update
|
// Verify update
|
||||||
$stmt = $this->db->getConnection()->prepare('SELECT * FROM jilo_agent WHERE id = ?');
|
$stmt = $this->db->getConnection()->prepare('SELECT * FROM jilo_agents WHERE id = ?');
|
||||||
$stmt->execute([$agentId]);
|
$stmt->execute([$agentId]);
|
||||||
$agent = $stmt->fetch(PDO::FETCH_ASSOC);
|
$agent = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
@ -171,7 +171,7 @@ class AgentTest extends TestCase
|
||||||
$this->agent->addAgent($hostId, $data);
|
$this->agent->addAgent($hostId, $data);
|
||||||
|
|
||||||
// Get agent ID
|
// Get agent ID
|
||||||
$stmt = $this->db->getConnection()->prepare('SELECT id FROM jilo_agent WHERE host_id = ? LIMIT 1');
|
$stmt = $this->db->getConnection()->prepare('SELECT id FROM jilo_agents WHERE host_id = ? LIMIT 1');
|
||||||
$stmt->execute([$hostId]);
|
$stmt->execute([$hostId]);
|
||||||
$agentId = $stmt->fetch(PDO::FETCH_COLUMN);
|
$agentId = $stmt->fetch(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
@ -180,7 +180,7 @@ class AgentTest extends TestCase
|
||||||
$this->assertTrue($result);
|
$this->assertTrue($result);
|
||||||
|
|
||||||
// Verify deletion
|
// Verify deletion
|
||||||
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) FROM jilo_agent WHERE id = ?');
|
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) FROM jilo_agents WHERE id = ?');
|
||||||
$stmt->execute([$agentId]);
|
$stmt->execute([$agentId]);
|
||||||
$count = $stmt->fetch(PDO::FETCH_COLUMN);
|
$count = $stmt->fetch(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
@ -201,7 +201,7 @@ class AgentTest extends TestCase
|
||||||
$this->agent->addAgent($hostId, $data);
|
$this->agent->addAgent($hostId, $data);
|
||||||
|
|
||||||
// Get agent ID
|
// Get agent ID
|
||||||
$stmt = $this->db->getConnection()->prepare('SELECT id FROM jilo_agent WHERE host_id = ? LIMIT 1');
|
$stmt = $this->db->getConnection()->prepare('SELECT id FROM jilo_agents WHERE host_id = ? LIMIT 1');
|
||||||
$stmt->execute([$hostId]);
|
$stmt->execute([$hostId]);
|
||||||
$agentId = $stmt->fetch(PDO::FETCH_COLUMN);
|
$agentId = $stmt->fetch(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
|
|
@ -23,9 +23,9 @@ class HostTest extends TestCase
|
||||||
'dbFile' => ':memory:'
|
'dbFile' => ':memory:'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Create host table
|
// Create hosts table
|
||||||
$this->db->getConnection()->exec("
|
$this->db->getConnection()->exec("
|
||||||
CREATE TABLE host (
|
CREATE TABLE hosts (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
platform_id INTEGER NOT NULL,
|
platform_id INTEGER NOT NULL,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
|
@ -33,9 +33,9 @@ class HostTest extends TestCase
|
||||||
)
|
)
|
||||||
");
|
");
|
||||||
|
|
||||||
// Create jilo_agent table for relationship testing
|
// Create jilo_agents table for relationship testing
|
||||||
$this->db->getConnection()->exec("
|
$this->db->getConnection()->exec("
|
||||||
CREATE TABLE jilo_agent (
|
CREATE TABLE jilo_agents (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
host_id INTEGER NOT NULL,
|
host_id INTEGER NOT NULL,
|
||||||
agent_type_id INTEGER NOT NULL,
|
agent_type_id INTEGER NOT NULL,
|
||||||
|
@ -60,7 +60,7 @@ class HostTest extends TestCase
|
||||||
$this->assertTrue($result);
|
$this->assertTrue($result);
|
||||||
|
|
||||||
// Verify host was created
|
// Verify host was created
|
||||||
$stmt = $this->db->getConnection()->prepare('SELECT * FROM host WHERE platform_id = ? AND name = ?');
|
$stmt = $this->db->getConnection()->prepare('SELECT * FROM hosts WHERE platform_id = ? AND name = ?');
|
||||||
$stmt->execute([$data['platform_id'], $data['name']]);
|
$stmt->execute([$data['platform_id'], $data['name']]);
|
||||||
$host = $stmt->fetch(\PDO::FETCH_ASSOC);
|
$host = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ class HostTest extends TestCase
|
||||||
$this->host->addHost($data);
|
$this->host->addHost($data);
|
||||||
|
|
||||||
// Get host ID
|
// Get host ID
|
||||||
$stmt = $this->db->getConnection()->prepare('SELECT id FROM host WHERE platform_id = ? AND name = ?');
|
$stmt = $this->db->getConnection()->prepare('SELECT id FROM hosts WHERE platform_id = ? AND name = ?');
|
||||||
$stmt->execute([$data['platform_id'], $data['name']]);
|
$stmt->execute([$data['platform_id'], $data['name']]);
|
||||||
$hostId = $stmt->fetch(\PDO::FETCH_COLUMN);
|
$hostId = $stmt->fetch(\PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
@ -122,7 +122,7 @@ class HostTest extends TestCase
|
||||||
$this->assertTrue($result);
|
$this->assertTrue($result);
|
||||||
|
|
||||||
// Verify update
|
// Verify update
|
||||||
$stmt = $this->db->getConnection()->prepare('SELECT * FROM host WHERE id = ?');
|
$stmt = $this->db->getConnection()->prepare('SELECT * FROM hosts WHERE id = ?');
|
||||||
$stmt->execute([$hostId]);
|
$stmt->execute([$hostId]);
|
||||||
$host = $stmt->fetch(\PDO::FETCH_ASSOC);
|
$host = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
@ -141,13 +141,13 @@ class HostTest extends TestCase
|
||||||
$this->host->addHost($data);
|
$this->host->addHost($data);
|
||||||
|
|
||||||
// Get host ID
|
// Get host ID
|
||||||
$stmt = $this->db->getConnection()->prepare('SELECT id FROM host WHERE platform_id = ? AND name = ?');
|
$stmt = $this->db->getConnection()->prepare('SELECT id FROM hosts WHERE platform_id = ? AND name = ?');
|
||||||
$stmt->execute([$data['platform_id'], $data['name']]);
|
$stmt->execute([$data['platform_id'], $data['name']]);
|
||||||
$hostId = $stmt->fetch(\PDO::FETCH_COLUMN);
|
$hostId = $stmt->fetch(\PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
// Add test agent to the host
|
// Add test agent to the host
|
||||||
$this->db->getConnection()->exec("
|
$this->db->getConnection()->exec("
|
||||||
INSERT INTO jilo_agent (host_id, agent_type_id, url, secret_key)
|
INSERT INTO jilo_agents (host_id, agent_type_id, url, secret_key)
|
||||||
VALUES ($hostId, 1, 'http://test:8080', 'secret')
|
VALUES ($hostId, 1, 'http://test:8080', 'secret')
|
||||||
");
|
");
|
||||||
|
|
||||||
|
@ -156,13 +156,13 @@ class HostTest extends TestCase
|
||||||
$this->assertTrue($result);
|
$this->assertTrue($result);
|
||||||
|
|
||||||
// Verify host deletion
|
// Verify host deletion
|
||||||
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) FROM host WHERE id = ?');
|
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) FROM hosts WHERE id = ?');
|
||||||
$stmt->execute([$hostId]);
|
$stmt->execute([$hostId]);
|
||||||
$hostCount = $stmt->fetch(\PDO::FETCH_COLUMN);
|
$hostCount = $stmt->fetch(\PDO::FETCH_COLUMN);
|
||||||
$this->assertEquals(0, $hostCount);
|
$this->assertEquals(0, $hostCount);
|
||||||
|
|
||||||
// Verify agent deletion
|
// Verify agent deletion
|
||||||
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) FROM jilo_agent WHERE host_id = ?');
|
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) FROM jilo_agents WHERE host_id = ?');
|
||||||
$stmt->execute([$hostId]);
|
$stmt->execute([$hostId]);
|
||||||
$agentCount = $stmt->fetch(\PDO::FETCH_COLUMN);
|
$agentCount = $stmt->fetch(\PDO::FETCH_COLUMN);
|
||||||
$this->assertEquals(0, $agentCount);
|
$this->assertEquals(0, $agentCount);
|
||||||
|
|
|
@ -1,261 +1,138 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
require_once dirname(__DIR__, 3) . '/app/classes/database.php';
|
require_once dirname(__DIR__, 3) . '/app/classes/database.php';
|
||||||
|
require_once dirname(__DIR__, 3) . '/app/classes/log.php';
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
/**
|
|
||||||
* TestLogger class for testing log functionality
|
|
||||||
* This is a simplified implementation that mimics the plugin's Log class
|
|
||||||
* but with a different name to avoid conflicts
|
|
||||||
*/
|
|
||||||
class TestLogger {
|
|
||||||
private $db;
|
|
||||||
|
|
||||||
public function __construct($database) {
|
|
||||||
$this->db = $database->getConnection();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function insertLog($userId, $message, $scope = 'user') {
|
|
||||||
try {
|
|
||||||
$sql = 'INSERT INTO log
|
|
||||||
(user_id, scope, message)
|
|
||||||
VALUES
|
|
||||||
(:user_id, :scope, :message)';
|
|
||||||
|
|
||||||
$query = $this->db->prepare($sql);
|
|
||||||
$query->execute([
|
|
||||||
':user_id' => $userId,
|
|
||||||
':scope' => $scope,
|
|
||||||
':message' => $message,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
return $e->getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function readLog($userId, $scope, $offset = 0, $items_per_page = '', $filters = []) {
|
|
||||||
$params = [];
|
|
||||||
$where_clauses = [];
|
|
||||||
|
|
||||||
// Base query with user join
|
|
||||||
$base_sql = 'SELECT l.*, u.username
|
|
||||||
FROM log l
|
|
||||||
LEFT JOIN user u ON l.user_id = u.id';
|
|
||||||
|
|
||||||
// Add scope condition
|
|
||||||
if ($scope === 'user') {
|
|
||||||
$where_clauses[] = 'l.user_id = :user_id';
|
|
||||||
$params[':user_id'] = $userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add time range filters if specified
|
|
||||||
if (!empty($filters['from_time'])) {
|
|
||||||
$where_clauses[] = 'l.time >= :from_time';
|
|
||||||
$params[':from_time'] = $filters['from_time'] . ' 00:00:00';
|
|
||||||
}
|
|
||||||
if (!empty($filters['until_time'])) {
|
|
||||||
$where_clauses[] = 'l.time <= :until_time';
|
|
||||||
$params[':until_time'] = $filters['until_time'] . ' 23:59:59';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add message search if specified
|
|
||||||
if (!empty($filters['message'])) {
|
|
||||||
$where_clauses[] = 'l.message LIKE :message';
|
|
||||||
$params[':message'] = '%' . $filters['message'] . '%';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add user ID search if specified
|
|
||||||
if (!empty($filters['id'])) {
|
|
||||||
$where_clauses[] = 'l.user_id = :search_user_id';
|
|
||||||
$params[':search_user_id'] = $filters['id'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combine WHERE clauses
|
|
||||||
$sql = $base_sql;
|
|
||||||
if (!empty($where_clauses)) {
|
|
||||||
$sql .= ' WHERE ' . implode(' AND ', $where_clauses);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add ordering
|
|
||||||
$sql .= ' ORDER BY l.time DESC';
|
|
||||||
|
|
||||||
// Add pagination
|
|
||||||
if ($items_per_page) {
|
|
||||||
$items_per_page = (int)$items_per_page;
|
|
||||||
$sql .= ' LIMIT ' . $offset . ',' . $items_per_page;
|
|
||||||
}
|
|
||||||
|
|
||||||
$query = $this->db->prepare($sql);
|
|
||||||
$query->execute($params);
|
|
||||||
|
|
||||||
return $query->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LogTest extends TestCase
|
class LogTest extends TestCase
|
||||||
{
|
{
|
||||||
private $db;
|
private $db;
|
||||||
private $log;
|
private $log;
|
||||||
private $testUserId;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
// Prepare DB for Github CI
|
|
||||||
$host = defined('CI_DB_HOST') ? CI_DB_HOST : '127.0.0.1';
|
|
||||||
$password = defined('CI_DB_PASSWORD') ? CI_DB_PASSWORD : '';
|
|
||||||
|
|
||||||
// Set up test database
|
// Set up test database
|
||||||
$this->db = new Database([
|
$this->db = new Database([
|
||||||
'type' => 'mariadb',
|
'type' => 'sqlite',
|
||||||
'host' => $host,
|
'dbFile' => ':memory:'
|
||||||
'port' => '3306',
|
|
||||||
'dbname' => 'jilo_test',
|
|
||||||
'user' => 'test_jilo',
|
|
||||||
'password' => $password
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Create user table
|
// Create users table
|
||||||
$this->db->getConnection()->exec("
|
$this->db->getConnection()->exec("
|
||||||
CREATE TABLE IF NOT EXISTS user (
|
CREATE TABLE users (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
username VARCHAR(255) NOT NULL,
|
username TEXT NOT NULL
|
||||||
password VARCHAR(255) NOT NULL,
|
|
||||||
email VARCHAR(255) NOT NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)
|
)
|
||||||
");
|
");
|
||||||
|
|
||||||
// Create log table with the expected schema from Log class
|
// Create test user
|
||||||
$this->db->getConnection()->exec("
|
$this->db->getConnection()->exec("
|
||||||
CREATE TABLE IF NOT EXISTS log (
|
INSERT INTO users (id, username) VALUES (1, 'testuser'), (2, 'testuser2')
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
");
|
||||||
user_id INT,
|
|
||||||
scope VARCHAR(50) NOT NULL DEFAULT 'user',
|
// Create logs table
|
||||||
|
$this->db->getConnection()->exec("
|
||||||
|
CREATE TABLE logs (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
user_id INTEGER,
|
||||||
|
scope TEXT NOT NULL,
|
||||||
message TEXT NOT NULL,
|
message TEXT NOT NULL,
|
||||||
time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (user_id) REFERENCES user(id)
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
)
|
)
|
||||||
");
|
");
|
||||||
|
|
||||||
// Create test users with all required fields
|
$this->log = new Log($this->db);
|
||||||
$this->db->getConnection()->exec("
|
|
||||||
INSERT INTO user (username, password, email)
|
|
||||||
VALUES
|
|
||||||
('testuser', 'password123', 'testuser@example.com'),
|
|
||||||
('testuser2', 'password123', 'testuser2@example.com')
|
|
||||||
");
|
|
||||||
|
|
||||||
// Store test user ID for later use
|
|
||||||
$stmt = $this->db->getConnection()->query("SELECT id FROM user WHERE username = 'testuser' LIMIT 1");
|
|
||||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
$this->testUserId = $user['id'];
|
|
||||||
|
|
||||||
// Create a TestLogger instance that will be used by the app's Log wrapper
|
|
||||||
$this->log = new TestLogger($this->db);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function tearDown(): void
|
|
||||||
{
|
|
||||||
// Drop tables in correct order (respect foreign key constraints)
|
|
||||||
$this->db->getConnection()->exec("DROP TABLE IF EXISTS log");
|
|
||||||
$this->db->getConnection()->exec("DROP TABLE IF EXISTS user");
|
|
||||||
parent::tearDown();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testInsertLog()
|
public function testInsertLog()
|
||||||
{
|
{
|
||||||
$result = $this->log->insertLog($this->testUserId, 'Test message', 'test');
|
$result = $this->log->insertLog(1, 'Test message', 'test');
|
||||||
$this->assertTrue($result);
|
$this->assertTrue($result);
|
||||||
|
|
||||||
// Verify the log was inserted
|
$stmt = $this->db->getConnection()->prepare("SELECT * FROM logs WHERE scope = ?");
|
||||||
$stmt = $this->db->getConnection()->query("SELECT * FROM log WHERE user_id = {$this->testUserId} LIMIT 1");
|
$stmt->execute(['test']);
|
||||||
$log = $stmt->fetch(PDO::FETCH_ASSOC);
|
$log = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$this->assertEquals(1, $log['user_id']);
|
||||||
$this->assertEquals('Test message', $log['message']);
|
$this->assertEquals('Test message', $log['message']);
|
||||||
$this->assertEquals('test', $log['scope']);
|
$this->assertEquals('test', $log['scope']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testReadLog()
|
public function testReadLog()
|
||||||
{
|
{
|
||||||
// Insert some test logs with a delay to ensure order
|
// Insert test logs
|
||||||
$this->log->insertLog($this->testUserId, 'Test message 1', 'user');
|
$this->log->insertLog(1, 'Test message 1', 'user');
|
||||||
sleep(1); // Ensure different timestamps
|
$this->log->insertLog(1, 'Test message 2', 'user');
|
||||||
$this->log->insertLog($this->testUserId, 'Test message 2', 'user');
|
|
||||||
|
|
||||||
$logs = $this->log->readLog($this->testUserId, 'user');
|
$logs = $this->log->readLog(1, 'user');
|
||||||
$this->assertCount(2, $logs);
|
$this->assertCount(2, $logs);
|
||||||
$this->assertEquals('Test message 2', $logs[0]['message']); // Most recent first (by time)
|
$this->assertEquals('Test message 1', $logs[0]['message']);
|
||||||
|
$this->assertEquals('Test message 2', $logs[1]['message']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testReadLogWithTimeFilter()
|
public function testReadLogWithTimeFilter()
|
||||||
{
|
{
|
||||||
// First message from yesterday
|
// Insert test logs with different times
|
||||||
$yesterday = date('Y-m-d', strtotime('-1 day'));
|
$this->log->insertLog(1, 'Old message', 'user');
|
||||||
$stmt = $this->db->getConnection()->prepare("
|
sleep(1); // Ensure different timestamps
|
||||||
INSERT INTO log (user_id, scope, message, time)
|
$this->log->insertLog(1, 'New message', 'user');
|
||||||
VALUES (?, ?, ?, ?)
|
|
||||||
");
|
|
||||||
$stmt->execute([$this->testUserId, 'user', 'Test message 1', $yesterday . ' 12:00:00']);
|
|
||||||
|
|
||||||
// Second message from today
|
$now = date('Y-m-d H:i:s');
|
||||||
$today = date('Y-m-d');
|
$oneHourAgo = date('Y-m-d H:i:s', strtotime('-1 hour'));
|
||||||
$stmt->execute([$this->testUserId, 'user', 'Test message 2', $today . ' 12:00:00']);
|
|
||||||
|
|
||||||
// Should get only today's messages
|
$logs = $this->log->readLog(1, 'user', 0, '', [
|
||||||
$logs = $this->log->readLog($this->testUserId, 'user', 0, '', [
|
'from_time' => $oneHourAgo,
|
||||||
'from_time' => $today
|
'until_time' => $now
|
||||||
]);
|
]);
|
||||||
$this->assertCount(1, $logs);
|
|
||||||
$this->assertEquals('Test message 2', $logs[0]['message']); // Most recent first
|
$this->assertCount(2, $logs);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testReadLogWithPagination()
|
public function testReadLogWithPagination()
|
||||||
{
|
{
|
||||||
// Insert multiple test logs with delays to ensure order
|
// Insert test logs
|
||||||
for ($i = 1; $i <= 5; $i++) {
|
$this->log->insertLog(1, 'Message 1', 'user');
|
||||||
$this->log->insertLog($this->testUserId, "Test message $i", 'user');
|
$this->log->insertLog(1, 'Message 2', 'user');
|
||||||
sleep(1); // Ensure different timestamps
|
$this->log->insertLog(1, 'Message 3', 'user');
|
||||||
}
|
|
||||||
|
|
||||||
// Get all logs to verify total
|
// Test with limit
|
||||||
$allLogs = $this->log->readLog($this->testUserId, 'user');
|
$logs = $this->log->readLog(1, 'user', 0, 2);
|
||||||
$this->assertCount(5, $allLogs);
|
|
||||||
|
|
||||||
// Get first page (offset 0, limit 2)
|
|
||||||
$logs = $this->log->readLog($this->testUserId, 'user', 0, 2);
|
|
||||||
$this->assertCount(2, $logs);
|
$this->assertCount(2, $logs);
|
||||||
$this->assertEquals('Test message 5', $logs[0]['message']); // Most recent first
|
|
||||||
$this->assertEquals('Test message 4', $logs[1]['message']);
|
|
||||||
|
|
||||||
// Get second page (offset 2, limit 2)
|
// Test with offset
|
||||||
$logs = $this->log->readLog($this->testUserId, 'user', 2, 2);
|
$logs = $this->log->readLog(1, 'user', 2, 2);
|
||||||
$this->assertCount(2, $logs);
|
$this->assertCount(1, $logs);
|
||||||
$this->assertEquals('Test message 3', $logs[0]['message']);
|
|
||||||
$this->assertEquals('Test message 2', $logs[1]['message']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testReadLogWithMessageFilter()
|
public function testReadLogWithMessageFilter()
|
||||||
{
|
{
|
||||||
// Insert test logs with different messages and delays
|
// Insert test logs
|
||||||
$this->log->insertLog($this->testUserId, 'Test message ABC', 'user');
|
$this->log->insertLog(1, 'Test message', 'user');
|
||||||
sleep(1); // Ensure different timestamps
|
$this->log->insertLog(1, 'Another message', 'user');
|
||||||
$this->log->insertLog($this->testUserId, 'Test message XYZ', 'user');
|
|
||||||
sleep(1); // Ensure different timestamps
|
|
||||||
$this->log->insertLog($this->testUserId, 'Different message', 'user');
|
|
||||||
|
|
||||||
// Filter by message content
|
$logs = $this->log->readLog(1, 'user', 0, '', [
|
||||||
$logs = $this->log->readLog($this->testUserId, 'user', 0, '', ['message' => 'Test message']);
|
'message' => 'Test'
|
||||||
$this->assertCount(2, $logs);
|
]);
|
||||||
|
|
||||||
// Verify filtered results
|
$this->assertCount(1, $logs);
|
||||||
foreach ($logs as $log) {
|
$this->assertEquals('Test message', $logs[0]['message']);
|
||||||
$this->assertStringContainsString('Test message', $log['message']);
|
}
|
||||||
}
|
|
||||||
|
public function testReadLogWithUserFilter()
|
||||||
|
{
|
||||||
|
// Insert test logs for different users
|
||||||
|
$this->log->insertLog(1, 'User 1 message', 'user');
|
||||||
|
$this->log->insertLog(2, 'User 2 message', 'user');
|
||||||
|
|
||||||
|
$logs = $this->log->readLog(1, 'user', 0, '', [
|
||||||
|
'id' => 1
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertCount(1, $logs);
|
||||||
|
$this->assertEquals('User 1 message', $logs[0]['message']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,26 +20,26 @@ class PlatformTest extends TestCase
|
||||||
'dbFile' => ':memory:'
|
'dbFile' => ':memory:'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Create host table
|
// Create hosts table
|
||||||
$this->db->getConnection()->exec("
|
$this->db->getConnection()->exec("
|
||||||
CREATE TABLE host (
|
CREATE TABLE hosts (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
platform_id INTEGER NOT NULL,
|
platform_id INTEGER NOT NULL,
|
||||||
name TEXT NOT NULL
|
name TEXT NOT NULL
|
||||||
)
|
)
|
||||||
");
|
");
|
||||||
|
|
||||||
// Create jilo_agent table
|
// Create jilo_agents table
|
||||||
$this->db->getConnection()->exec("
|
$this->db->getConnection()->exec("
|
||||||
CREATE TABLE jilo_agent (
|
CREATE TABLE jilo_agents (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
host_id INTEGER NOT NULL
|
host_id INTEGER NOT NULL
|
||||||
)
|
)
|
||||||
");
|
");
|
||||||
|
|
||||||
// Create platform table
|
// Create platforms table
|
||||||
$this->db->getConnection()->exec("
|
$this->db->getConnection()->exec("
|
||||||
CREATE TABLE platform (
|
CREATE TABLE platforms (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
jitsi_url TEXT NOT NULL,
|
jitsi_url TEXT NOT NULL,
|
||||||
|
@ -64,7 +64,7 @@ class PlatformTest extends TestCase
|
||||||
$this->assertTrue($result);
|
$this->assertTrue($result);
|
||||||
|
|
||||||
// Verify platform was created
|
// Verify platform was created
|
||||||
$stmt = $this->db->getConnection()->prepare('SELECT * FROM platform WHERE name = ?');
|
$stmt = $this->db->getConnection()->prepare('SELECT * FROM platforms WHERE name = ?');
|
||||||
$stmt->execute([$data['name']]);
|
$stmt->execute([$data['name']]);
|
||||||
$platform = $stmt->fetch(PDO::FETCH_ASSOC);
|
$platform = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ class PlatformTest extends TestCase
|
||||||
public function testGetPlatformDetails()
|
public function testGetPlatformDetails()
|
||||||
{
|
{
|
||||||
// Create test platform
|
// Create test platform
|
||||||
$stmt = $this->db->getConnection()->prepare('INSERT INTO platform (name, jitsi_url, jilo_database) VALUES (?, ?, ?)');
|
$stmt = $this->db->getConnection()->prepare('INSERT INTO platforms (name, jitsi_url, jilo_database) VALUES (?, ?, ?)');
|
||||||
$stmt->execute(['Test platform', 'https://jitsi.example.com', '/path/to/jilo.db']);
|
$stmt->execute(['Test platform', 'https://jitsi.example.com', '/path/to/jilo.db']);
|
||||||
$platformId = $this->db->getConnection()->lastInsertId();
|
$platformId = $this->db->getConnection()->lastInsertId();
|
||||||
|
|
||||||
|
@ -95,7 +95,7 @@ class PlatformTest extends TestCase
|
||||||
public function testEditPlatform()
|
public function testEditPlatform()
|
||||||
{
|
{
|
||||||
// Create test platform
|
// Create test platform
|
||||||
$stmt = $this->db->getConnection()->prepare('INSERT INTO platform (name, jitsi_url, jilo_database) VALUES (?, ?, ?)');
|
$stmt = $this->db->getConnection()->prepare('INSERT INTO platforms (name, jitsi_url, jilo_database) VALUES (?, ?, ?)');
|
||||||
$stmt->execute(['Test platform', 'https://jitsi.example.com', '/path/to/jilo.db']);
|
$stmt->execute(['Test platform', 'https://jitsi.example.com', '/path/to/jilo.db']);
|
||||||
$platformId = $this->db->getConnection()->lastInsertId();
|
$platformId = $this->db->getConnection()->lastInsertId();
|
||||||
|
|
||||||
|
@ -109,7 +109,7 @@ class PlatformTest extends TestCase
|
||||||
$this->assertTrue($result);
|
$this->assertTrue($result);
|
||||||
|
|
||||||
// Verify update
|
// Verify update
|
||||||
$stmt = $this->db->getConnection()->prepare('SELECT * FROM platform WHERE id = ?');
|
$stmt = $this->db->getConnection()->prepare('SELECT * FROM platforms WHERE id = ?');
|
||||||
$stmt->execute([$platformId]);
|
$stmt->execute([$platformId]);
|
||||||
$platform = $stmt->fetch(PDO::FETCH_ASSOC);
|
$platform = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
@ -120,36 +120,36 @@ class PlatformTest extends TestCase
|
||||||
public function testDeletePlatform()
|
public function testDeletePlatform()
|
||||||
{
|
{
|
||||||
// Create test platform
|
// Create test platform
|
||||||
$stmt = $this->db->getConnection()->prepare('INSERT INTO platform (name, jitsi_url, jilo_database) VALUES (?, ?, ?)');
|
$stmt = $this->db->getConnection()->prepare('INSERT INTO platforms (name, jitsi_url, jilo_database) VALUES (?, ?, ?)');
|
||||||
$stmt->execute(['Test platform', 'https://jitsi.example.com', '/path/to/jilo.db']);
|
$stmt->execute(['Test platform', 'https://jitsi.example.com', '/path/to/jilo.db']);
|
||||||
$platformId = $this->db->getConnection()->lastInsertId();
|
$platformId = $this->db->getConnection()->lastInsertId();
|
||||||
|
|
||||||
// Create test host
|
// Create test host
|
||||||
$stmt = $this->db->getConnection()->prepare('INSERT INTO host (platform_id, name) VALUES (?, ?)');
|
$stmt = $this->db->getConnection()->prepare('INSERT INTO hosts (platform_id, name) VALUES (?, ?)');
|
||||||
$stmt->execute([$platformId, 'Test host']);
|
$stmt->execute([$platformId, 'Test host']);
|
||||||
$hostId = $this->db->getConnection()->lastInsertId();
|
$hostId = $this->db->getConnection()->lastInsertId();
|
||||||
|
|
||||||
// Create test agent
|
// Create test agent
|
||||||
$stmt = $this->db->getConnection()->prepare('INSERT INTO jilo_agent (host_id) VALUES (?)');
|
$stmt = $this->db->getConnection()->prepare('INSERT INTO jilo_agents (host_id) VALUES (?)');
|
||||||
$stmt->execute([$hostId]);
|
$stmt->execute([$hostId]);
|
||||||
|
|
||||||
$result = $this->platform->deletePlatform($platformId);
|
$result = $this->platform->deletePlatform($platformId);
|
||||||
$this->assertTrue($result);
|
$this->assertTrue($result);
|
||||||
|
|
||||||
// Verify platform deletion
|
// Verify platform deletion
|
||||||
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM platform WHERE id = ?');
|
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM platforms WHERE id = ?');
|
||||||
$stmt->execute([$platformId]);
|
$stmt->execute([$platformId]);
|
||||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
$this->assertEquals(0, $result['count']);
|
$this->assertEquals(0, $result['count']);
|
||||||
|
|
||||||
// Verify host deletion
|
// Verify host deletion
|
||||||
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM host WHERE platform_id = ?');
|
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM hosts WHERE platform_id = ?');
|
||||||
$stmt->execute([$platformId]);
|
$stmt->execute([$platformId]);
|
||||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
$this->assertEquals(0, $result['count']);
|
$this->assertEquals(0, $result['count']);
|
||||||
|
|
||||||
// Verify agent deletion
|
// Verify agent deletion
|
||||||
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM jilo_agent WHERE host_id = ?');
|
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM jilo_agents WHERE host_id = ?');
|
||||||
$stmt->execute([$hostId]);
|
$stmt->execute([$hostId]);
|
||||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
$this->assertEquals(0, $result['count']);
|
$this->assertEquals(0, $result['count']);
|
||||||
|
@ -167,7 +167,7 @@ class PlatformTest extends TestCase
|
||||||
$this->assertTrue($result);
|
$this->assertTrue($result);
|
||||||
|
|
||||||
// Verify platform was created
|
// Verify platform was created
|
||||||
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM platform WHERE name = ?');
|
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM platforms WHERE name = ?');
|
||||||
$stmt->execute([$validData['name']]);
|
$stmt->execute([$validData['name']]);
|
||||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
$this->assertEquals(1, $result['count']);
|
$this->assertEquals(1, $result['count']);
|
||||||
|
@ -182,7 +182,7 @@ class PlatformTest extends TestCase
|
||||||
$this->assertIsString($result); // Should return error message
|
$this->assertIsString($result); // Should return error message
|
||||||
|
|
||||||
// Verify platform was not created
|
// Verify platform was not created
|
||||||
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM platform WHERE name = ?');
|
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM platforms WHERE name = ?');
|
||||||
$stmt->execute([$invalidData['name']]);
|
$stmt->execute([$invalidData['name']]);
|
||||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
$this->assertEquals(0, $result['count']);
|
$this->assertEquals(0, $result['count']);
|
||||||
|
@ -205,7 +205,7 @@ class PlatformTest extends TestCase
|
||||||
$this->assertTrue($result);
|
$this->assertTrue($result);
|
||||||
|
|
||||||
// Verify platform was created
|
// Verify platform was created
|
||||||
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM platform WHERE jilo_database = ?');
|
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM platforms WHERE jilo_database = ?');
|
||||||
$stmt->execute([$tempDb]);
|
$stmt->execute([$tempDb]);
|
||||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
$this->assertEquals(1, $result['count']);
|
$this->assertEquals(1, $result['count']);
|
||||||
|
|
|
@ -8,99 +8,103 @@ use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
class RateLimiterTest extends TestCase
|
class RateLimiterTest extends TestCase
|
||||||
{
|
{
|
||||||
private $db;
|
|
||||||
private $rateLimiter;
|
private $rateLimiter;
|
||||||
|
private $db;
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
// Prepare DB for Github CI
|
// Set up in-memory SQLite database
|
||||||
$host = defined('CI_DB_HOST') ? CI_DB_HOST : '127.0.0.1';
|
|
||||||
$password = defined('CI_DB_PASSWORD') ? CI_DB_PASSWORD : '';
|
|
||||||
|
|
||||||
// Set up test database
|
|
||||||
$this->db = new Database([
|
$this->db = new Database([
|
||||||
'type' => 'mariadb',
|
'type' => 'sqlite',
|
||||||
'host' => $host,
|
'dbFile' => ':memory:'
|
||||||
'port' => '3306',
|
|
||||||
'dbname' => 'jilo_test',
|
|
||||||
'user' => 'test_jilo',
|
|
||||||
'password' => $password
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// The RateLimiter constructor will create all necessary tables
|
|
||||||
$this->rateLimiter = new RateLimiter($this->db);
|
$this->rateLimiter = new RateLimiter($this->db);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function tearDown(): void
|
|
||||||
{
|
|
||||||
// Drop tables in correct order
|
|
||||||
$this->db->getConnection()->exec("DROP TABLE IF EXISTS {$this->rateLimiter->authRatelimitTable}");
|
|
||||||
$this->db->getConnection()->exec("DROP TABLE IF EXISTS {$this->rateLimiter->pagesRatelimitTable}");
|
|
||||||
$this->db->getConnection()->exec("DROP TABLE IF EXISTS {$this->rateLimiter->blacklistTable}");
|
|
||||||
$this->db->getConnection()->exec("DROP TABLE IF EXISTS {$this->rateLimiter->whitelistTable}");
|
|
||||||
parent::tearDown();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGetRecentAttempts()
|
public function testGetRecentAttempts()
|
||||||
{
|
{
|
||||||
$ip = '8.8.8.8';
|
$ip = '127.0.0.1';
|
||||||
|
$username = 'testuser';
|
||||||
|
|
||||||
// Record some login attempts
|
// Clean up any existing attempts first
|
||||||
$stmt = $this->db->getConnection()->prepare("INSERT INTO {$this->rateLimiter->authRatelimitTable}
|
$stmt = $this->db->getConnection()->prepare("DELETE FROM {$this->rateLimiter->authRatelimitTable} WHERE ip_address = ?");
|
||||||
(ip_address, username, attempted_at) VALUES (?, ?, NOW())");
|
$stmt->execute([$ip]);
|
||||||
|
|
||||||
// Add 3 attempts
|
|
||||||
for ($i = 0; $i < 3; $i++) {
|
|
||||||
$stmt->execute([$ip, 'testuser']);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Initially should have no attempts
|
||||||
$attempts = $this->rateLimiter->getRecentAttempts($ip);
|
$attempts = $this->rateLimiter->getRecentAttempts($ip);
|
||||||
$this->assertEquals(3, $attempts);
|
$this->assertEquals(0, $attempts);
|
||||||
|
|
||||||
|
// Add a login attempt
|
||||||
|
$stmt = $this->db->getConnection()->prepare("INSERT INTO {$this->rateLimiter->authRatelimitTable} (ip_address, username) VALUES (?, ?)");
|
||||||
|
$stmt->execute([$ip, $username]);
|
||||||
|
|
||||||
|
// Should now have 1 attempt
|
||||||
|
$attempts = $this->rateLimiter->getRecentAttempts($ip);
|
||||||
|
$this->assertEquals(1, $attempts);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testIsIpBlacklisted()
|
public function testIpBlacklisting()
|
||||||
{
|
{
|
||||||
$ip = '8.8.8.8';
|
$ip = '192.0.2.1'; // Using TEST-NET-1 range
|
||||||
|
|
||||||
|
// Should be blacklisted by default (TEST-NET-1 range)
|
||||||
|
$this->assertTrue($this->rateLimiter->isIpBlacklisted($ip));
|
||||||
|
|
||||||
|
// Test with non-blacklisted IP
|
||||||
|
$nonBlacklistedIp = '8.8.8.8'; // Google DNS
|
||||||
|
$this->assertFalse($this->rateLimiter->isIpBlacklisted($nonBlacklistedIp));
|
||||||
|
|
||||||
// Add IP to blacklist
|
// Add IP to blacklist
|
||||||
$stmt = $this->db->getConnection()->prepare("INSERT INTO {$this->rateLimiter->blacklistTable}
|
$stmt = $this->db->getConnection()->prepare("INSERT INTO {$this->rateLimiter->blacklistTable} (ip_address, reason) VALUES (?, ?)");
|
||||||
(ip_address, is_network, reason) VALUES (?, ?, ?)");
|
$stmt->execute([$nonBlacklistedIp, 'Test blacklist']);
|
||||||
$stmt->execute([$ip, 0, 'Test blacklist']); // Explicitly set is_network to 0 (false)
|
|
||||||
|
|
||||||
$this->assertTrue($this->rateLimiter->isIpBlacklisted($ip));
|
// IP should now be blacklisted
|
||||||
$this->assertFalse($this->rateLimiter->isIpBlacklisted('8.8.4.4'));
|
$this->assertTrue($this->rateLimiter->isIpBlacklisted($nonBlacklistedIp));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testIsIpWhitelisted()
|
public function testIpWhitelisting()
|
||||||
{
|
{
|
||||||
// Test with an IP that's not in the default whitelisted ranges
|
$ip = '127.0.0.1'; // Localhost
|
||||||
$ip = '8.8.8.8'; // Google's DNS, definitely not in private ranges
|
|
||||||
|
|
||||||
// Add IP to whitelist
|
// Clean up any existing whitelist entries
|
||||||
$stmt = $this->db->getConnection()->prepare("INSERT INTO {$this->rateLimiter->whitelistTable}
|
$stmt = $this->db->getConnection()->prepare("DELETE FROM {$this->rateLimiter->whitelistTable} WHERE ip_address = ?");
|
||||||
(ip_address, is_network, description) VALUES (?, ?, ?)");
|
$stmt->execute([$ip]);
|
||||||
$stmt->execute([$ip, 0, 'Test whitelist']); // Explicitly set is_network to 0 (false)
|
|
||||||
|
|
||||||
|
// Add to whitelist
|
||||||
|
$stmt = $this->db->getConnection()->prepare("INSERT INTO {$this->rateLimiter->whitelistTable} (ip_address, description) VALUES (?, ?)");
|
||||||
|
$stmt->execute([$ip, 'Test whitelist']);
|
||||||
|
|
||||||
|
// Should be whitelisted
|
||||||
$this->assertTrue($this->rateLimiter->isIpWhitelisted($ip));
|
$this->assertTrue($this->rateLimiter->isIpWhitelisted($ip));
|
||||||
$this->assertFalse($this->rateLimiter->isIpWhitelisted('8.8.4.4')); // Another IP not in private ranges
|
|
||||||
|
// Test with non-whitelisted IP
|
||||||
|
$nonWhitelistedIp = '8.8.8.8'; // Google DNS
|
||||||
|
$this->assertFalse($this->rateLimiter->isIpWhitelisted($nonWhitelistedIp));
|
||||||
|
|
||||||
|
// Add to whitelist
|
||||||
|
$stmt = $this->db->getConnection()->prepare("INSERT INTO {$this->rateLimiter->whitelistTable} (ip_address, description) VALUES (?, ?)");
|
||||||
|
$stmt->execute([$nonWhitelistedIp, 'Test whitelist']);
|
||||||
|
|
||||||
|
// Should now be whitelisted
|
||||||
|
$this->assertTrue($this->rateLimiter->isIpWhitelisted($nonWhitelistedIp));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRateLimitCheck()
|
public function testIpRangeBlacklisting()
|
||||||
{
|
{
|
||||||
$ip = '8.8.8.8'; // Use non-whitelisted IP
|
$ip = '8.8.8.8'; // Google DNS
|
||||||
$endpoint = '/test';
|
$networkIp = '8.8.8.0/24'; // Network containing Google DNS
|
||||||
|
|
||||||
// First request should be allowed
|
// Initially IP should not be blacklisted
|
||||||
$this->assertTrue($this->rateLimiter->isPageRequestAllowed($ip, $endpoint));
|
$this->assertFalse($this->rateLimiter->isIpBlacklisted($ip));
|
||||||
|
|
||||||
// Add requests up to the limit
|
// Add network to blacklist
|
||||||
for ($i = 0; $i < 60; $i++) { // Default limit is 60 per minute
|
$stmt = $this->db->getConnection()->prepare("INSERT INTO {$this->rateLimiter->blacklistTable} (ip_address, is_network, reason) VALUES (?, 1, ?)");
|
||||||
$this->rateLimiter->recordPageRequest($ip, $endpoint);
|
$stmt->execute([$networkIp, 'Test network blacklist']);
|
||||||
}
|
|
||||||
|
|
||||||
// The next request should be rate limited
|
// IP in range should now be blacklisted
|
||||||
$this->assertFalse($this->rateLimiter->isPageRequestAllowed($ip, $endpoint));
|
$this->assertTrue($this->rateLimiter->isIpBlacklisted($ip));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,197 +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();
|
|
||||||
|
|
||||||
// Prepare DB for Github CI
|
|
||||||
$host = defined('CI_DB_HOST') ? CI_DB_HOST : '127.0.0.1';
|
|
||||||
$password = defined('CI_DB_PASSWORD') ? CI_DB_PASSWORD : '';
|
|
||||||
|
|
||||||
$this->db = new Database([
|
|
||||||
'type' => 'mariadb',
|
|
||||||
'host' => $host,
|
|
||||||
'port' => '3306',
|
|
||||||
'dbname' => 'jilo_test',
|
|
||||||
'user' => 'test_jilo',
|
|
||||||
'password' => $password
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Create user table with MariaDB syntax
|
|
||||||
$this->db->getConnection()->exec("
|
|
||||||
CREATE TABLE IF NOT EXISTS user (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
username VARCHAR(255) NOT NULL UNIQUE,
|
|
||||||
password VARCHAR(255) NOT NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)
|
|
||||||
");
|
|
||||||
|
|
||||||
// Create user_meta table with MariaDB syntax
|
|
||||||
$this->db->getConnection()->exec("
|
|
||||||
CREATE TABLE IF NOT EXISTS user_meta (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
user_id INT NOT NULL,
|
|
||||||
name VARCHAR(255),
|
|
||||||
email VARCHAR(255),
|
|
||||||
timezone VARCHAR(100),
|
|
||||||
bio TEXT,
|
|
||||||
avatar VARCHAR(255),
|
|
||||||
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE
|
|
||||||
)
|
|
||||||
");
|
|
||||||
|
|
||||||
// Create security_rate_auth table for rate limiting
|
|
||||||
$this->db->getConnection()->exec("
|
|
||||||
CREATE TABLE IF NOT EXISTS security_rate_auth (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
ip_address VARCHAR(45) NOT NULL,
|
|
||||||
username VARCHAR(255) NOT NULL,
|
|
||||||
attempted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_ip_username (ip_address, username)
|
|
||||||
)
|
|
||||||
");
|
|
||||||
|
|
||||||
// Create user_2fa table for two-factor authentication
|
|
||||||
$this->db->getConnection()->exec("
|
|
||||||
CREATE TABLE IF NOT EXISTS user_2fa (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
user_id INT NOT NULL,
|
|
||||||
secret_key VARCHAR(255) NOT NULL,
|
|
||||||
backup_codes TEXT,
|
|
||||||
enabled TINYINT(1) NOT NULL DEFAULT 0,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE
|
|
||||||
)
|
|
||||||
");
|
|
||||||
|
|
||||||
$this->register = new Register($this->db);
|
|
||||||
$this->user = new User($this->db);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function tearDown(): void
|
|
||||||
{
|
|
||||||
// Drop tables in correct order
|
|
||||||
$this->db->getConnection()->exec("DROP TABLE IF EXISTS user_2fa");
|
|
||||||
$this->db->getConnection()->exec("DROP TABLE IF EXISTS security_rate_auth");
|
|
||||||
$this->db->getConnection()->exec("DROP TABLE IF EXISTS user_meta");
|
|
||||||
$this->db->getConnection()->exec("DROP TABLE IF EXISTS user");
|
|
||||||
parent::tearDown();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testRegister()
|
|
||||||
{
|
|
||||||
// Register a new user
|
|
||||||
$username = 'testuser';
|
|
||||||
$password = 'password123';
|
|
||||||
|
|
||||||
$result = $this->register->register($username, $password);
|
|
||||||
$this->assertTrue($result);
|
|
||||||
|
|
||||||
// Verify user was created
|
|
||||||
$stmt = $this->db->getConnection()->prepare("SELECT * FROM user WHERE username = ?");
|
|
||||||
$stmt->execute([$username]);
|
|
||||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
$this->assertNotNull($user);
|
|
||||||
$this->assertEquals($username, $user['username']);
|
|
||||||
$this->assertTrue(password_verify($password, $user['password']));
|
|
||||||
|
|
||||||
// Verify metadata was created
|
|
||||||
$stmt = $this->db->getConnection()->prepare("SELECT * FROM user_meta WHERE user_id = ?");
|
|
||||||
$stmt->execute([$user['id']]);
|
|
||||||
$meta = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
$this->assertNotNull($meta);
|
|
||||||
$this->assertEquals($user['id'], $meta['user_id']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testLogin()
|
|
||||||
{
|
|
||||||
// First register a user
|
|
||||||
$username = 'testuser';
|
|
||||||
$password = 'password123';
|
|
||||||
|
|
||||||
$this->register->register($username, $password);
|
|
||||||
|
|
||||||
// Mock $_SERVER['REMOTE_ADDR'] for rate limiter
|
|
||||||
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
|
|
||||||
|
|
||||||
// Test successful login
|
|
||||||
try {
|
|
||||||
$result = $this->user->login($username, $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('username', $_SESSION);
|
|
||||||
$this->assertArrayHasKey('CREATED', $_SESSION);
|
|
||||||
$this->assertArrayHasKey('LAST_ACTIVITY', $_SESSION);
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$this->fail('Login should not throw for valid credentials: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test failed login
|
|
||||||
$result = $this->user->login($username, 'wrongpassword');
|
|
||||||
$this->assertIsArray($result);
|
|
||||||
$this->assertEquals('failed', $result['status']);
|
|
||||||
$this->assertArrayHasKey('message', $result);
|
|
||||||
$this->assertStringContainsString('Invalid credentials', $result['message']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGetUserDetails()
|
|
||||||
{
|
|
||||||
// Register a test user first
|
|
||||||
$username = 'testuser';
|
|
||||||
$password = 'password123';
|
|
||||||
$result = $this->register->register($username, $password);
|
|
||||||
$this->assertTrue($result);
|
|
||||||
|
|
||||||
// Get user ID from database
|
|
||||||
$stmt = $this->db->getConnection()->prepare("SELECT id FROM user WHERE username = ?");
|
|
||||||
$stmt->execute([$username]);
|
|
||||||
$userId = $stmt->fetchColumn();
|
|
||||||
$this->assertNotFalse($userId);
|
|
||||||
|
|
||||||
// Insert user metadata
|
|
||||||
$stmt = $this->db->getConnection()->prepare("
|
|
||||||
UPDATE user_meta
|
|
||||||
SET name = ?, email = ?
|
|
||||||
WHERE user_id = ?
|
|
||||||
");
|
|
||||||
$stmt->execute(['Test User', 'test@example.com', $userId]);
|
|
||||||
|
|
||||||
// Get user details
|
|
||||||
$userDetails = $this->user->getUserDetails($userId);
|
|
||||||
|
|
||||||
$this->assertIsArray($userDetails);
|
|
||||||
$this->assertNotEmpty($userDetails);
|
|
||||||
$this->assertArrayHasKey(0, $userDetails, 'User details should be returned as an array');
|
|
||||||
|
|
||||||
// Get first row since we're querying by primary key
|
|
||||||
$userDetails = $userDetails[0];
|
|
||||||
|
|
||||||
$this->assertArrayHasKey('username', $userDetails, 'User details should include username');
|
|
||||||
$this->assertArrayHasKey('name', $userDetails, 'User details should include name');
|
|
||||||
$this->assertArrayHasKey('email', $userDetails, 'User details should include email');
|
|
||||||
|
|
||||||
// Verify values
|
|
||||||
$this->assertEquals($username, $userDetails['username'], 'Username should match');
|
|
||||||
$this->assertEquals('Test User', $userDetails['name'], 'Name should match');
|
|
||||||
$this->assertEquals('test@example.com', $userDetails['email'], 'Email should match');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
require_once dirname(__DIR__, 3) . '/app/classes/database.php';
|
require_once dirname(__DIR__, 3) . '/app/classes/database.php';
|
||||||
require_once dirname(__DIR__, 3) . '/app/classes/user.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';
|
require_once dirname(__DIR__, 3) . '/app/classes/ratelimiter.php';
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
@ -10,161 +9,175 @@ use PHPUnit\Framework\TestCase;
|
||||||
class UserTest extends TestCase
|
class UserTest extends TestCase
|
||||||
{
|
{
|
||||||
private $db;
|
private $db;
|
||||||
private $register;
|
|
||||||
private $user;
|
private $user;
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
// Prepare DB for Github CI
|
// Set up test database
|
||||||
$host = defined('CI_DB_HOST') ? CI_DB_HOST : '127.0.0.1';
|
|
||||||
$password = defined('CI_DB_PASSWORD') ? CI_DB_PASSWORD : '';
|
|
||||||
|
|
||||||
$this->db = new Database([
|
$this->db = new Database([
|
||||||
'type' => 'mariadb',
|
'type' => 'sqlite',
|
||||||
'host' => $host,
|
'dbFile' => ':memory:'
|
||||||
'port' => '3306',
|
|
||||||
'dbname' => 'jilo_test',
|
|
||||||
'user' => 'test_jilo',
|
|
||||||
'password' => $password
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Create user table with MariaDB syntax
|
// Create users table
|
||||||
$this->db->getConnection()->exec("
|
$this->db->getConnection()->exec("
|
||||||
CREATE TABLE IF NOT EXISTS user (
|
CREATE TABLE users (
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
username VARCHAR(255) NOT NULL UNIQUE,
|
username TEXT NOT NULL UNIQUE,
|
||||||
password VARCHAR(255) NOT NULL,
|
password TEXT NOT NULL
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)
|
)
|
||||||
");
|
");
|
||||||
|
|
||||||
// Create user_meta table with MariaDB syntax
|
// Create users_meta table
|
||||||
$this->db->getConnection()->exec("
|
$this->db->getConnection()->exec("
|
||||||
CREATE TABLE IF NOT EXISTS user_meta (
|
CREATE TABLE users_meta (
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
user_id INT NOT NULL,
|
user_id INTEGER NOT NULL,
|
||||||
name VARCHAR(255),
|
name TEXT,
|
||||||
email VARCHAR(255),
|
email TEXT,
|
||||||
timezone VARCHAR(100),
|
timezone TEXT,
|
||||||
bio TEXT,
|
bio TEXT,
|
||||||
avatar VARCHAR(255),
|
avatar TEXT,
|
||||||
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
)
|
|
||||||
");
|
|
||||||
|
|
||||||
// Create security_rate_auth table for rate limiting
|
|
||||||
$this->db->getConnection()->exec("
|
|
||||||
CREATE TABLE IF NOT EXISTS security_rate_auth (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
ip_address VARCHAR(45) NOT NULL,
|
|
||||||
username VARCHAR(255) NOT NULL,
|
|
||||||
attempted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_ip_username (ip_address, username)
|
|
||||||
)
|
)
|
||||||
");
|
");
|
||||||
|
|
||||||
// Create user_2fa table for two-factor authentication
|
// Create user_2fa table for two-factor authentication
|
||||||
$this->db->getConnection()->exec("
|
$this->db->getConnection()->exec("
|
||||||
CREATE TABLE IF NOT EXISTS user_2fa (
|
CREATE TABLE user_2fa (
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
user_id INT NOT NULL,
|
user_id INTEGER NOT NULL,
|
||||||
secret_key VARCHAR(255) NOT NULL,
|
secret_key TEXT NOT NULL,
|
||||||
backup_codes TEXT,
|
backup_codes TEXT,
|
||||||
enabled TINYINT(1) NOT NULL DEFAULT 0,
|
enabled TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE
|
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->user = new User($this->db);
|
$this->user = new User($this->db);
|
||||||
$this->register = new Register($this->db);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function tearDown(): void
|
public function testRegister()
|
||||||
{
|
{
|
||||||
// Drop tables in correct order
|
$result = $this->user->register('testuser', 'password123');
|
||||||
$this->db->getConnection()->exec("DROP TABLE IF EXISTS user_2fa");
|
$this->assertTrue($result);
|
||||||
$this->db->getConnection()->exec("DROP TABLE IF EXISTS security_rate_auth");
|
|
||||||
$this->db->getConnection()->exec("DROP TABLE IF EXISTS user_meta");
|
// Verify user was created
|
||||||
$this->db->getConnection()->exec("DROP TABLE IF EXISTS user");
|
$stmt = $this->db->getConnection()->prepare('SELECT * FROM users WHERE username = ?');
|
||||||
parent::tearDown();
|
$stmt->execute(['testuser']);
|
||||||
|
$user = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$this->assertEquals('testuser', $user['username']);
|
||||||
|
$this->assertTrue(password_verify('password123', $user['password']));
|
||||||
|
|
||||||
|
// Verify user_meta was created
|
||||||
|
$stmt = $this->db->getConnection()->prepare('SELECT * FROM users_meta WHERE user_id = ?');
|
||||||
|
$stmt->execute([$user['id']]);
|
||||||
|
$meta = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$this->assertNotNull($meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testLogin()
|
public function testLogin()
|
||||||
{
|
{
|
||||||
// First register a user
|
// Create a test user
|
||||||
$username = 'testuser';
|
|
||||||
$password = 'password123';
|
$password = 'password123';
|
||||||
|
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
|
||||||
$this->register->register($username, $password);
|
$stmt = $this->db->getConnection()->prepare('INSERT INTO users (username, password) VALUES (?, ?)');
|
||||||
|
$stmt->execute(['testuser', $hashedPassword]);
|
||||||
|
|
||||||
// Mock $_SERVER['REMOTE_ADDR'] for rate limiter
|
// Mock $_SERVER['REMOTE_ADDR'] for rate limiter
|
||||||
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
|
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
|
||||||
|
|
||||||
// Test successful login
|
// Test successful login
|
||||||
try {
|
try {
|
||||||
$result = $this->user->login($username, $password);
|
$result = $this->user->login('testuser', $password);
|
||||||
$this->assertIsArray($result);
|
$this->assertIsArray($result);
|
||||||
$this->assertEquals('success', $result['status']);
|
$this->assertEquals('success', $result['status']);
|
||||||
$this->assertArrayHasKey('user_id', $result);
|
$this->assertArrayHasKey('user_id', $result);
|
||||||
$this->assertArrayHasKey('username', $result);
|
$this->assertArrayHasKey('username', $result);
|
||||||
$this->assertArrayHasKey('user_id', $_SESSION);
|
$this->assertArrayHasKey('user_id', $_SESSION);
|
||||||
$this->assertArrayHasKey('username', $_SESSION);
|
|
||||||
$this->assertArrayHasKey('CREATED', $_SESSION);
|
$this->assertArrayHasKey('CREATED', $_SESSION);
|
||||||
$this->assertArrayHasKey('LAST_ACTIVITY', $_SESSION);
|
$this->assertArrayHasKey('LAST_ACTIVITY', $_SESSION);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$this->fail('Login should not throw for valid credentials: ' . $e->getMessage());
|
$this->fail('Login should not throw an exception for valid credentials: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test failed login
|
// Test failed login
|
||||||
$result = $this->user->login($username, 'wrongpassword');
|
try {
|
||||||
$this->assertIsArray($result);
|
$this->user->login('testuser', 'wrongpassword');
|
||||||
$this->assertEquals('failed', $result['status']);
|
$this->fail('Login should throw an exception for invalid credentials');
|
||||||
$this->assertArrayHasKey('message', $result);
|
} catch (Exception $e) {
|
||||||
$this->assertStringContainsString('Invalid credentials', $result['message']);
|
$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()
|
public function testGetUserDetails()
|
||||||
{
|
{
|
||||||
// Register a test user first
|
// Create a test user
|
||||||
$username = 'testuser';
|
$stmt = $this->db->getConnection()->prepare('INSERT INTO users (username, password) VALUES (?, ?)');
|
||||||
$password = 'password123';
|
$stmt->execute(['testuser', 'hashedpassword']);
|
||||||
$result = $this->register->register($username, $password);
|
$userId = $this->db->getConnection()->lastInsertId();
|
||||||
$this->assertTrue($result);
|
|
||||||
|
|
||||||
// Get user ID from database
|
// Create user meta with some data
|
||||||
$stmt = $this->db->getConnection()->prepare("SELECT id FROM user WHERE username = ?");
|
$stmt = $this->db->getConnection()->prepare('INSERT INTO users_meta (user_id, name, email) VALUES (?, ?, ?)');
|
||||||
$stmt->execute([$username]);
|
$stmt->execute([$userId, 'Test User', 'test@example.com']);
|
||||||
$userId = $stmt->fetchColumn();
|
|
||||||
$this->assertNotFalse($userId);
|
|
||||||
|
|
||||||
// Insert user metadata
|
|
||||||
$stmt = $this->db->getConnection()->prepare("
|
|
||||||
UPDATE user_meta
|
|
||||||
SET name = ?, email = ?
|
|
||||||
WHERE user_id = ?
|
|
||||||
");
|
|
||||||
$stmt->execute(['Test User', 'test@example.com', $userId]);
|
|
||||||
|
|
||||||
// Get user details
|
|
||||||
$userDetails = $this->user->getUserDetails($userId);
|
$userDetails = $this->user->getUserDetails($userId);
|
||||||
|
|
||||||
$this->assertIsArray($userDetails);
|
$this->assertIsArray($userDetails);
|
||||||
$this->assertNotEmpty($userDetails);
|
$this->assertCount(1, $userDetails); // Should return one row
|
||||||
$this->assertArrayHasKey(0, $userDetails, 'User details should be returned as an array');
|
$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']);
|
||||||
|
|
||||||
// Get first row since we're querying by primary key
|
// Test nonexistent user
|
||||||
$userDetails = $userDetails[0];
|
$userDetails = $this->user->getUserDetails(999);
|
||||||
|
$this->assertEmpty($userDetails);
|
||||||
$this->assertArrayHasKey('username', $userDetails, 'User details should include username');
|
|
||||||
$this->assertArrayHasKey('name', $userDetails, 'User details should include name');
|
|
||||||
$this->assertArrayHasKey('email', $userDetails, 'User details should include email');
|
|
||||||
|
|
||||||
// Verify values
|
|
||||||
$this->assertEquals($username, $userDetails['username'], 'Username should match');
|
|
||||||
$this->assertEquals('Test User', $userDetails['name'], 'Name should match');
|
|
||||||
$this->assertEquals('test@example.com', $userDetails['email'], 'Email should match');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,19 +12,9 @@ if (!headers_sent()) {
|
||||||
ini_set('session.gc_maxlifetime', 1440); // 24 minutes
|
ini_set('session.gc_maxlifetime', 1440); // 24 minutes
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load plugin Log model and IP helper early so fallback wrapper is bypassed
|
|
||||||
require_once __DIR__ . '/../app/helpers/ip_helper.php';
|
|
||||||
|
|
||||||
// Initialize global user_IP for tests
|
|
||||||
global $user_IP;
|
|
||||||
$user_IP = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
|
||||||
|
|
||||||
// Load Composer's autoloader
|
// Load Composer's autoloader
|
||||||
require_once __DIR__ . '/vendor/autoload.php';
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
// Ensure core NullLogger is available during tests
|
|
||||||
require_once __DIR__ . '/../app/core/NullLogger.php';
|
|
||||||
|
|
||||||
// Set error reporting
|
// Set error reporting
|
||||||
error_reporting(E_ALL);
|
error_reporting(E_ALL);
|
||||||
ini_set('display_errors', 1);
|
ini_set('display_errors', 1);
|
||||||
|
@ -36,23 +26,18 @@ date_default_timezone_set('UTC');
|
||||||
// Define global variables needed by the application
|
// Define global variables needed by the application
|
||||||
$GLOBALS['app_root'] = '/';
|
$GLOBALS['app_root'] = '/';
|
||||||
$GLOBALS['config'] = [
|
$GLOBALS['config'] = [
|
||||||
'db_type' => 'mariadb',
|
'db' => [
|
||||||
'sql' => [
|
'type' => 'sqlite',
|
||||||
'sql_host' => 'localhost',
|
'dbFile' => ':memory:'
|
||||||
'sql_port' => '3306',
|
]
|
||||||
'sql_database' => 'jilo_test',
|
|
||||||
'sql_username' => 'test_jilo',
|
|
||||||
'sql_password' => '',
|
|
||||||
],
|
|
||||||
'environment' => 'testing'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Define global connectDB function
|
// Define global connectDB function
|
||||||
if (!function_exists('connectDB')) {
|
if (!function_exists('connectDB')) {
|
||||||
function connectDB($config) {
|
function connectDB($config) {
|
||||||
global $db;
|
global $dbWeb;
|
||||||
return [
|
return [
|
||||||
'db' => $db
|
'db' => $dbWeb
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,11 +26,7 @@
|
||||||
</coverage>
|
</coverage>
|
||||||
<php>
|
<php>
|
||||||
<env name="APP_ENV" value="testing"/>
|
<env name="APP_ENV" value="testing"/>
|
||||||
<env name="DB_TYPE" value="mariadb"/>
|
<env name="DB_CONNECTION" value="sqlite"/>
|
||||||
<env name="DB_HOST" value="localhost"/>
|
<env name="DB_DATABASE" value=":memory:"/>
|
||||||
<env name="DB_PORT" value="3306"/>
|
|
||||||
<env name="DB_DATABASE" value="jilo_test"/>
|
|
||||||
<env name="DB_USERNAME" value="root"/>
|
|
||||||
<env name="DB_PASSWORD" value=""/>
|
|
||||||
</php>
|
</php>
|
||||||
</phpunit>
|
</phpunit>
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
-- Create test database if not exists
|
|
||||||
CREATE DATABASE IF NOT EXISTS jilo_test;
|
|
||||||
USE jilo_test;
|
|
||||||
|
|
||||||
-- Create rate limiter table if not exists
|
|
||||||
CREATE TABLE IF NOT EXISTS security_rate_page (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
ip_address VARCHAR(45) NOT NULL,
|
|
||||||
endpoint VARCHAR(255) NOT NULL,
|
|
||||||
request_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Create security_ip_whitelist table if not exists
|
|
||||||
CREATE TABLE IF NOT EXISTS security_ip_whitelist (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
ip_address VARCHAR(45) NOT NULL UNIQUE,
|
|
||||||
is_network BOOLEAN DEFAULT 0,
|
|
||||||
description TEXT,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
created_by VARCHAR(255)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Create security_ip_blacklist table if not exists
|
|
||||||
CREATE TABLE IF NOT EXISTS security_ip_blacklist (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
ip_address VARCHAR(45) NOT NULL UNIQUE,
|
|
||||||
is_network BOOLEAN DEFAULT 0,
|
|
||||||
reason TEXT,
|
|
||||||
expiry_time TIMESTAMP NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
created_by VARCHAR(255)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Grant permissions to root user
|
|
||||||
GRANT ALL PRIVILEGES ON jilo_test.* TO 'root'@'localhost';
|
|
|
@ -1,12 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
return [
|
|
||||||
'name' => 'Modern theme',
|
|
||||||
'description' => 'Example theme. A modern, clean theme for Jilo Web.',
|
|
||||||
'version' => '1.0.0',
|
|
||||||
'author' => 'Lindeas Inc.',
|
|
||||||
'screenshot' => 'screenshot.png',
|
|
||||||
'options' => [
|
|
||||||
// Theme-specific options can be defined here
|
|
||||||
]
|
|
||||||
];
|
|
|
@ -1,180 +0,0 @@
|
||||||
/* Modern Theme Styles */
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--primary-color: #4361ee;
|
|
||||||
--secondary-color: #3f37c9;
|
|
||||||
--accent-color: #4895ef;
|
|
||||||
--light-color: #f8f9fa;
|
|
||||||
--dark-color: #212529;
|
|
||||||
--success-color: #4bb543;
|
|
||||||
--warning-color: #f9c74f;
|
|
||||||
--danger-color: #ef476f;
|
|
||||||
--border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* General Styles */
|
|
||||||
body.modern-theme {
|
|
||||||
background-color: #f5f7fa;
|
|
||||||
color: #333;
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navigation */
|
|
||||||
.menu-container {
|
|
||||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Cards */
|
|
||||||
.card {
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.05);
|
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
background-color: white;
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
.btn-primary {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
padding: 0.375rem 1rem;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background-color: var(--secondary-color);
|
|
||||||
border-color: var(--secondary-color);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Forms */
|
|
||||||
.form-control {
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
border: 1px solid #e1e5eb;
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control:focus {
|
|
||||||
border-color: var(--accent-color);
|
|
||||||
box-shadow: 0 0 0 0.2rem rgba(67, 97, 238, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tables */
|
|
||||||
.table {
|
|
||||||
background-color: white;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table thead th {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
border-bottom: 2px solid #e9ecef;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Alerts */
|
|
||||||
.alert {
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
border: none;
|
|
||||||
padding: 1rem 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-success {
|
|
||||||
background-color: rgba(75, 181, 67, 0.1);
|
|
||||||
color: var(--success-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-warning {
|
|
||||||
background-color: rgba(249, 199, 79, 0.1);
|
|
||||||
color: #c9a227;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-danger {
|
|
||||||
background-color: rgba(239, 71, 111, 0.1);
|
|
||||||
color: var(--danger-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Theme Switcher */
|
|
||||||
.theme-switcher {
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-switcher .dropdown-toggle {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
border: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: white;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-switcher .dropdown-toggle:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-option {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-option:hover {
|
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-preview {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-right: 10px;
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive adjustments */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.menu-container {
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-left, .menu-right {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0.25rem 0;
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue