From 1741545318035a23d5550b9041c646b07c1f12fb Mon Sep 17 00:00:00 2001 From: Agent Date: Sun, 7 Jun 2026 17:26:29 -0400 Subject: [PATCH] Add soft deletes via status column - Add status ENUM('active','disabled','deleted') to 8 tables - Change all physical DELETE to UPDATE status='deleted' - All SELECT queries filter by status='active' - toggleActive now alternates between active/disabled status - Exceptions: monitoring_history, rate_limits, sessions keep physical purge - audit_logs remains immutable --- database/migrations/004_soft_deletes.sql | 44 +++++++++++++++++++ src/Models/CommandHistory.php | 18 ++++++-- src/Models/Server.php | 56 ++++++++++++++---------- src/Models/Team.php | 40 ++++++++++------- src/Models/User.php | 24 +++++----- src/Services/MonitoringService.php | 16 +++---- 6 files changed, 138 insertions(+), 60 deletions(-) create mode 100644 database/migrations/004_soft_deletes.sql diff --git a/database/migrations/004_soft_deletes.sql b/database/migrations/004_soft_deletes.sql new file mode 100644 index 0000000..a5cb9be --- /dev/null +++ b/database/migrations/004_soft_deletes.sql @@ -0,0 +1,44 @@ +-- ============================================================================ +-- ServerManager Soft Deletes +-- Adds status column for soft deletes instead of physical row removal +-- Exceptions: monitoring_history, rate_limits, sessions (physical purge OK) +-- Exceptions: audit_logs (immutable, never deleted) +-- ============================================================================ + +ALTER TABLE `users` + ADD COLUMN `status` ENUM('active', 'disabled', 'deleted') NOT NULL DEFAULT 'active' AFTER `updated_at`, + ADD INDEX `idx_users_status` (`status`); + +ALTER TABLE `servers` + ADD COLUMN `status` ENUM('active', 'disabled', 'deleted') NOT NULL DEFAULT 'active' AFTER `updated_at`, + ADD INDEX `idx_servers_status` (`status`); + +ALTER TABLE `teams` + ADD COLUMN `status` ENUM('active', 'disabled', 'deleted') NOT NULL DEFAULT 'active' AFTER `updated_at`, + ADD INDEX `idx_teams_status` (`status`); + +ALTER TABLE `command_history` + ADD COLUMN `status` ENUM('active', 'disabled', 'deleted') NOT NULL DEFAULT 'active' AFTER `executed_at`, + ADD INDEX `idx_command_history_status` (`status`); + +ALTER TABLE `server_user` + ADD COLUMN `status` ENUM('active', 'disabled', 'deleted') NOT NULL DEFAULT 'active' AFTER `role`, + ADD INDEX `idx_server_user_status` (`status`); + +ALTER TABLE `team_user` + ADD COLUMN `status` ENUM('active', 'disabled', 'deleted') NOT NULL DEFAULT 'active' AFTER `created_at`, + ADD INDEX `idx_team_user_status` (`status`); + +ALTER TABLE `team_server` + ADD COLUMN `status` ENUM('active', 'disabled', 'deleted') NOT NULL DEFAULT 'active' AFTER `role`, + ADD INDEX `idx_team_server_status` (`status`); + +ALTER TABLE `api_tokens` + ADD COLUMN `status` ENUM('active', 'disabled', 'deleted') NOT NULL DEFAULT 'active' AFTER `expires_at`, + ADD INDEX `idx_api_tokens_status` (`status`); + +-- ============================================================================ +-- FK constraints remain unchanged: +-- ON DELETE CASCADE only fires on physical DELETE, which we no longer do. +-- monitoring_history, rate_limits, sessions keep physical purge. +-- ============================================================================ diff --git a/src/Models/CommandHistory.php b/src/Models/CommandHistory.php index 8afa865..2f1c610 100755 --- a/src/Models/CommandHistory.php +++ b/src/Models/CommandHistory.php @@ -33,7 +33,7 @@ class CommandHistory 'SELECT ch.*, u.username FROM command_history ch LEFT JOIN users u ON ch.user_id = u.id - WHERE ch.server_id = ? + WHERE ch.server_id = ? AND ch.status = \'active\' ORDER BY ch.executed_at DESC LIMIT ?', [$serverId, $limit] @@ -47,6 +47,7 @@ class CommandHistory FROM command_history ch LEFT JOIN users u ON ch.user_id = u.id LEFT JOIN servers s ON ch.server_id = s.id + WHERE ch.status = \'active\' ORDER BY ch.executed_at DESC LIMIT ?', [$limit] @@ -60,7 +61,7 @@ class CommandHistory FROM command_history ch LEFT JOIN users u ON ch.user_id = u.id LEFT JOIN servers s ON ch.server_id = s.id - WHERE ch.id = ?', + WHERE ch.id = ? AND ch.status = \'active\'', [$id] ); } @@ -68,8 +69,17 @@ class CommandHistory public function clearHistory(int $serverId = null): int { if ($serverId) { - return $this->db->delete('command_history', 'server_id = ?', [$serverId]); + return $this->db->update( + 'command_history', + ['status' => 'deleted'], + 'server_id = ?', + [$serverId] + ); } - return $this->db->delete('command_history', '1=1'); + return $this->db->update( + 'command_history', + ['status' => 'deleted'], + '1=1' + ); } } diff --git a/src/Models/Server.php b/src/Models/Server.php index 8f7b260..71c77de 100755 --- a/src/Models/Server.php +++ b/src/Models/Server.php @@ -23,7 +23,7 @@ class Server public function findById(int $id): ?array { - $server = $this->db->fetch('SELECT * FROM servers WHERE id = ?', [$id]); + $server = $this->db->fetch('SELECT * FROM servers WHERE id = ? AND status = \'active\'', [$id]); if ($server) { $server = $this->encryption->decryptArray($server, $this->encryptedFields); } @@ -32,11 +32,11 @@ class Server public function findAll(bool $activeOnly = false): array { - $sql = 'SELECT * FROM servers'; + $sql = 'SELECT * FROM servers WHERE status = \'active\''; $params = []; if ($activeOnly) { - $sql .= ' WHERE is_active = 1'; + $sql .= ' AND is_active = 1'; } $sql .= ' ORDER BY name ASC'; @@ -49,7 +49,7 @@ class Server public function getAll(int $page = 1, int $perPage = 20, array $filters = []): array { - $where = []; + $where = ["s.status = 'active'"]; $params = []; if (!empty($filters['search'])) { @@ -162,18 +162,27 @@ class Server public function delete(int $id): bool { - return $this->db->delete('servers', 'id = ?', [$id]) > 0; + return $this->db->update('servers', [ + 'status' => 'deleted', + 'is_active' => 0, + 'updated_at' => date('Y-m-d H:i:s'), + ], 'id = ?', [$id]) > 0; } public function toggleActive(int $id): ?bool { - $server = $this->db->fetch('SELECT is_active FROM servers WHERE id = ?', [$id]); + $server = $this->db->fetch('SELECT is_active, status FROM servers WHERE id = ?', [$id]); if (!$server) { return null; } $newState = $server['is_active'] ? 0 : 1; - $this->db->update('servers', ['is_active' => $newState, 'updated_at' => date('Y-m-d H:i:s')], 'id = ?', [$id]); + $newStatus = $newState ? 'active' : 'disabled'; + $this->db->update('servers', [ + 'is_active' => $newState, + 'status' => $newStatus, + 'updated_at' => date('Y-m-d H:i:s'), + ], 'id = ?', [$id]); return (bool) $newState; } @@ -181,28 +190,28 @@ class Server public function getGroups(): array { $groups = $this->db->fetchAll( - 'SELECT DISTINCT group_name FROM servers WHERE group_name IS NOT NULL AND group_name != "" ORDER BY group_name' + 'SELECT DISTINCT group_name FROM servers WHERE status = \'active\' AND group_name IS NOT NULL AND group_name != "" ORDER BY group_name' ); return array_column($groups, 'group_name'); } public function countAll(): int { - $result = $this->db->fetch("SELECT COUNT(*) as total FROM servers"); + $result = $this->db->fetch("SELECT COUNT(*) as total FROM servers WHERE status = 'active'"); return (int) ($result['total'] ?? 0); } public function countOnline(): int { $result = $this->db->fetch( - "SELECT COUNT(*) as total FROM servers WHERE current_status = 'online' AND is_active = 1" + "SELECT COUNT(*) as total FROM servers WHERE current_status = 'online' AND status = 'active' AND is_active = 1" ); return (int) ($result['total'] ?? 0); } public function getUserRole(int $serverId, int $userId): ?string { - $server = $this->db->fetch('SELECT created_by FROM servers WHERE id = ?', [$serverId]); + $server = $this->db->fetch('SELECT created_by FROM servers WHERE id = ? AND status = \'active\'', [$serverId]); if (!$server) { return null; } @@ -212,14 +221,14 @@ class Server } $direct = $this->db->fetch( - 'SELECT role FROM server_user WHERE server_id = ? AND user_id = ?', + 'SELECT role FROM server_user WHERE server_id = ? AND user_id = ? AND status = \'active\'', [$serverId, $userId] ); $teamRole = $this->db->fetch( 'SELECT ts.role FROM team_user tu JOIN team_server ts ON tu.team_id = ts.team_id - WHERE tu.user_id = ? AND ts.server_id = ? + WHERE tu.user_id = ? AND ts.server_id = ? AND tu.status = \'active\' AND ts.status = \'active\' ORDER BY FIELD(ts.role, \'manager\', \'viewer\') LIMIT 1', [$userId, $serverId] @@ -241,19 +250,19 @@ class Server public function getAccessibleServerIds(int $userId): array { $owned = $this->db->fetchAll( - 'SELECT id FROM servers WHERE created_by = ?', + 'SELECT id FROM servers WHERE created_by = ? AND status = \'active\'', [$userId] ); $assigned = $this->db->fetchAll( - 'SELECT server_id FROM server_user WHERE user_id = ?', + 'SELECT server_id FROM server_user WHERE user_id = ? AND status = \'active\'', [$userId] ); $teamServerIds = $this->db->fetchAll( 'SELECT DISTINCT ts.server_id FROM team_user tu JOIN team_server ts ON tu.team_id = ts.team_id - WHERE tu.user_id = ?', + WHERE tu.user_id = ? AND tu.status = \'active\' AND ts.status = \'active\'', [$userId] ); @@ -273,7 +282,7 @@ class Server public function isOwner(int $serverId, int $userId): bool { - $server = $this->db->fetch('SELECT created_by FROM servers WHERE id = ?', [$serverId]); + $server = $this->db->fetch('SELECT created_by FROM servers WHERE id = ? AND status = \'active\'', [$serverId]); return $server && (int) $server['created_by'] === $userId; } @@ -294,7 +303,7 @@ class Server 'SELECT su.*, u.username, u.email FROM server_user su JOIN users u ON su.user_id = u.id - WHERE su.server_id = ? + WHERE su.server_id = ? AND su.status = \'active\' AND u.status = \'active\' ORDER BY su.created_at ASC', [$serverId] ); @@ -303,7 +312,7 @@ class Server 'SELECT u.id, u.username, u.email FROM servers s JOIN users u ON s.created_by = u.id - WHERE s.id = ?', + WHERE s.id = ? AND s.status = \'active\' AND u.status = \'active\'', [$serverId] ); @@ -334,7 +343,7 @@ class Server } $existing = $this->db->fetch( - 'SELECT id FROM server_user WHERE server_id = ? AND user_id = ?', + 'SELECT id FROM server_user WHERE server_id = ? AND user_id = ? AND status = \'active\'', [$serverId, $userId] ); if ($existing) { @@ -351,8 +360,9 @@ class Server public function removeUser(int $serverId, int $userId): bool { - return $this->db->delete( + return $this->db->update( 'server_user', + ['status' => 'deleted'], 'server_id = ? AND user_id = ?', [$serverId, $userId] ) > 0; @@ -367,7 +377,7 @@ class Server return $this->db->update( 'server_user', ['role' => $role], - 'server_id = ? AND user_id = ?', + 'server_id = ? AND user_id = ? AND status = \'active\'', [$serverId, $userId] ) > 0; } @@ -375,7 +385,7 @@ class Server public function getOwnedServers(int $userId): array { return $this->db->fetchAll( - 'SELECT * FROM servers WHERE created_by = ? ORDER BY name ASC', + 'SELECT * FROM servers WHERE created_by = ? AND status = \'active\' ORDER BY name ASC', [$userId] ); } diff --git a/src/Models/Team.php b/src/Models/Team.php index b512be6..5f5417b 100644 --- a/src/Models/Team.php +++ b/src/Models/Team.php @@ -18,13 +18,18 @@ class Team public function findAll(?int $userId = null): array { - $where = $userId ? ' WHERE t.created_by = ?' : ''; - $params = $userId ? [$userId] : []; + $where = ' WHERE t.status = \'active\''; + $params = []; + + if ($userId) { + $where .= ' AND t.created_by = ?'; + $params[] = $userId; + } return $this->db->fetchAll( "SELECT t.*, - (SELECT COUNT(*) FROM team_user WHERE team_id = t.id) AS member_count, - (SELECT COUNT(*) FROM team_server WHERE team_id = t.id) AS server_count + (SELECT COUNT(*) FROM team_user WHERE team_id = t.id AND status = 'active') AS member_count, + (SELECT COUNT(*) FROM team_server WHERE team_id = t.id AND status = 'active') AS server_count FROM teams t{$where} ORDER BY t.name ASC", $params @@ -40,9 +45,9 @@ class Team { $team = $this->db->fetch( 'SELECT t.*, - (SELECT COUNT(*) FROM team_user WHERE team_id = t.id) AS member_count, - (SELECT COUNT(*) FROM team_server WHERE team_id = t.id) AS server_count - FROM teams t WHERE t.id = ?', + (SELECT COUNT(*) FROM team_user WHERE team_id = t.id AND status = \'active\') AS member_count, + (SELECT COUNT(*) FROM team_server WHERE team_id = t.id AND status = \'active\') AS server_count + FROM teams t WHERE t.id = ? AND t.status = \'active\'', [$id] ); return $team ?: null; @@ -64,7 +69,10 @@ class Team public function delete(int $id): bool { - return $this->db->delete('teams', 'id = ?', [$id]) > 0; + return $this->db->update('teams', [ + 'status' => 'deleted', + 'updated_at' => date('Y-m-d H:i:s'), + ], 'id = ?', [$id]) > 0; } public function getMembers(int $teamId): array @@ -73,7 +81,7 @@ class Team 'SELECT tu.*, u.username, u.email FROM team_user tu JOIN users u ON tu.user_id = u.id - WHERE tu.team_id = ? + WHERE tu.team_id = ? AND tu.status = \'active\' AND u.status = \'active\' ORDER BY u.username ASC', [$teamId] ); @@ -82,7 +90,7 @@ class Team public function addMember(int $teamId, int $userId): bool { $existing = $this->db->fetch( - 'SELECT id FROM team_user WHERE team_id = ? AND user_id = ?', + 'SELECT id FROM team_user WHERE team_id = ? AND user_id = ? AND status = \'active\'', [$teamId, $userId] ); if ($existing) { @@ -98,8 +106,9 @@ class Team public function removeMember(int $teamId, int $userId): bool { - return $this->db->delete( + return $this->db->update( 'team_user', + ['status' => 'deleted'], 'team_id = ? AND user_id = ?', [$teamId, $userId] ) > 0; @@ -111,7 +120,7 @@ class Team 'SELECT ts.*, s.name AS server_name, s.ip_address FROM team_server ts JOIN servers s ON ts.server_id = s.id - WHERE ts.team_id = ? + WHERE ts.team_id = ? AND ts.status = \'active\' AND s.status = \'active\' ORDER BY s.name ASC', [$teamId] ); @@ -124,7 +133,7 @@ class Team } $existing = $this->db->fetch( - 'SELECT id FROM team_server WHERE team_id = ? AND server_id = ?', + 'SELECT id FROM team_server WHERE team_id = ? AND server_id = ? AND status = \'active\'', [$teamId, $serverId] ); if ($existing) { @@ -141,8 +150,9 @@ class Team public function removeServer(int $teamId, int $serverId): bool { - return $this->db->delete( + return $this->db->update( 'team_server', + ['status' => 'deleted'], 'team_id = ? AND server_id = ?', [$teamId, $serverId] ) > 0; @@ -157,7 +167,7 @@ class Team return $this->db->update( 'team_server', ['role' => $role], - 'team_id = ? AND server_id = ?', + 'team_id = ? AND server_id = ? AND status = \'active\'', [$teamId, $serverId] ) > 0; } diff --git a/src/Models/User.php b/src/Models/User.php index c5a9aa6..ce425c1 100755 --- a/src/Models/User.php +++ b/src/Models/User.php @@ -20,17 +20,17 @@ class User public function findById(int $id): ?array { - return $this->db->fetch('SELECT * FROM users WHERE id = ?', [$id]); + return $this->db->fetch('SELECT * FROM users WHERE id = ? AND status = \'active\'', [$id]); } public function findByUsername(string $username): ?array { - return $this->db->fetch('SELECT * FROM users WHERE username = ?', [$username]); + return $this->db->fetch('SELECT * FROM users WHERE username = ? AND status = \'active\'', [$username]); } public function findByEmail(string $email): ?array { - return $this->db->fetch('SELECT * FROM users WHERE email = ?', [$email]); + return $this->db->fetch('SELECT * FROM users WHERE email = ? AND status = \'active\'', [$email]); } public function authenticate(string $username, string $password): ?array @@ -41,7 +41,7 @@ class User return null; } - if (!$user['is_active']) { + if ($user['status'] !== 'active' || !$user['is_active']) { return null; } @@ -83,17 +83,21 @@ class User public function delete(int $id): bool { - return $this->db->delete('users', 'id = ?', [$id]) > 0; + return $this->db->update('users', [ + 'status' => 'deleted', + 'is_active' => 0, + 'updated_at' => date('Y-m-d H:i:s'), + ], 'id = ?', [$id]) > 0; } public function getAll(int $page = 1, int $perPage = 20): array { $offset = ($page - 1) * $perPage; - $total = $this->db->fetch("SELECT COUNT(*) as total FROM users"); + $total = $this->db->fetch("SELECT COUNT(*) as total FROM users WHERE status = 'active'"); $users = $this->db->fetchAll( - 'SELECT id, username, email, role, is_active, last_login_at, created_at - FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?', + 'SELECT id, username, email, role, status, is_active, last_login_at, created_at + FROM users WHERE status = \'active\' ORDER BY created_at DESC LIMIT ? OFFSET ?', [$perPage, $offset] ); @@ -108,7 +112,7 @@ class User public function findByApiToken(string $token): ?array { - return $this->db->fetch('SELECT * FROM users WHERE api_token = ? AND is_active = 1', [$token]); + return $this->db->fetch('SELECT * FROM users WHERE api_token = ? AND status = \'active\' AND is_active = 1', [$token]); } public function updateLastLogin(int $id): void @@ -118,7 +122,7 @@ class User public function countAll(): int { - $result = $this->db->fetch("SELECT COUNT(*) as total FROM users"); + $result = $this->db->fetch("SELECT COUNT(*) as total FROM users WHERE status = 'active'"); return (int) ($result['total'] ?? 0); } } diff --git a/src/Services/MonitoringService.php b/src/Services/MonitoringService.php index b73fe59..8ccb0fa 100755 --- a/src/Services/MonitoringService.php +++ b/src/Services/MonitoringService.php @@ -22,7 +22,7 @@ class MonitoringService public function collectMetrics(int $serverId): ?array { - $server = $this->db->fetch('SELECT * FROM servers WHERE id = ?', [$serverId]); + $server = $this->db->fetch('SELECT * FROM servers WHERE id = ? AND status = \'active\'', [$serverId]); if (!$server || !$server['is_active']) { return null; @@ -71,7 +71,7 @@ class MonitoringService public function collectAllMetrics(): array { - $servers = $this->db->fetchAll('SELECT * FROM servers WHERE is_active = 1'); + $servers = $this->db->fetchAll('SELECT * FROM servers WHERE status = \'active\' AND is_active = 1'); $results = []; foreach ($servers as $server) { @@ -187,26 +187,26 @@ class MonitoringService $params = $serverIds; } - $totalServers = $this->db->fetch("SELECT COUNT(*) as total FROM servers s WHERE 1=1{$where}", $params); + $totalServers = $this->db->fetch("SELECT COUNT(*) as total FROM servers s WHERE s.status = 'active'{$where}", $params); $onlineServers = $this->db->fetch( - "SELECT COUNT(*) as total FROM servers s WHERE s.current_status = 'online' AND s.is_active = 1{$where}", + "SELECT COUNT(*) as total FROM servers s WHERE s.current_status = 'online' AND s.status = 'active' AND s.is_active = 1{$where}", $params ); $offlineServers = $this->db->fetch( - "SELECT COUNT(*) as total FROM servers s WHERE s.current_status = 'offline' AND s.is_active = 1{$where}", + "SELECT COUNT(*) as total FROM servers s WHERE s.current_status = 'offline' AND s.status = 'active' AND s.is_active = 1{$where}", $params ); $avgCpu = $this->db->fetch( - "SELECT AVG(s.cpu_usage) as avg FROM servers s WHERE s.is_active = 1 AND s.current_status = 'online'{$where}", + "SELECT AVG(s.cpu_usage) as avg FROM servers s WHERE s.status = 'active' AND s.is_active = 1 AND s.current_status = 'online'{$where}", $params ); $avgRam = $this->db->fetch( - "SELECT AVG(s.ram_usage) as avg FROM servers s WHERE s.is_active = 1 AND s.current_status = 'online'{$where}", + "SELECT AVG(s.ram_usage) as avg FROM servers s WHERE s.status = 'active' AND s.is_active = 1 AND s.current_status = 'online'{$where}", $params ); $avgDisk = $this->db->fetch( - "SELECT AVG(s.disk_usage) as avg FROM servers s WHERE s.is_active = 1 AND s.current_status = 'online'{$where}", + "SELECT AVG(s.disk_usage) as avg FROM servers s WHERE s.status = 'active' AND s.is_active = 1 AND s.current_status = 'online'{$where}", $params );