🐘
index_bothconnections.php
Back
📝 Php ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
<?php // Force login + disable caching session_start(); require_once __DIR__ . '/../core/db_config.php'; // Anti-cache headers header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); header('Cache-Control: post-check=0, pre-check=0', false); header('Pragma: no-cache'); header('Expires: 0'); // Redirect if not logged in if (empty($_SESSION['username'])) { $redirect = urlencode($_SERVER['REQUEST_URI'] ?? '/'); header("Location: /core/auth/login.php?redirect={$redirect}"); exit; } // Handle SFTP API requests if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['sftp_action'])) { header('Content-Type: application/json'); error_reporting(E_ALL); ini_set('display_errors', 0); ini_set('log_errors', 1); ob_start(); // Clear any stale connections on new request if (!isset($_SESSION['sftp_last_activity'])) { unset($_SESSION['sftp_connected']); unset($_SESSION['sftp_config']); } elseif (time() - $_SESSION['sftp_last_activity'] > 3600) { unset($_SESSION['sftp_connected']); unset($_SESSION['sftp_config']); } class SFTPConnector { private $connection; private $sftp; public function __construct() { if (!extension_loaded('ssh2')) { throw new Exception('SSH2 extension is not installed'); } } public function connect($host, $port, $username, $password) { try { $this->connection = ssh2_connect($host, $port); if (!$this->connection) { throw new Exception("Could not connect to $host on port $port"); } if (!ssh2_auth_password($this->connection, $username, $password)) { throw new Exception("Authentication failed for user $username"); } $this->sftp = ssh2_sftp($this->connection); if (!$this->sftp) { throw new Exception("Could not initialize SFTP subsystem"); } return true; } catch (Exception $e) { return $e->getMessage(); } } public function listDirectory($path = '.') { try { $handle = opendir("ssh2.sftp://{$this->sftp}$path"); if (!$handle) { throw new Exception("Could not open directory: $path"); } $files = []; while (($file = readdir($handle)) !== false) { if ($file !== '.' && $file !== '..') { $fullPath = rtrim($path, '/') . '/' . $file; $stat = ssh2_sftp_stat($this->sftp, $fullPath); if ($stat !== false) { $files[] = [ 'name' => $file, 'path' => $fullPath, 'size' => $stat['size'], 'modified' => date('Y-m-d H:i:s', $stat['mtime']), 'type' => is_dir("ssh2.sftp://{$this->sftp}$fullPath") ? 'directory' : 'file' ]; } } } closedir($handle); usort($files, function($a, $b) { if ($a['type'] === 'directory' && $b['type'] === 'file') return -1; if ($a['type'] === 'file' && $b['type'] === 'directory') return 1; return strcasecmp($a['name'], $b['name']); }); return $files; } catch (Exception $e) { return false; } } public function deleteFile($remotePath) { try { if (ssh2_sftp_unlink($this->sftp, $remotePath)) { return true; } return "Failed to delete file"; } catch (Exception $e) { return $e->getMessage(); } } public function disconnect() { $this->connection = null; $this->sftp = null; } } class SystemSFTPConnector { private $host; private $port; private $username; private $password; private $connected = false; public function connect($host, $port, $username, $password) { $this->host = $host; $this->port = $port; $this->username = $username; $this->password = $password; if (!function_exists('exec')) { return 'System commands are disabled on this server'; } $testCommand = "echo 'quit' | sftp -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -P {$port} {$username}@{$host} 2>&1"; $output = []; $returnCode = null; exec($testCommand, $output, $returnCode); if ($returnCode === 0 || strpos(implode(' ', $output), 'Connected to') !== false) { $this->connected = true; return true; } return 'Cannot connect to SFTP server: ' . implode(' ', $output); } public function listDirectory($path = '/') { if (!$this->connected) { return false; } $scriptFile = tempnam(sys_get_temp_dir(), 'sftp_script'); $script = "cd {$path}\nls -la\nquit\n"; file_put_contents($scriptFile, $script); $command = "sftp -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -P {$this->port} -b {$scriptFile} {$this->username}@{$this->host} 2>&1"; $output = []; exec($command, $output); unlink($scriptFile); $files = []; $inListing = false; foreach ($output as $line) { if (strpos($line, 'sftp>') !== false && strpos($line, 'ls') !== false) { $inListing = true; continue; } if ($inListing && strpos($line, 'sftp>') !== false) { break; } if ($inListing && preg_match('/^([drwx-]+)\s+\d+\s+\w+\s+\w+\s+(\d+)\s+(\w+\s+\d+\s+[\d:]+)\s+(.+)$/', $line, $matches)) { $name = $matches[4]; if ($name !== '.' && $name !== '..') { $files[] = [ 'name' => $name, 'path' => rtrim($path, '/') . '/' . $name, 'size' => (int)$matches[2], 'modified' => $matches[3], 'type' => substr($matches[1], 0, 1) === 'd' ? 'directory' : 'file' ]; } } } return $files; } public function deleteFile($remotePath) { if (!$this->connected) { return 'Not connected'; } $scriptFile = tempnam(sys_get_temp_dir(), 'sftp_script'); $script = "rm {$remotePath}\nquit\n"; file_put_contents($scriptFile, $script); $command = "sftp -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -P {$this->port} -b {$scriptFile} {$this->username}@{$this->host} 2>&1"; $output = []; exec($command, $output); unlink($scriptFile); if (strpos(implode(' ', $output), 'Removing') !== false || empty(array_filter($output))) { return true; } return 'Delete failed: ' . implode(' ', $output); } public function disconnect() { $this->connected = false; } } $action = $_POST['sftp_action'] ?? ''; $response = ['success' => false, 'message' => '', 'data' => null]; try { if (extension_loaded('ssh2')) { $connector = new SFTPConnector(); $connectorType = 'SSH2 Extension'; } else if (function_exists('exec')) { $connector = new SystemSFTPConnector(); $connectorType = 'System Commands'; } else { throw new Exception('Neither SSH2 extension nor system commands are available'); } switch ($action) { case 'connect': $result = $connector->connect( $_POST['host'] ?? '', $_POST['port'] ?? 22, $_POST['username'] ?? '', $_POST['password'] ?? '' ); if ($result === true) { $_SESSION['sftp_connected'] = true; $_SESSION['sftp_config'] = [ 'host' => $_POST['host'], 'port' => $_POST['port'] ?? 22, 'username' => $_POST['username'], 'password' => $_POST['password'] ]; $_SESSION['sftp_last_activity'] = time(); $response['success'] = true; $response['message'] = "Connected successfully via {$connectorType}"; } else { $response['message'] = $result; } break; case 'list': if (!isset($_SESSION['sftp_connected']) || !$_SESSION['sftp_connected']) { $response['success'] = false; $response['message'] = 'Not connected to SFTP server. Please connect first.'; break; } $config = $_SESSION['sftp_config']; if (extension_loaded('ssh2')) { $connector = new SFTPConnector(); } else { $connector = new SystemSFTPConnector(); } $connectResult = $connector->connect( $config['host'], $config['port'], $config['username'], $config['password'] ); if ($connectResult !== true) { unset($_SESSION['sftp_connected']); unset($_SESSION['sftp_config']); $response['message'] = 'Session expired. Please reconnect.'; break; } $_SESSION['sftp_last_activity'] = time(); $files = $connector->listDirectory($_POST['path'] ?? '/'); if ($files !== false) { $response['success'] = true; $response['data'] = $files; } else { $response['message'] = 'Failed to list directory'; } break; case 'delete': if (!isset($_SESSION['sftp_connected']) || !$_SESSION['sftp_connected']) { $response['message'] = 'Not connected to SFTP server'; break; } $config = $_SESSION['sftp_config']; if (extension_loaded('ssh2')) { $connector = new SFTPConnector(); } else { $connector = new SystemSFTPConnector(); } $connectResult = $connector->connect( $config['host'], $config['port'], $config['username'], $config['password'] ); if ($connectResult !== true) { unset($_SESSION['sftp_connected']); unset($_SESSION['sftp_config']); $response['message'] = 'Session expired. Please reconnect.'; break; } $_SESSION['sftp_last_activity'] = time(); $result = $connector->deleteFile($_POST['path'] ?? ''); if ($result === true) { $response['success'] = true; $response['message'] = 'File deleted successfully'; } else { $response['message'] = $result; } break; case 'disconnect': unset($_SESSION['sftp_connected']); unset($_SESSION['sftp_config']); unset($_SESSION['sftp_last_activity']); if (isset($connector)) { $connector->disconnect(); } $response['success'] = true; $response['message'] = 'Disconnected successfully'; break; case 'status': $response['success'] = true; $response['data'] = [ 'connected' => isset($_SESSION['sftp_connected']) && $_SESSION['sftp_connected'], 'config' => isset($_SESSION['sftp_config']) ? [ 'host' => $_SESSION['sftp_config']['host'], 'username' => $_SESSION['sftp_config']['username'], 'port' => $_SESSION['sftp_config']['port'] ] : null, 'last_activity' => $_SESSION['sftp_last_activity'] ?? null ]; break; default: $response['message'] = 'Invalid action: ' . $action; } } catch (Exception $e) { $response['message'] = 'Server error: ' . $e->getMessage(); } ob_clean(); echo json_encode($response); exit; } // Cache-busting helper function asset($path) { $isAbsolute = strlen($path) && $path[0] === '/'; $abs = $isAbsolute ? rtrim($_SERVER['DOCUMENT_ROOT'], '/') . $path : __DIR__ . '/' . $path; $v = is_file($abs) ? filemtime($abs) : time(); return $path . '?v=' . $v; } ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>SFTP Manager</title> <!-- Shared overlay CSS --> <link rel="stylesheet" href="<?= asset('/core/css/overlay.css') ?>"> <style> html, body { height: 100%; margin: 0; overscroll-behavior-y: contain; } body { background: #0b0f14; color: #e6edf3; font-family: system-ui, -apple-system, Segoe UI, Roboto, Inter, "Helvetica Neue", Arial; } .body-lock { overflow: hidden !important; touch-action: none; } .topbar { position: sticky; top: 0; z-index: 5; background: linear-gradient(180deg, rgba(11,15,20,.95), rgba(11,15,20,.85)); border-bottom: 1px solid #1e2633; backdrop-filter: blur(6px); } .topbar-inner { max-width: 1400px; margin: 0 auto; padding: .75rem; display: flex; align-items: center; gap: .75rem; } #buttonRow { flex: 1 1 auto; min-width: 0; display: flex; gap: .75rem; align-items: center; overflow-x: auto; overflow-y: hidden; scrollbar-width: thin; -webkit-overflow-scrolling: touch; } #menuContainer { flex: 0 0 auto; margin-left: .25rem; position: relative; } .chip { flex: 0 0 auto; border: 1px solid #2a3648; background: #1a2332; color: #e6edf3; padding: .55rem .9rem; border-radius: 999px; font-weight: 600; cursor: pointer; transition: background .15s ease; } .chip:hover { background: #263244; } .container { max-width: 1400px; margin: 0 auto; padding: 1.25rem .75rem; } .menu-trigger { width: 38px; text-align: center; } .menu-list { display: none; position: absolute; right: 0; top: calc(100% + 6px); background: #1a2332; border: 1px solid #2a3648; border-radius: 10px; min-width: 180px; padding: .25rem 0; z-index: 9999; box-shadow: 0 10px 30px rgba(0,0,0,.3); } .menu-list.open { display: block; } .menu-item { display: block; width: 100%; text-align: left; background: none; border: none; color: #e6edf3; padding: .6rem 1rem; cursor: pointer; font: inherit; } .menu-item:hover { background: #263244; } /* Quick Connect Card - matches connection card style */ .quick-connect-card { position: relative; background: rgba(30, 41, 59, 0.7); border: 2px solid #2a3648; border-radius: 12px; padding: 1rem; cursor: pointer; transition: all 0.2s ease; } .quick-connect-card:hover { border-color: #3b82f6; transform: translateY(-2px); box-shadow: 0 8px 16px rgba(59, 130, 246, 0.2); } .quick-connect-card.active { border-color: #10b981; background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(16, 185, 129, 0.05)); } .quick-status-bar { position: absolute; top: 0; left: 0; right: 0; height: 4px; border-radius: 12px 12px 0 0; background: #2a3648; } .quick-connect-card.active .quick-status-bar { background: linear-gradient(90deg, #10b981, #34d399); } .quick-connect-header { display: flex; justify-content: space-between; align-items: start; margin-top: 8px; margin-bottom: 12px; } .quick-connect-title { font-size: 18px; font-weight: 700; color: #e6edf3; display: flex; align-items: center; gap: 0.5rem; } .quick-badge { display: inline-block; background: linear-gradient(135deg, #10b981, #059669); color: white; font-size: 0.65rem; padding: 0.2rem 0.4rem; border-radius: 4px; font-weight: 600; } .quick-connect-info { font-size: 14px; color: #9aa4b2; line-height: 1.6; margin-bottom: 12px; } .quick-info-row { display: flex; gap: 8px; margin-bottom: 4px; } .quick-info-label { font-weight: 600; min-width: 60px; } .quick-password-input { width: 100%; padding: 0.625rem 0.75rem; background: #0f1725; border: 1px solid #2a3648; border-radius: 8px; color: #e6edf3; font-size: 0.875rem; margin-bottom: 8px; transition: border-color 0.15s; } .quick-password-input:focus { outline: none; border-color: #3b82f6; } .quick-connect-btn { width: 100%; padding: 0.625rem; border: none; border-radius: 8px; font-weight: 600; font-size: 0.875rem; cursor: pointer; transition: all 0.15s; background: linear-gradient(135deg, #10b981, #059669); color: white; } .quick-connect-btn:hover { background: linear-gradient(135deg, #059669, #047857); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3); } .quick-connect-btn:active { transform: translateY(0); } .quick-status-badge { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-top: 8px; } .quick-status-badge.active { background: rgba(16, 185, 129, 0.2); color: #10b981; } .quick-status-badge.inactive { background: rgba(148, 163, 184, 0.2); color: #94a3b8; } /* Login Section Styles */ .login-section { background: rgba(15, 23, 37, 0.5); border: 1px solid #2a3648; border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; } .login-header { margin-bottom: 1.5rem; } .login-header h2 { margin: 0 0 0.5rem 0; font-size: 1.5rem; display: flex; align-items: center; gap: 0.5rem; } .login-header p { margin: 0; color: #94a3b8; font-size: 0.875rem; } .quick-login-badge { display: inline-block; background: linear-gradient(135deg, #10b981, #059669); color: white; font-size: 0.75rem; padding: 0.25rem 0.5rem; border-radius: 4px; font-weight: 600; } .login-form-group { margin-bottom: 1rem; } .login-label { display: block; margin-bottom: 0.5rem; font-weight: 600; font-size: 0.875rem; color: #cbd5e1; } .login-input { width: 100%; padding: 0.75rem; background: #0f1725; border: 1px solid #2a3648; border-radius: 8px; color: #e6edf3; font-size: 0.875rem; transition: border-color 0.15s; } .login-input:focus { outline: none; border-color: #3b82f6; } .login-input:disabled { background: #1a2332; color: #64748b; cursor: not-allowed; } .login-input-readonly { background: rgba(30, 41, 59, 0.5); border-color: #334155; } .login-btn { width: 100%; padding: 0.875rem; border: none; border-radius: 8px; font-weight: 600; font-size: 0.9375rem; cursor: pointer; transition: all 0.15s; margin-top: 0.5rem; } .login-btn-primary { background: linear-gradient(135deg, #10b981, #059669); color: white; } .login-btn-primary:hover { background: linear-gradient(135deg, #059669, #047857); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3); } .login-btn-primary:active { transform: translateY(0); } .login-btn-secondary { background: linear-gradient(135deg, #3b82f6, #9333ea); color: white; } .login-btn-secondary:hover { opacity: 0.9; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); } .login-divider { display: flex; align-items: center; gap: 1rem; margin: 1.5rem 0; color: #64748b; font-size: 0.875rem; } .login-divider::before, .login-divider::after { content: ''; flex: 1; height: 1px; background: #2a3648; } .info-box { background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.3); border-radius: 8px; padding: 0.75rem; margin-top: 1rem; font-size: 0.8125rem; color: #94a3b8; } .info-box strong { color: #60a5fa; } /* Connection Manager Styles */ .conn-section { background: rgba(15, 23, 37, 0.5); border: 1px solid #2a3648; border-radius: 12px; padding: 1.5rem; } .conn-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; } .conn-header h2 { margin: 0; font-size: 1.5rem; } .connections-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; } .conn-card { position: relative; background: rgba(30, 41, 59, 0.7); border: 2px solid #2a3648; border-radius: 12px; padding: 1rem; cursor: pointer; transition: all 0.2s ease; } .conn-card:hover { border-color: #3b82f6; transform: translateY(-2px); box-shadow: 0 8px 16px rgba(59, 130, 246, 0.2); } .conn-card.active { border-color: #10b981; background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(16, 185, 129, 0.05)); } .conn-card.connecting { border-color: #f59e0b; } .conn-status-bar { position: absolute; top: 0; left: 0; right: 0; height: 4px; border-radius: 12px 12px 0 0; background: #2a3648; } .conn-card.active .conn-status-bar { background: linear-gradient(90deg, #10b981, #34d399); } .conn-card.connecting .conn-status-bar { background: linear-gradient(90deg, #f59e0b, #fbbf24); animation: connPulse 1.5s ease-in-out infinite; } @keyframes connPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .conn-card-header { display: flex; justify-content: space-between; align-items: start; margin-top: 8px; margin-bottom: 12px; } .conn-card-title { font-size: 18px; font-weight: 700; color: #e6edf3; } .conn-card-actions { display: flex; gap: 4px; } .conn-icon-btn { background: transparent; border: none; color: #9aa4b2; cursor: pointer; padding: 4px 8px; border-radius: 6px; font-size: 16px; transition: all 0.15s; } .conn-icon-btn:hover { background: #263244; color: #e6edf3; } .conn-card-info { font-size: 14px; color: #9aa4b2; line-height: 1.6; } .conn-info-row { display: flex; gap: 8px; margin-bottom: 4px; } .conn-info-label { font-weight: 600; min-width: 60px; } .conn-status-badge { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-top: 8px; } .conn-status-badge.active { background: rgba(16, 185, 129, 0.2); color: #10b981; } .conn-status-badge.inactive { background: rgba(148, 163, 184, 0.2); color: #94a3b8; } .conn-add-card { background: transparent; border: 2px dashed #2a3648; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; min-height: 140px; color: #9aa4b2; font-weight: 600; } .conn-add-card:hover { border-color: #3b82f6; color: #3b82f6; } .conn-add-icon { font-size: 32px; line-height: 1; } .conn-btn { padding: 10px 20px; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; transition: all 0.15s; font-size: 14px; } .conn-btn-primary { background: linear-gradient(135deg, #3b82f6, #9333ea); color: white; } .conn-btn-primary:hover { opacity: 0.9; } .conn-btn-secondary { background: #2a3648; color: #e6edf3; } .conn-modal { display: none; position: fixed; inset: 0; z-index: 99999; align-items: center; justify-content: center; } .conn-modal[aria-hidden="false"] { display: flex; } .conn-modal__backdrop { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(4px); } .conn-modal__dialog { position: relative; background: #1a2332; border: 1px solid #2a3648; border-radius: 16px; padding: 24px; max-width: 480px; width: 90%; max-height: 90vh; overflow-y: auto; z-index: 1; } .conn-modal__header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .conn-modal__header h3 { margin: 0; font-size: 20px; font-weight: 700; } .conn-close-btn { background: transparent; border: none; color: #9aa4b2; font-size: 28px; cursor: pointer; padding: 0; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 6px; line-height: 1; } .conn-close-btn:hover { background: #263244; color: #e6edf3; } .conn-form-group { margin-bottom: 16px; } .conn-label { display: block; margin-bottom: 6px; font-weight: 600; font-size: 14px; } .conn-input { width: 100%; padding: 10px 12px; background: #0f1725; border: 1px solid #2a3648; border-radius: 8px; color: #e6edf3; font-size: 14px; } .conn-input:focus { outline: none; border-color: #3b82f6; } .conn-pw-wrap { position: relative; display: flex; align-items: center; } .conn-pw-toggle { position: absolute; right: 8px; background: transparent; border: none; color: #9aa4b2; cursor: pointer; padding: 6px; border-radius: 6px; } .conn-pw-toggle:hover { background: #263244; } .conn-form-actions { display: flex; gap: 8px; margin-top: 24px; } .conn-toast-container { position: fixed; right: 16px; bottom: 16px; z-index: 999999; display: flex; flex-direction: column; gap: 8px; } .conn-toast { background: rgba(15, 23, 42, 0.98); border: 1px solid rgba(71, 85, 105, 0.5); color: #e2e8f0; padding: 10px 14px; border-radius: 10px; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.35); font-size: 0.9rem; opacity: 0; transform: translateY(8px); animation: connToastIn 200ms ease forwards; } .conn-toast.success { border-color: rgba(16, 185, 129, 0.6); } .conn-toast.error { border-color: rgba(239, 68, 68, 0.6); } @keyframes connToastIn { to { opacity: 1; transform: translateY(0); } } @keyframes connToastOut { to { opacity: 0; transform: translateY(8px); } } </style> </head> <body> <?php include __DIR__ . '/../core/auth/header.php'; ?> <header class="topbar" aria-label="Top navigation"> <div class="topbar-inner"> <div id="buttonRow" role="tablist" aria-label="App sections"></div> <div id="menuContainer" aria-label="More actions"></div> </div> </header> <main class="container"> <!-- Quick SFTP Login --> <div class="login-section"> <div class="login-header"> <h2>🔐 SFTP Connections</h2> <button class="conn-btn conn-btn-primary" onclick="openConnectionModal()">+ New Connection</button> </div> <div class="connections-grid" style="margin-bottom: 1rem;"> <!-- Quick Connect Card --> <div class="quick-connect-card" id="quickConnectCard" onclick="handleQuickConnectClick(event)"> <div class="quick-status-bar"></div> <div class="quick-connect-header"> <div class="quick-connect-title"> ⚡ DevBrewing Quick Connect <span class="quick-badge">QUICK</span> </div> </div> <div class="quick-connect-info"> <div class="quick-info-row"> <span class="quick-info-label">User:</span> <span><?= htmlspecialchars($_SESSION['username']) ?></span> </div> </div> <form id="quickLoginForm" onclick="event.stopPropagation()"> <input type="password" class="quick-password-input" id="quickPassword" placeholder="Enter SFTP password" required > <button type="submit" class="quick-connect-btn">🚀 Connect</button> </form> <span class="quick-status-badge inactive" id="quickStatusBadge">⚫ Disconnected</span> </div> </div> <!-- Custom Connections Grid --> <div class="connections-grid" id="connectionsGrid"> <!-- Connections will be rendered here --> </div> </div> </main> <!-- Connection Modal --> <div class="conn-modal" id="connectionModal" aria-hidden="true"> <div class="conn-modal__backdrop" onclick="closeConnectionModal()"></div> <div class="conn-modal__dialog"> <div class="conn-modal__header"> <h3 id="modalTitle">New Connection</h3> <button class="conn-close-btn" onclick="closeConnectionModal()">×</button> </div> <form id="connectionForm"> <input type="hidden" id="connectionId"> <div class="conn-form-group"> <label class="conn-label">Connection Name</label> <input type="text" class="conn-input" id="connName" placeholder="My Server" required> </div> <div class="conn-form-group"> <label class="conn-label">Host</label> <input type="text" class="conn-input" id="connHost" placeholder="server.example.com" required> </div> <div class="conn-form-group"> <label class="conn-label">Port</label> <input type="number" class="conn-input" id="connPort" value="22" required> </div> <div class="conn-form-group"> <label class="conn-label">Username</label> <input type="text" class="conn-input" id="connUser" required> </div> <div class="conn-form-group"> <label class="conn-label">Password</label> <div class="conn-pw-wrap"> <input type="password" class="conn-input" id="connPass" required> <button type="button" class="conn-pw-toggle" onclick="toggleConnPassword()">👁️</button> </div> </div> <div class="conn-form-actions"> <button type="submit" class="conn-btn conn-btn-primary">Save Connection</button> <button type="button" class="conn-btn conn-btn-secondary" onclick="closeConnectionModal()">Cancel</button> </div> <div id="formMessage"></div> </form> </div> </div> <!-- Toast Container --> <div class="conn-toast-container" id="connToastContainer"></div> <script> // Global component registry window.AppItems = []; window.AppMenu = []; </script> <!-- Core system --> <script src="/core/js/core.js" defer></script> <script src="<?= asset('/core/js/overlay.js') ?>" defer></script> <script src="<?= asset('filemanager.js') ?>" defer></script> <script src="<?= asset('menu.js') ?>" defer></script> <!-- Quick Login Handler --> <script> // Handle clicking on quick connect card to disconnect window.handleQuickConnectClick = async function(event) { // Don't disconnect if clicking on form elements if (event.target.closest('form') || event.target.closest('input') || event.target.closest('button')) { return; } const connections = getConnections(); const dbConn = connections.find(c => c.id === 'devbrewing_quick'); if (dbConn && dbConn.active) { // Disconnect try { const formData = new FormData(); formData.append('sftp_action', 'disconnect'); await fetch(window.location.href, { method: 'POST', body: formData }); dbConn.active = false; saveConnections(connections); renderConnections(); updateQuickConnectStatus(false); showToast('Disconnected from DevBrewing', 'success'); } catch (err) { showToast('Disconnect failed: ' + err.message, 'error'); } } }; // Update quick connect visual status function updateQuickConnectStatus(isActive) { const card = document.getElementById('quickConnectCard'); const badge = document.getElementById('quickStatusBadge'); if (isActive) { card.classList.add('active'); badge.className = 'quick-status-badge active'; badge.textContent = '🟢 Connected'; } else { card.classList.remove('active'); badge.className = 'quick-status-badge inactive'; badge.textContent = '⚫ Disconnected'; } } document.getElementById('quickLoginForm').addEventListener('submit', async (e) => { e.preventDefault(); const password = document.getElementById('quickPassword').value; const btn = e.target.querySelector('button[type="submit"]'); const originalText = btn.textContent; if (!password) { showToast('❌ Please enter password', 'error'); return; } btn.disabled = true; btn.textContent = '🔄 Connecting...'; try { // ALWAYS disconnect any active connection first const connections = getConnections(); const activeConn = connections.find(c => c.active); if (activeConn) { await disconnectFromServer(); // Wait for disconnect to complete await new Promise(resolve => setTimeout(resolve, 300)); } // Check if DevBrewing connection already exists let dbConn = connections.find(c => c.id === 'devbrewing_quick'); if (!dbConn) { // Create new DevBrewing connection dbConn = { id: 'devbrewing_quick', name: 'DevBrewing SFTP', host: 'files.devbrewing.com', port: 22, username: '<?= htmlspecialchars($_SESSION['username']) ?>', password: password, active: false }; connections.push(dbConn); } else { // Update password dbConn.password = password; } // Now connect using the same method as regular connections const formData = new FormData(); formData.append('sftp_action', 'connect'); formData.append('host', 'files.devbrewing.com'); formData.append('port', 22); formData.append('username', '<?= htmlspecialchars($_SESSION['username']) ?>'); formData.append('password', password); const res = await fetch(window.location.href, { method: 'POST', body: formData }); const data = await res.json(); console.log('Quick connect response:', data); if (data.success) { // ENSURE ONLY this connection is active connections.forEach(c => c.active = false); dbConn.active = true; saveConnections(connections); renderConnections(); updateQuickConnectStatus(true); showToast(`✅ Connected to DevBrewing`, 'success'); // DON'T clear password field - keep it filled // document.getElementById('quickPassword').value = ''; } else { throw new Error(data.message || 'Connection failed'); } } catch (err) { console.error('Quick connect error:', err); showToast(`❌ Connection failed: ${err.message}`, 'error'); } finally { btn.disabled = false; btn.textContent = originalText; } }); // Check status on load and restore password document.addEventListener('DOMContentLoaded', () => { setTimeout(() => { const connections = getConnections(); const dbConn = connections.find(c => c.id === 'devbrewing_quick'); if (dbConn) { updateQuickConnectStatus(dbConn.active || false); // Restore saved password to input field if (dbConn.password) { document.getElementById('quickPassword').value = dbConn.password; } } }, 200); }); // Listen for connection changes to update quick connect status const originalSaveConnections = window.saveConnections; window.saveConnections = function(connections) { originalSaveConnections(connections); const dbConn = connections.find(c => c.id === 'devbrewing_quick'); updateQuickConnectStatus(dbConn?.active || false); }; </script> <!-- Connection Manager Script --> <script> (function () { const STORAGE_KEY = 'sftp_connections'; window.openConnectionModal = function(connectionId = null) { const modal = document.getElementById('connectionModal'); const form = document.getElementById('connectionForm'); const title = document.getElementById('modalTitle'); form.reset(); document.getElementById('formMessage').innerHTML = ''; if (connectionId) { const connections = getConnections(); const conn = connections.find(c => c.id === connectionId); if (conn) { title.textContent = 'Edit Connection'; document.getElementById('connectionId').value = conn.id; document.getElementById('connName').value = conn.name; document.getElementById('connHost').value = conn.host; document.getElementById('connPort').value = conn.port; document.getElementById('connUser').value = conn.username; document.getElementById('connPass').value = conn.password; } } else { title.textContent = 'New Connection'; document.getElementById('connectionId').value = ''; } modal.setAttribute('aria-hidden', 'false'); }; window.closeConnectionModal = function() { document.getElementById('connectionModal').setAttribute('aria-hidden', 'true'); }; window.toggleConnPassword = function() { const input = document.getElementById('connPass'); const btn = event.target; input.type = input.type === 'password' ? 'text' : 'password'; btn.textContent = input.type === 'password' ? '👁️' : '🙈'; }; window.getConnections = function() { const data = localStorage.getItem(STORAGE_KEY); return data ? JSON.parse(data) : []; }; window.saveConnections = function(connections) { localStorage.setItem(STORAGE_KEY, JSON.stringify(connections)); }; window.showToast = function(message, type = 'success', timeout = 2500) { const container = document.getElementById('connToastContainer'); if (!container) return; const toast = document.createElement('div'); toast.className = `conn-toast ${type}`; toast.textContent = message; container.appendChild(toast); setTimeout(() => { toast.style.animation = 'connToastOut 160ms ease forwards'; setTimeout(() => container.removeChild(toast), 170); }, timeout); }; window.renderConnections = function() { const grid = document.getElementById('connectionsGrid'); if (!grid) return; const connections = getConnections(); grid.innerHTML = ''; if (connections.length === 0) { grid.innerHTML = ` <p style="color: #64748b; text-align: center; padding: 2rem;"> No saved connections yet.<br>Click "+ New Connection" to add one. </p> `; return; } connections.forEach(conn => { // Skip DevBrewing Quick connection - it's shown separately if (conn.id === 'devbrewing_quick') { return; } const card = document.createElement('div'); card.className = `conn-card ${conn.active ? 'active' : ''}`; card.innerHTML = ` <div class="conn-status-bar"></div> <div class="conn-card-header"> <div class="conn-card-title">${escapeHtml(conn.name)}</div> <div class="conn-card-actions"> <button class="conn-icon-btn" onclick="editConnection('${conn.id}')" title="Edit">✏️</button> <button class="conn-icon-btn" onclick="deleteConnection('${conn.id}')" title="Delete">🗑️</button> </div> </div> <div class="conn-card-info"> <div class="conn-info-row"> <span class="conn-info-label">Host:</span> <span>${escapeHtml(conn.host)}</span> </div> <div class="conn-info-row"> <span class="conn-info-label">User:</span> <span>${escapeHtml(conn.username)}</span> </div> <div class="conn-info-row"> <span class="conn-info-label">Port:</span> <span>${conn.port}</span> </div> </div> <span class="conn-status-badge ${conn.active ? 'active' : 'inactive'}"> ${conn.active ? '🟢 Connected' : '⚫ Disconnected'} </span> `; card.addEventListener('click', (e) => { if (!e.target.closest('.conn-icon-btn')) { if (conn.active) { disconnectConnection(conn.id); } else { connectToServer(conn.id, card); } } }); grid.appendChild(card); }); }; window.editConnection = function(id) { openConnectionModal(id); }; window.deleteConnection = async function(id) { if (!confirm('Are you sure you want to delete this connection?')) return; const connections = getConnections(); const deleted = connections.find(c => c.id === id); if (deleted && deleted.active) { await disconnectFromServer(); } const filtered = connections.filter(c => c.id !== id); saveConnections(filtered); renderConnections(); showToast('Connection deleted', 'success'); }; window.disconnectConnection = async function(id) { try { const formData = new FormData(); formData.append('sftp_action', 'disconnect'); await fetch(window.location.href, { method: 'POST', body: formData }); const connections = getConnections(); const conn = connections.find(c => c.id === id); if (conn) { conn.active = false; saveConnections(connections); renderConnections(); showToast(`Disconnected from ${conn.name}`, 'success'); } } catch (err) { showToast('Disconnect failed: ' + err.message, 'error'); } }; async function connectToServer(id, card) { const connections = getConnections(); const conn = connections.find(c => c.id === id); if (!conn) return; card.classList.add('connecting'); try { // ALWAYS disconnect any active connection first const activeConn = connections.find(c => c.active); if (activeConn) { await disconnectFromServer(); // Wait a moment for disconnect to complete await new Promise(resolve => setTimeout(resolve, 300)); } const formData = new FormData(); formData.append('sftp_action', 'connect'); formData.append('host', conn.host); formData.append('port', conn.port); formData.append('username', conn.username); formData.append('password', conn.password); const res = await fetch(window.location.href, { method: 'POST', body: formData }); const data = await res.json(); console.log('Connection response:', data); if (data.success) { // Ensure ONLY this connection is active connections.forEach(c => c.active = false); conn.active = true; saveConnections(connections); renderConnections(); showToast(`✅ Connected to ${conn.name}`, 'success'); } else { throw new Error(data.message); } } catch (err) { showToast(`❌ Connection failed: ${err.message}`, 'error'); } finally { card.classList.remove('connecting'); } } async function disconnectFromServer() { try { const formData = new FormData(); formData.append('sftp_action', 'disconnect'); await fetch(window.location.href, { method: 'POST', body: formData }); const connections = getConnections(); connections.forEach(c => c.active = false); saveConnections(connections); } catch (err) { console.error('Disconnect error:', err); } } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } document.addEventListener('submit', (e) => { if (e.target.id === 'connectionForm') { e.preventDefault(); const connections = getConnections(); const id = document.getElementById('connectionId').value || Date.now().toString(); const name = document.getElementById('connName').value; const host = document.getElementById('connHost').value; const port = parseInt(document.getElementById('connPort').value); const username = document.getElementById('connUser').value; const password = document.getElementById('connPass').value; const existingIndex = connections.findIndex(c => c.id === id); const connection = { id, name, host, port, username, password, active: existingIndex >= 0 ? connections[existingIndex].active : false }; if (existingIndex >= 0) { connections[existingIndex] = connection; showToast('Connection updated', 'success'); } else { connections.push(connection); showToast('Connection saved', 'success'); } saveConnections(connections); renderConnections(); closeConnectionModal(); } }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { const modal = document.getElementById('connectionModal'); if (modal && modal.getAttribute('aria-hidden') === 'false') { closeConnectionModal(); } } }); document.addEventListener('DOMContentLoaded', () => { renderConnections(); setTimeout(async () => { try { const formData = new FormData(); formData.append('sftp_action', 'status'); const res = await fetch(window.location.href, { method: 'POST', body: formData }); const data = await res.json(); const connections = getConnections(); const hasActiveLocal = connections.some(c => c.active); if (!data.data?.connected && hasActiveLocal) { connections.forEach(c => c.active = false); saveConnections(connections); renderConnections(); } } catch (err) { console.error('Status check failed:', err); } }, 100); }); console.log('[connectionmanager] Loaded - dual login system with quick connect'); })(); </script> <!-- Render chips + menu --> <script defer> document.addEventListener('DOMContentLoaded', () => { const row = document.getElementById('buttonRow'); row.innerHTML = ''; (window.AppItems || []).forEach((item, i) => { const btn = document.createElement('button'); btn.className = 'chip'; btn.textContent = item.title || `Item ${i+1}`; btn.onclick = () => window.AppOverlay && AppOverlay.open(window.AppItems, i, btn); row.appendChild(btn); }); const menuContainer = document.getElementById('menuContainer'); menuContainer.innerHTML = ''; const menuItems = window.AppMenu || []; if (menuItems.length > 0) { const trigger = document.createElement('button'); trigger.className = 'chip menu-trigger'; trigger.textContent = '⋮'; menuContainer.appendChild(trigger); const dropdown = document.createElement('div'); dropdown.className = 'menu-list'; menuContainer.appendChild(dropdown); menuItems.forEach((m, idx) => { const item = document.createElement('button'); item.className = 'menu-item'; item.textContent = m.label || `Action ${idx+1}`; item.onclick = () => { dropdown.classList.remove('open'); m.action && m.action(); }; dropdown.appendChild(item); }); trigger.addEventListener('click', (e)=>{ e.stopPropagation(); dropdown.classList.toggle('open'); }); document.addEventListener('click', (e)=>{ if (!menuContainer.contains(e.target)) dropdown.classList.remove('open'); }); } }); </script> </body> </html>