diff --git a/application/controllers/dashboard_mcu/User.php b/application/controllers/dashboard_mcu/User.php new file mode 100644 index 0000000..8054864 --- /dev/null +++ b/application/controllers/dashboard_mcu/User.php @@ -0,0 +1,447 @@ +db_dashboard = $this->load->database("cpone_dashboard", true); + $this->db_log = $this->load->database("log", true); + } + + public function save() + { + if (!$this->isLogin) { + $this->sys_error("Invalid Token"); + return; + } + + $username = $this->sanitize_username($this->get_input('username')); + $password = (string) $this->get_input('password'); + $displayName = $this->sanitize_text($this->get_input('display_name'), 100); + + if ($username === '') { + $this->sys_error("username wajib diisi"); + return; + } + + if (!$this->is_valid_password_policy($password)) { + $this->sys_error("password harus minimal 8 karakter dan mengandung huruf besar, huruf kecil, angka, serta simbol"); + return; + } + + $query = $this->db_dashboard->query( + "CALL sp_insert_dashboard_user(?, ?, ?)", + array($username, $password, $displayName) + ); + $this->clean_mysqli_connection($this->db_dashboard->conn_id); + + if (!$query) { + $error = $this->db_dashboard->error(); + $this->log_activity("SAVE_USER", $username, $this->sys_input, "N", $error['message']); + $this->sys_error_db("gagal insert dashboard user", $this->db_dashboard); + return; + } + + $this->log_activity("SAVE_USER", $username, $this->sys_input, "Y", "success"); + $this->sys_ok(array( + "message" => "user berhasil disimpan", + "username" => $username + )); + } + + public function reset_password() + { + if (!$this->isLogin) { + $this->sys_error("Invalid Token"); + return; + } + + $username = $this->sanitize_username($this->get_input('username')); + $newPassword = (string) $this->get_input('password'); + + if ($username === '') { + $this->sys_error("username wajib diisi"); + return; + } + + if (!$this->is_valid_password_policy($newPassword)) { + $this->sys_error("password harus minimal 8 karakter dan mengandung huruf besar, huruf kecil, angka, serta simbol"); + return; + } + + $query = $this->db_dashboard->query( + "CALL sp_reset_dashboard_user_password(?, ?)", + array($username, $newPassword) + ); + $this->clean_mysqli_connection($this->db_dashboard->conn_id); + + if (!$query) { + $error = $this->db_dashboard->error(); + $this->log_activity("RESET_PASSWORD", $username, $this->sys_input, "N", $error['message']); + $this->sys_error_db("gagal reset password", $this->db_dashboard); + return; + } + + $this->log_activity("RESET_PASSWORD", $username, $this->sys_input, "Y", "success"); + $this->sys_ok(array( + "message" => "password berhasil direset", + "username" => $username + )); + } + + public function assign_project() + { + if (!$this->isLogin) { + $this->sys_error("Invalid Token"); + return; + } + + $username = $this->sanitize_username($this->get_input('username')); + $mcuID = intval($this->get_input('mcu_id')); + + if ($username === '') { + $this->sys_error("username wajib diisi"); + return; + } + + if ($mcuID <= 0) { + $this->sys_error("mcu_id tidak valid"); + return; + } + + $query = $this->db_dashboard->query( + "CALL sp_assign_user_project(?, ?)", + array($username, $mcuID) + ); + $this->clean_mysqli_connection($this->db_dashboard->conn_id); + + if (!$query) { + $error = $this->db_dashboard->error(); + $this->log_activity("ASSIGN_PROJECT", $username, $this->sys_input, "N", $error['message']); + $this->sys_error_db("gagal assign project", $this->db_dashboard); + return; + } + + $this->log_activity("ASSIGN_PROJECT", $username, $this->sys_input, "Y", "success"); + $this->sys_ok(array( + "message" => "project berhasil di-assign", + "username" => $username, + "mcu_id" => $mcuID + )); + } + + public function remove_project() + { + if (!$this->isLogin) { + $this->sys_error("Invalid Token"); + return; + } + + $username = $this->sanitize_username($this->get_input('username')); + $mcuID = intval($this->get_input('mcu_id')); + + if ($username === '') { + $this->sys_error("username wajib diisi"); + return; + } + + if ($mcuID <= 0) { + $this->sys_error("mcu_id tidak valid"); + return; + } + + $query = $this->db_dashboard->query( + "CALL sp_remove_user_project(?, ?)", + array($username, $mcuID) + ); + $this->clean_mysqli_connection($this->db_dashboard->conn_id); + + if (!$query) { + $error = $this->db_dashboard->error(); + $this->log_activity("REMOVE_PROJECT", $username, $this->sys_input, "N", $error['message']); + $this->sys_error_db("gagal remove project", $this->db_dashboard); + return; + } + + $this->log_activity("REMOVE_PROJECT", $username, $this->sys_input, "Y", "success"); + $this->sys_ok(array( + "message" => "project berhasil dihapus dari user", + "username" => $username, + "mcu_id" => $mcuID + )); + } + + public function search_project() + { + if (!$this->isLogin) { + $this->sys_error("Invalid Token"); + return; + } + + $keyword = $this->sanitize_text($this->get_input('search'), 100); + $keywordLike = "%" . $keyword . "%"; + + $sql = "SELECT + Mgm_McuID AS mcu_id, + Mgm_McuNumber AS project_number, + Mgm_McuLabel AS project_name + FROM cpone.mgm_mcu + WHERE Mgm_McuIsActive = 'Y' + AND ( + Mgm_McuNumber LIKE ? + OR Mgm_McuLabel LIKE ? + ) + ORDER BY Mgm_McuID DESC + LIMIT 20"; + + $query = $this->db_dashboard->query($sql, array($keywordLike, $keywordLike)); + if (!$query) { + $this->sys_error_db("gagal search project", $this->db_dashboard); + return; + } + + $this->sys_ok(array( + "records" => $query->result_array() + )); + } + + public function search() + { + if (!$this->isLogin) { + $this->sys_error("Invalid Token"); + return; + } + + $username = $this->sanitize_text($this->get_input('username'), 50); + $project = $this->sanitize_text($this->get_input('project'), 20); + $page = intval($this->get_input('page')); + $limit = intval($this->get_input('limit')); + + if ($page <= 0) { + $page = 1; + } + if ($limit <= 0) { + $limit = 20; + } + if ($limit > 100) { + $limit = 100; + } + + $offset = ($page - 1) * $limit; + $usernameLike = "%" . $username . "%"; + $projectID = 0; + if ($project !== '' && strtolower($project) !== 'all') { + $projectID = intval($project); + } + + $whereProject = ""; + $paramsCount = array($usernameLike); + $paramsData = array($usernameLike); + if ($projectID > 0) { + $whereProject = " AND up.UserProj_McuID = ? "; + $paramsCount[] = $projectID; + $paramsData[] = $projectID; + } + + $sqlCount = "SELECT COUNT(DISTINCT u.User_ID) AS total_rows + FROM cpone_dashboard.dashboard_user u + LEFT JOIN cpone_dashboard.dashboard_user_project up + ON up.UserProj_UserID = u.User_ID + AND up.UserProj_IsActive = 'Y' + WHERE u.User_IsActive = 'Y' + AND u.User_Username LIKE ? " . $whereProject; + + $countQuery = $this->db_dashboard->query($sqlCount, $paramsCount); + if (!$countQuery) { + $this->sys_error_db("gagal hitung data user", $this->db_dashboard); + return; + } + $totalRows = intval($countQuery->row()->total_rows); + $totalPages = $totalRows > 0 ? intval(ceil($totalRows / $limit)) : 0; + + $sqlData = "SELECT + u.User_ID, + u.User_Username, + u.User_DisplayName, + u.User_IsActive, + u.User_CreatedAt, + u.User_UpdatedAt, + GROUP_CONCAT( + DISTINCT CONCAT( + up.UserProj_McuID, '|', + IFNULL(m.Mgm_McuNumber, ''), '|', + IFNULL(m.Mgm_McuLabel, '') + ) + ORDER BY up.UserProj_McuID DESC + SEPARATOR '||' + ) AS project_list + FROM cpone_dashboard.dashboard_user u + LEFT JOIN cpone_dashboard.dashboard_user_project up + ON up.UserProj_UserID = u.User_ID + AND up.UserProj_IsActive = 'Y' + LEFT JOIN cpone.mgm_mcu m + ON m.Mgm_McuID = up.UserProj_McuID + WHERE u.User_IsActive = 'Y' + AND u.User_Username LIKE ? " . $whereProject . " + GROUP BY + u.User_ID, + u.User_Username, + u.User_DisplayName, + u.User_IsActive, + u.User_CreatedAt, + u.User_UpdatedAt + ORDER BY u.User_ID DESC + LIMIT ? OFFSET ?"; + + $paramsData[] = $limit; + $paramsData[] = $offset; + + $dataQuery = $this->db_dashboard->query($sqlData, $paramsData); + if (!$dataQuery) { + $this->sys_error_db("gagal ambil data user", $this->db_dashboard); + return; + } + + $rows = $dataQuery->result_array(); + foreach ($rows as &$row) { + $row['projects'] = array(); + if (!empty($row['project_list'])) { + $items = explode("||", $row['project_list']); + foreach ($items as $item) { + $parts = explode("|", $item); + $row['projects'][] = array( + "mcu_id" => isset($parts[0]) ? intval($parts[0]) : 0, + "project_number" => isset($parts[1]) ? $parts[1] : "", + "project_name" => isset($parts[2]) ? $parts[2] : "" + ); + } + } + unset($row['project_list']); + } + unset($row); + + $this->sys_ok(array( + "pagination" => array( + "page" => $page, + "limit" => $limit, + "total_rows" => $totalRows, + "total_pages" => $totalPages + ), + "filters" => array( + "username" => $username, + "project" => $project === '' ? 'all' : $project + ), + "records" => $rows + )); + } + + private function log_activity($action, $target, $payload, $isSuccess, $message) + { + $actorUserID = isset($this->sys_user["M_UserID"]) ? intval($this->sys_user["M_UserID"]) : 0; + $actorUsername = isset($this->sys_user["M_UserUsername"]) ? $this->sys_user["M_UserUsername"] : ""; + $actorEmail = isset($this->sys_user["M_UserEmail"]) ? $this->sys_user["M_UserEmail"] : ""; + + $sql = "INSERT INTO cpone_log.dashboard_user_activity_log ( + DashboardUserActivityLogAction, + DashboardUserActivityLogTarget, + DashboardUserActivityLogPayloadJson, + DashboardUserActivityLogIsSuccess, + DashboardUserActivityLogMessage, + DashboardUserActivityLogActorUserID, + DashboardUserActivityLogActorUsername, + DashboardUserActivityLogActorEmail, + DashboardUserActivityLogCreatedAt + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())"; + + $safePayload = $this->mask_sensitive_payload($payload); + $payloadJson = json_encode($safePayload); + if ($payloadJson === false) { + $payloadJson = "{}"; + } + + $this->db_log->query($sql, array( + $this->sanitize_text($action, 50), + $this->sanitize_text($target, 100), + $payloadJson, + $isSuccess === "Y" ? "Y" : "N", + $this->sanitize_text($message, 255), + $actorUserID, + $this->sanitize_text($actorUsername, 100), + $this->sanitize_text($actorEmail, 150) + )); + } + + private function mask_sensitive_payload($payload) + { + if (!is_array($payload)) { + return $payload; + } + + $sensitiveKeys = array( + 'password', + 'new_password', + 'old_password', + 'pass', + 'passwd' + ); + + foreach ($payload as $key => $value) { + $lowerKey = strtolower((string) $key); + if (in_array($lowerKey, $sensitiveKeys, true)) { + $payload[$key] = hash('sha256', (string) $value); + continue; + } + + if (is_array($value)) { + $payload[$key] = $this->mask_sensitive_payload($value); + } + } + + return $payload; + } + + private function get_input($key) + { + return isset($this->sys_input[$key]) ? $this->sys_input[$key] : ""; + } + + private function sanitize_username($value) + { + $value = trim((string) $value); + $value = preg_replace('/[^a-zA-Z0-9._-]/', '', $value); + return substr($value, 0, 50); + } + + private function sanitize_text($value, $maxLen = 255) + { + $value = trim((string) $value); + $value = strip_tags($value); + if (strlen($value) > $maxLen) { + $value = substr($value, 0, $maxLen); + } + return $value; + } + + private function is_valid_password_policy($password) + { + if (!is_string($password)) { + return false; + } + + if (strlen($password) < 8) { + return false; + } + + $hasUpper = preg_match('/[A-Z]/', $password) === 1; + $hasLower = preg_match('/[a-z]/', $password) === 1; + $hasNumber = preg_match('/[0-9]/', $password) === 1; + $hasSymbol = preg_match('/[^a-zA-Z0-9]/', $password) === 1; + + return $hasUpper && $hasLower && $hasNumber && $hasSymbol; + } +} diff --git a/scripts/sql/2026-05-07_create_table_dashboard_user_activity_log.sql b/scripts/sql/2026-05-07_create_table_dashboard_user_activity_log.sql new file mode 100644 index 0000000..f50402e --- /dev/null +++ b/scripts/sql/2026-05-07_create_table_dashboard_user_activity_log.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS `cpone_log`.`dashboard_user_activity_log` ( + `DashboardUserActivityLogID` BIGINT NOT NULL AUTO_INCREMENT, + `DashboardUserActivityLogAction` VARCHAR(50) NOT NULL, + `DashboardUserActivityLogTarget` VARCHAR(100) DEFAULT NULL, + `DashboardUserActivityLogPayloadJson` LONGTEXT, + `DashboardUserActivityLogIsSuccess` CHAR(1) NOT NULL DEFAULT 'Y', + `DashboardUserActivityLogMessage` VARCHAR(255) DEFAULT NULL, + `DashboardUserActivityLogActorUserID` INT NOT NULL DEFAULT 0, + `DashboardUserActivityLogActorUsername` VARCHAR(100) DEFAULT NULL, + `DashboardUserActivityLogActorEmail` VARCHAR(150) DEFAULT NULL, + `DashboardUserActivityLogCreatedAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`DashboardUserActivityLogID`), + KEY `idx_dashboard_user_activity_log_created` (`DashboardUserActivityLogCreatedAt`), + KEY `idx_dashboard_user_activity_log_action` (`DashboardUserActivityLogAction`), + KEY `idx_dashboard_user_activity_log_actor` (`DashboardUserActivityLogActorUserID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +