388 lines
13 KiB
Python
388 lines
13 KiB
Python
"""Main application window."""
|
|
|
|
from PyQt6.QtWidgets import (
|
|
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
|
QSplitter, QListWidget, QListWidgetItem, QLabel,
|
|
QPushButton, QGroupBox, QTextEdit, QStatusBar,
|
|
QMessageBox, QProgressDialog
|
|
)
|
|
from PyQt6.QtCore import Qt, QTimer
|
|
from PyQt6.QtGui import QIcon, QColor
|
|
|
|
from config import APP_NAME, APP_VERSION
|
|
from services.api_client import APIClient, Gateway, Endpoint
|
|
from services.vpn_manager import VPNManager
|
|
from .login_dialog import LoginDialog
|
|
|
|
|
|
class MainWindow(QMainWindow):
|
|
"""Main application window."""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.setWindowTitle(f"{APP_NAME} v{APP_VERSION}")
|
|
self.setMinimumSize(900, 600)
|
|
|
|
self.api_client: APIClient | None = None
|
|
self.vpn_manager = VPNManager()
|
|
self.current_gateway: Gateway | None = None
|
|
self.current_endpoint: Endpoint | None = None
|
|
self.current_connection_id: int | None = None
|
|
|
|
self._setup_ui()
|
|
self._setup_timers()
|
|
|
|
# Show login dialog on start
|
|
QTimer.singleShot(100, self._show_login)
|
|
|
|
def _setup_ui(self):
|
|
"""Setup UI components."""
|
|
central = QWidget()
|
|
self.setCentralWidget(central)
|
|
layout = QVBoxLayout(central)
|
|
|
|
# Toolbar
|
|
toolbar = QHBoxLayout()
|
|
self.user_label = QLabel("Not logged in")
|
|
toolbar.addWidget(self.user_label)
|
|
toolbar.addStretch()
|
|
|
|
self.refresh_button = QPushButton("Refresh")
|
|
self.refresh_button.clicked.connect(self._refresh_data)
|
|
self.refresh_button.setEnabled(False)
|
|
toolbar.addWidget(self.refresh_button)
|
|
|
|
self.logout_button = QPushButton("Logout")
|
|
self.logout_button.clicked.connect(self._logout)
|
|
self.logout_button.setEnabled(False)
|
|
toolbar.addWidget(self.logout_button)
|
|
|
|
layout.addLayout(toolbar)
|
|
|
|
# Main content splitter
|
|
splitter = QSplitter(Qt.Orientation.Horizontal)
|
|
|
|
# Gateway list
|
|
gateway_group = QGroupBox("Gateways")
|
|
gateway_layout = QVBoxLayout(gateway_group)
|
|
self.gateway_list = QListWidget()
|
|
self.gateway_list.currentItemChanged.connect(self._on_gateway_selected)
|
|
gateway_layout.addWidget(self.gateway_list)
|
|
splitter.addWidget(gateway_group)
|
|
|
|
# Endpoint list
|
|
endpoint_group = QGroupBox("Endpoints")
|
|
endpoint_layout = QVBoxLayout(endpoint_group)
|
|
self.endpoint_list = QListWidget()
|
|
self.endpoint_list.currentItemChanged.connect(self._on_endpoint_selected)
|
|
endpoint_layout.addWidget(self.endpoint_list)
|
|
splitter.addWidget(endpoint_group)
|
|
|
|
# Connection panel
|
|
connection_group = QGroupBox("Connection")
|
|
connection_layout = QVBoxLayout(connection_group)
|
|
|
|
self.gateway_info = QLabel("Select a gateway")
|
|
connection_layout.addWidget(self.gateway_info)
|
|
|
|
self.endpoint_info = QLabel("Select an endpoint")
|
|
connection_layout.addWidget(self.endpoint_info)
|
|
|
|
self.status_label = QLabel("Status: Disconnected")
|
|
self.status_label.setStyleSheet("font-weight: bold;")
|
|
connection_layout.addWidget(self.status_label)
|
|
|
|
connection_layout.addStretch()
|
|
|
|
# Connect button
|
|
button_layout = QHBoxLayout()
|
|
self.connect_button = QPushButton("Connect")
|
|
self.connect_button.setEnabled(False)
|
|
self.connect_button.clicked.connect(self._toggle_connection)
|
|
self.connect_button.setMinimumHeight(40)
|
|
button_layout.addWidget(self.connect_button)
|
|
connection_layout.addLayout(button_layout)
|
|
|
|
# Log area
|
|
self.log_area = QTextEdit()
|
|
self.log_area.setReadOnly(True)
|
|
self.log_area.setMaximumHeight(150)
|
|
connection_layout.addWidget(self.log_area)
|
|
|
|
splitter.addWidget(connection_group)
|
|
|
|
# Set splitter sizes
|
|
splitter.setSizes([200, 200, 400])
|
|
layout.addWidget(splitter)
|
|
|
|
# Status bar
|
|
self.status_bar = QStatusBar()
|
|
self.setStatusBar(self.status_bar)
|
|
self.status_bar.showMessage("Ready")
|
|
|
|
def _setup_timers(self):
|
|
"""Setup periodic timers."""
|
|
# Refresh gateway status every 30 seconds
|
|
self.status_timer = QTimer()
|
|
self.status_timer.timeout.connect(self._refresh_gateway_status)
|
|
self.status_timer.setInterval(30000)
|
|
|
|
def _show_login(self):
|
|
"""Show login dialog."""
|
|
dialog = LoginDialog(self)
|
|
|
|
while True:
|
|
if dialog.exec() != LoginDialog.DialogCode.Accepted:
|
|
self.close()
|
|
return
|
|
|
|
server, username, password = dialog.get_credentials()
|
|
|
|
if not all([server, username, password]):
|
|
dialog.show_error("Please fill all fields")
|
|
continue
|
|
|
|
# Try to connect
|
|
self.api_client = APIClient(server)
|
|
if self.api_client.login(username, password):
|
|
dialog.accept()
|
|
self._on_login_success(username)
|
|
break
|
|
else:
|
|
dialog.show_error("Login failed. Check credentials.")
|
|
|
|
def _on_login_success(self, username: str):
|
|
"""Handle successful login."""
|
|
self.user_label.setText(f"Logged in as: {username}")
|
|
self.refresh_button.setEnabled(True)
|
|
self.logout_button.setEnabled(True)
|
|
self.status_bar.showMessage("Connected to server")
|
|
self._log("Connected to server")
|
|
|
|
# Load data
|
|
self._refresh_data()
|
|
|
|
# Start status timer
|
|
self.status_timer.start()
|
|
|
|
def _logout(self):
|
|
"""Logout and show login dialog."""
|
|
# Disconnect VPN if connected
|
|
if self.vpn_manager.is_connected():
|
|
self.vpn_manager.disconnect()
|
|
|
|
if self.api_client:
|
|
self.api_client.logout()
|
|
self.api_client.close()
|
|
self.api_client = None
|
|
|
|
# Clear UI
|
|
self.gateway_list.clear()
|
|
self.endpoint_list.clear()
|
|
self.user_label.setText("Not logged in")
|
|
self.refresh_button.setEnabled(False)
|
|
self.logout_button.setEnabled(False)
|
|
self.connect_button.setEnabled(False)
|
|
self.status_timer.stop()
|
|
|
|
self._log("Logged out")
|
|
|
|
# Show login dialog
|
|
QTimer.singleShot(100, self._show_login)
|
|
|
|
def _refresh_data(self):
|
|
"""Refresh all data from server."""
|
|
if not self.api_client:
|
|
return
|
|
|
|
self._log("Refreshing data...")
|
|
self.status_bar.showMessage("Refreshing...")
|
|
|
|
# Load gateways
|
|
gateways = self.api_client.get_gateways()
|
|
self.gateway_list.clear()
|
|
|
|
for gateway in gateways:
|
|
item = QListWidgetItem()
|
|
item.setText(f"{gateway.name}")
|
|
item.setData(Qt.ItemDataRole.UserRole, gateway)
|
|
|
|
# Set color based on status
|
|
if gateway.is_online:
|
|
item.setForeground(QColor("green"))
|
|
item.setToolTip(f"Online - {gateway.location or 'No location'}")
|
|
else:
|
|
item.setForeground(QColor("gray"))
|
|
item.setToolTip("Offline")
|
|
|
|
self.gateway_list.addItem(item)
|
|
|
|
self.status_bar.showMessage(f"Loaded {len(gateways)} gateways")
|
|
self._log(f"Loaded {len(gateways)} gateways")
|
|
|
|
def _refresh_gateway_status(self):
|
|
"""Refresh only gateway online status."""
|
|
if not self.api_client:
|
|
return
|
|
|
|
status_list = self.api_client.get_gateways_status()
|
|
status_map = {s["id"]: s["is_online"] for s in status_list}
|
|
|
|
for i in range(self.gateway_list.count()):
|
|
item = self.gateway_list.item(i)
|
|
gateway: Gateway = item.data(Qt.ItemDataRole.UserRole)
|
|
if gateway.id in status_map:
|
|
is_online = status_map[gateway.id]
|
|
if is_online:
|
|
item.setForeground(QColor("green"))
|
|
else:
|
|
item.setForeground(QColor("gray"))
|
|
|
|
def _on_gateway_selected(self, current, previous):
|
|
"""Handle gateway selection."""
|
|
if not current:
|
|
self.endpoint_list.clear()
|
|
self.current_gateway = None
|
|
self.gateway_info.setText("Select a gateway")
|
|
return
|
|
|
|
gateway: Gateway = current.data(Qt.ItemDataRole.UserRole)
|
|
self.current_gateway = gateway
|
|
|
|
self.gateway_info.setText(
|
|
f"Gateway: {gateway.name}\n"
|
|
f"Type: {gateway.router_type}\n"
|
|
f"Status: {'Online' if gateway.is_online else 'Offline'}\n"
|
|
f"Location: {gateway.location or 'N/A'}"
|
|
)
|
|
|
|
# Load endpoints
|
|
self._load_endpoints(gateway.id)
|
|
|
|
def _load_endpoints(self, gateway_id: int):
|
|
"""Load endpoints for gateway."""
|
|
if not self.api_client:
|
|
return
|
|
|
|
endpoints = self.api_client.get_endpoints(gateway_id)
|
|
self.endpoint_list.clear()
|
|
|
|
for endpoint in endpoints:
|
|
item = QListWidgetItem()
|
|
app_name = endpoint.application_name or "Custom"
|
|
item.setText(f"{endpoint.name} ({app_name})")
|
|
item.setToolTip(f"{endpoint.internal_ip}:{endpoint.port} ({endpoint.protocol})")
|
|
item.setData(Qt.ItemDataRole.UserRole, endpoint)
|
|
self.endpoint_list.addItem(item)
|
|
|
|
def _on_endpoint_selected(self, current, previous):
|
|
"""Handle endpoint selection."""
|
|
if not current:
|
|
self.current_endpoint = None
|
|
self.endpoint_info.setText("Select an endpoint")
|
|
self.connect_button.setEnabled(False)
|
|
return
|
|
|
|
endpoint: Endpoint = current.data(Qt.ItemDataRole.UserRole)
|
|
self.current_endpoint = endpoint
|
|
|
|
self.endpoint_info.setText(
|
|
f"Endpoint: {endpoint.name}\n"
|
|
f"Address: {endpoint.internal_ip}:{endpoint.port}\n"
|
|
f"Protocol: {endpoint.protocol.upper()}\n"
|
|
f"Application: {endpoint.application_name or 'N/A'}"
|
|
)
|
|
|
|
# Enable connect button if gateway is online
|
|
if self.current_gateway and self.current_gateway.is_online:
|
|
self.connect_button.setEnabled(True)
|
|
else:
|
|
self.connect_button.setEnabled(False)
|
|
|
|
def _toggle_connection(self):
|
|
"""Toggle VPN connection."""
|
|
if self.vpn_manager.is_connected():
|
|
self._disconnect()
|
|
else:
|
|
self._connect()
|
|
|
|
def _connect(self):
|
|
"""Establish VPN connection."""
|
|
if not self.api_client or not self.current_gateway or not self.current_endpoint:
|
|
return
|
|
|
|
self._log(f"Connecting to {self.current_endpoint.name}...")
|
|
self.status_label.setText("Status: Connecting...")
|
|
self.connect_button.setEnabled(False)
|
|
|
|
# Request connection from server
|
|
result = self.api_client.connect(
|
|
self.current_gateway.id,
|
|
self.current_endpoint.id
|
|
)
|
|
|
|
if not result.get("success"):
|
|
self._log(f"Error: {result.get('message')}")
|
|
self.status_label.setText("Status: Connection Failed")
|
|
self.connect_button.setEnabled(True)
|
|
return
|
|
|
|
self.current_connection_id = result.get("connection_id")
|
|
vpn_config = result.get("vpn_config")
|
|
|
|
if not vpn_config:
|
|
self._log("Error: No VPN config received")
|
|
self.status_label.setText("Status: Error")
|
|
self.connect_button.setEnabled(True)
|
|
return
|
|
|
|
# Start VPN connection
|
|
status = self.vpn_manager.connect(vpn_config)
|
|
|
|
if status.connected:
|
|
self._log("VPN connected!")
|
|
self.status_label.setText("Status: Connected")
|
|
self.status_label.setStyleSheet("font-weight: bold; color: green;")
|
|
self.connect_button.setText("Disconnect")
|
|
self.connect_button.setEnabled(True)
|
|
|
|
self._log(f"Target: {result.get('target_ip')}:{result.get('target_port')}")
|
|
else:
|
|
self._log(f"VPN Error: {status.error}")
|
|
self.status_label.setText("Status: VPN Failed")
|
|
self.connect_button.setEnabled(True)
|
|
|
|
def _disconnect(self):
|
|
"""Disconnect VPN."""
|
|
self._log("Disconnecting...")
|
|
|
|
# Disconnect VPN
|
|
self.vpn_manager.disconnect()
|
|
|
|
# Notify server
|
|
if self.api_client and self.current_connection_id:
|
|
self.api_client.disconnect(self.current_connection_id)
|
|
|
|
self.current_connection_id = None
|
|
self.status_label.setText("Status: Disconnected")
|
|
self.status_label.setStyleSheet("font-weight: bold; color: black;")
|
|
self.connect_button.setText("Connect")
|
|
self._log("Disconnected")
|
|
|
|
def _log(self, message: str):
|
|
"""Add message to log area."""
|
|
from datetime import datetime
|
|
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
self.log_area.append(f"[{timestamp}] {message}")
|
|
|
|
def closeEvent(self, event):
|
|
"""Handle window close."""
|
|
# Disconnect VPN if connected
|
|
if self.vpn_manager.is_connected():
|
|
self.vpn_manager.disconnect()
|
|
|
|
if self.api_client:
|
|
self.api_client.close()
|
|
|
|
event.accept()
|