From 6d1f934d369c32e66cfbadda5079398dc2710ae6 Mon Sep 17 00:00:00 2001 From: Pinga <121483313+getpinga@users.noreply.github.com> Date: Fri, 5 Jan 2024 14:18:40 +0200 Subject: [PATCH] Added user audit details in panel --- automation/audit.json | 4 ++ cp/app/Auth/Auth.php | 17 +++++ cp/app/Controllers/Auth/AuthController.php | 38 ++++++++++- cp/app/Controllers/ProfileController.php | 25 ++++++- cp/bootstrap/helper.php | 29 +++++++++ cp/resources/views/admin/profile/profile.twig | 65 +++++++++++++++---- database/registry.mariadb.sql | 22 +++++-- database/registry.postgres.sql | 22 +++++-- 8 files changed, 197 insertions(+), 25 deletions(-) diff --git a/automation/audit.json b/automation/audit.json index bd39c78..e44ecaa 100644 --- a/automation/audit.json +++ b/automation/audit.json @@ -204,6 +204,10 @@ "audit": true, "skip": null }, + "users_audit": { + "audit": true, + "skip": null + }, "users_confirmations": { "audit": null, "skip": null diff --git a/cp/app/Auth/Auth.php b/cp/app/Auth/Auth.php index 3017b7a..ae1643d 100644 --- a/cp/app/Auth/Auth.php +++ b/cp/app/Auth/Auth.php @@ -277,6 +277,23 @@ class Auth public static function changeCurrentPassword($oldPassword, $newPassword){ $auth = self::$auth; try { + global $container; + $db = $container->get('db'); + $currentDateTime = new \DateTime(); + $currentDate = $currentDateTime->format('Y-m-d H:i:s.v'); // Current timestamp + $db->insert( + 'users_audit', + [ + 'user_id' => $_SESSION['auth_user_id'], + 'user_event' => 'user.update.password', + 'user_resource' => 'control.panel', + 'user_agent' => $_SERVER['HTTP_USER_AGENT'], + 'user_ip' => get_client_ip(), + 'user_location' => get_client_location(), + 'event_time' => $currentDate, + 'user_data' => null + ] + ); $auth->changePassword($oldPassword, $newPassword); redirect()->route('profile')->with('success','Password has been changed'); } diff --git a/cp/app/Controllers/Auth/AuthController.php b/cp/app/Controllers/Auth/AuthController.php index 916d919..b4e3a2e 100644 --- a/cp/app/Controllers/Auth/AuthController.php +++ b/cp/app/Controllers/Auth/AuthController.php @@ -59,6 +59,8 @@ class AuthController extends Controller * @throws \Pinga\Auth\AuthError */ public function login(Request $request, Response $response){ + global $container; + $data = $request->getParsedBody(); if(isset($data['remember'])){ $remember = $data['remember']; @@ -71,8 +73,25 @@ class AuthController extends Controller $code = null; } $login = Auth::login($data['email'], $data['password'], $remember, $code); - if($login===true) + if($login===true) { + $db = $container->get('db'); + $currentDateTime = new \DateTime(); + $currentDate = $currentDateTime->format('Y-m-d H:i:s.v'); // Current timestamp + $db->insert( + 'users_audit', + [ + 'user_id' => $_SESSION['auth_user_id'], + 'user_event' => 'user.login', + 'user_resource' => 'control.panel', + 'user_agent' => $_SERVER['HTTP_USER_AGENT'], + 'user_ip' => get_client_ip(), + 'user_location' => get_client_location(), + 'event_time' => $currentDate, + 'user_data' => null + ] + ); redirect()->route('home'); + } } /** @@ -80,6 +99,23 @@ class AuthController extends Controller */ public function logout() { + global $container; + $db = $container->get('db'); + $currentDateTime = new \DateTime(); + $currentDate = $currentDateTime->format('Y-m-d H:i:s.v'); // Current timestamp + $db->insert( + 'users_audit', + [ + 'user_id' => $_SESSION['auth_user_id'], + 'user_event' => 'user.logout', + 'user_resource' => 'control.panel', + 'user_agent' => $_SERVER['HTTP_USER_AGENT'], + 'user_ip' => get_client_ip(), + 'user_location' => get_client_location(), + 'event_time' => $currentDate, + 'user_data' => null + ] + ); Auth::logout(); redirect()->route('login'); } diff --git a/cp/app/Controllers/ProfileController.php b/cp/app/Controllers/ProfileController.php index dad0ad8..fd2c1b6 100644 --- a/cp/app/Controllers/ProfileController.php +++ b/cp/app/Controllers/ProfileController.php @@ -73,12 +73,16 @@ class ProfileController extends Controller 'SELECT * FROM users_webauthn WHERE user_id = ?', [$userId] ); + $user_audit = $db->select( + 'SELECT * FROM users_audit WHERE user_id = ? ORDER BY event_time DESC', + [$userId] + ); if ($is_2fa_activated) { - return view($response,'admin/profile/profile.twig',['email' => $email, 'username' => $username, 'status' => $status, 'role' => $role, 'csrf_name' => $csrfName, 'csrf_value' => $csrfValue]); + return view($response,'admin/profile/profile.twig',['email' => $email, 'username' => $username, 'status' => $status, 'role' => $role, 'csrf_name' => $csrfName, 'csrf_value' => $csrfValue, 'userAudit' => $user_audit]); } else if ($is_weba_activated) { - return view($response,'admin/profile/profile.twig',['email' => $email, 'username' => $username, 'status' => $status, 'role' => $role, 'qrcodeDataUri' => $qrcodeDataUri, 'secret' => $secret, 'csrf_name' => $csrfName, 'csrf_value' => $csrfValue, 'weba' => $is_weba_activated]); + return view($response,'admin/profile/profile.twig',['email' => $email, 'username' => $username, 'status' => $status, 'role' => $role, 'qrcodeDataUri' => $qrcodeDataUri, 'secret' => $secret, 'csrf_name' => $csrfName, 'csrf_value' => $csrfValue, 'weba' => $is_weba_activated, 'userAudit' => $user_audit]); } else { - return view($response,'admin/profile/profile.twig',['email' => $email, 'username' => $username, 'status' => $status, 'role' => $role, 'qrcodeDataUri' => $qrcodeDataUri, 'secret' => $secret, 'csrf_name' => $csrfName, 'csrf_value' => $csrfValue]); + return view($response,'admin/profile/profile.twig',['email' => $email, 'username' => $username, 'status' => $status, 'role' => $role, 'qrcodeDataUri' => $qrcodeDataUri, 'secret' => $secret, 'csrf_name' => $csrfName, 'csrf_value' => $csrfValue, 'userAudit' => $user_audit]); } } @@ -114,6 +118,21 @@ class ProfileController extends Controller } try { + $currentDateTime = new \DateTime(); + $currentDate = $currentDateTime->format('Y-m-d H:i:s.v'); // Current timestamp + $db->insert( + 'users_audit', + [ + 'user_id' => $_SESSION['auth_user_id'], + 'user_event' => 'user.enable.2fa', + 'user_resource' => 'control.panel', + 'user_agent' => $_SERVER['HTTP_USER_AGENT'], + 'user_ip' => get_client_ip(), + 'user_location' => get_client_location(), + 'event_time' => $currentDate, + 'user_data' => null + ] + ); $db->update( 'users', [ diff --git a/cp/bootstrap/helper.php b/cp/bootstrap/helper.php index cf4fde0..6078694 100644 --- a/cp/bootstrap/helper.php +++ b/cp/bootstrap/helper.php @@ -396,4 +396,33 @@ function createUuidFromId($id) { // Handle exception return null; } +} + +// Function to get the client IP address +function get_client_ip() { + $ipaddress = ''; + if (getenv('HTTP_CLIENT_IP')) + $ipaddress = getenv('HTTP_CLIENT_IP'); + else if(getenv('HTTP_X_FORWARDED_FOR')) + $ipaddress = getenv('HTTP_X_FORWARDED_FOR'); + else if(getenv('HTTP_X_FORWARDED')) + $ipaddress = getenv('HTTP_X_FORWARDED'); + else if(getenv('HTTP_FORWARDED_FOR')) + $ipaddress = getenv('HTTP_FORWARDED_FOR'); + else if(getenv('HTTP_FORWARDED')) + $ipaddress = getenv('HTTP_FORWARDED'); + else if(getenv('REMOTE_ADDR')) + $ipaddress = getenv('REMOTE_ADDR'); + else + $ipaddress = 'UNKNOWN'; + return $ipaddress; +} + +function get_client_location() { + $PublicIP = get_client_ip(); + $json = file_get_contents("http://ipinfo.io/$PublicIP/geo"); + $json = json_decode($json, true); + $country = $json['country']; + + return $country; } \ No newline at end of file diff --git a/cp/resources/views/admin/profile/profile.twig b/cp/resources/views/admin/profile/profile.twig index f9f336d..bab419d 100644 --- a/cp/resources/views/admin/profile/profile.twig +++ b/cp/resources/views/admin/profile/profile.twig @@ -36,6 +36,9 @@ +
@@ -158,19 +161,55 @@ - {% for device in weba %} - - {{ device.user_agent }} - {{ device.created_at }} - - Edit - - - {% else %} - - No devices found. - - {% endfor %} + {% for device in weba %} + + {{ device.user_agent }} + {{ device.created_at }} + + Edit + + + {% else %} + + No devices found. + + {% endfor %} + + +
+ + + +
+

User Audit Log

+
+
+

Track and review all user activities in your account below. Monitor logins, profile changes, and other key actions to ensure security and transparency.

+
+ + + + + + + + + + + + {% for user in userAudit %} + + + + + + + + {% else %} + + + + {% endfor %}
EventUser AgentIPLocationTimestamp
{{ user.user_event }}{{ user.user_agent }}{{ user.user_ip }}{{ user.user_location }}{{ user.event_time }}
No log data for user.
diff --git a/database/registry.mariadb.sql b/database/registry.mariadb.sql index 5bce1b5..fd4a65c 100644 --- a/database/registry.mariadb.sql +++ b/database/registry.mariadb.sql @@ -601,6 +601,20 @@ CREATE TABLE IF NOT EXISTS `registry`.`users` ( UNIQUE KEY `email` (`email`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Panel Users'; +CREATE TABLE IF NOT EXISTS `registry`.`users_audit` ( + `user_id` int(10) unsigned NOT NULL, + `user_event` VARCHAR(255) NOT NULL, + `user_resource` VARCHAR(255) default NULL, + `user_agent` VARCHAR(255) NOT NULL, + `user_ip` VARCHAR(45) NOT NULL, + `user_location` VARCHAR(45) default NULL, + `event_time` DATETIME(3) NOT NULL, + `user_data` JSON default NULL, + KEY `user_id` (`user_id`), + KEY `user_event` (`user_event`), + KEY `user_ip` (`user_ip`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Panel User Audit'; + CREATE TABLE IF NOT EXISTS `registry`.`users_confirmations` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `user_id` int(10) unsigned NOT NULL, @@ -616,24 +630,24 @@ CREATE TABLE IF NOT EXISTS `registry`.`users_confirmations` ( CREATE TABLE IF NOT EXISTS `registry`.`users_remembered` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, - `user` int(10) unsigned NOT NULL, + `user_id` int(10) unsigned NOT NULL, `selector` varchar(24) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL, `token` varchar(255) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL, `expires` int(10) unsigned NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `selector` (`selector`), - KEY `user` (`user`) + KEY `user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Panel Users Remember'; CREATE TABLE IF NOT EXISTS `registry`.`users_resets` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, - `user` int(10) unsigned NOT NULL, + `user_id` int(10) unsigned NOT NULL, `selector` varchar(20) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL, `token` varchar(255) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL, `expires` int(10) unsigned NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `selector` (`selector`), - KEY `user_expires` (`user`,`expires`) + KEY `user_expires` (`user_id`,`expires`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Panel Users Reset'; CREATE TABLE IF NOT EXISTS `registry`.`users_throttling` ( diff --git a/database/registry.postgres.sql b/database/registry.postgres.sql index e5f01ad..4b9b5dd 100644 --- a/database/registry.postgres.sql +++ b/database/registry.postgres.sql @@ -573,6 +573,20 @@ CREATE TABLE IF NOT EXISTS registry.users ( "backup_codes" TEXT, ); +CREATE TABLE IF NOT EXISTS registry.users_audit ( + "user_id" SERIAL PRIMARY KEY CHECK ("id" >= 0), + "user_event" VARCHAR(255) NOT NULL, + "user_resource" VARCHAR(255) DEFAULT NULL, + "user_agent" VARCHAR(255) NOT NULL, + "user_ip" VARCHAR(45) NOT NULL, + "user_location" VARCHAR(45) DEFAULT NULL, + "event_time" TIMESTAMP(3) NOT NULL, + "user_data" JSONB DEFAULT NULL, + CONSTRAINT pk_users_audit PRIMARY KEY (user_id) +); +CREATE INDEX idx_user_event ON registry.users_audit (user_event); +CREATE INDEX idx_user_ip ON registry.users_audit (user_ip); + CREATE TABLE IF NOT EXISTS registry.users_confirmations ( "id" SERIAL PRIMARY KEY CHECK ("id" >= 0), "user_id" INTEGER NOT NULL CHECK ("user_id" >= 0), @@ -586,21 +600,21 @@ CREATE INDEX IF NOT EXISTS "user_id" ON registry.users_confirmations ("user_id") CREATE TABLE IF NOT EXISTS registry.users_remembered ( "id" BIGSERIAL PRIMARY KEY CHECK ("id" >= 0), - "user" INTEGER NOT NULL CHECK ("user" >= 0), + "user_id" INTEGER NOT NULL CHECK ("user_id" >= 0), "selector" VARCHAR(24) UNIQUE NOT NULL, "token" VARCHAR(255) NOT NULL, "expires" INTEGER NOT NULL CHECK ("expires" >= 0) ); -CREATE INDEX IF NOT EXISTS "user" ON registry.users_remembered ("user"); +CREATE INDEX IF NOT EXISTS "user_id" ON registry.users_remembered ("user_id"); CREATE TABLE IF NOT EXISTS registry.users_resets ( "id" BIGSERIAL PRIMARY KEY CHECK ("id" >= 0), - "user" INTEGER NOT NULL CHECK ("user" >= 0), + "user_id" INTEGER NOT NULL CHECK ("user_id" >= 0), "selector" VARCHAR(20) UNIQUE NOT NULL, "token" VARCHAR(255) NOT NULL, "expires" INTEGER NOT NULL CHECK ("expires" >= 0) ); -CREATE INDEX IF NOT EXISTS "user_expires" ON registry.users_resets ("user", "expires"); +CREATE INDEX IF NOT EXISTS "user_expires" ON registry.users_resets ("user_id", "expires"); CREATE TABLE IF NOT EXISTS registry.users_throttling ( "bucket" VARCHAR(44) PRIMARY KEY,