feat: add interactive terminal support for containers via WebSocket
All checks were successful
Build and Push Docker Images / docker (push) Successful in 30s
All checks were successful
Build and Push Docker Images / docker (push) Successful in 30s
- Implemented WebSocket endpoint for executing interactive shells in Docker containers. - Added ticket-based authentication for terminal access to enhance security. - Updated frontend to support terminal interactions, including lazy loading of terminal components. - Enhanced backend to manage terminal session tickets and settings for enabling/disabling terminal access. - Updated Nginx configuration to support WebSocket connections for terminal sessions. - Added necessary dependencies for terminal functionality in the frontend. - Improved user interface to include terminal access controls and feedback on terminal status.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
|
||||
import { useState, useEffect, useCallback, useMemo, useRef, lazy, Suspense } from 'react'
|
||||
import { ServerCrash, SearchX, Plus, RefreshCw } from 'lucide-react'
|
||||
import { fetchAllStatus, containerAction, addVps, deleteVps, fetchLogs, authStatus, getToken, setToken, composeUpdate, updateVps, updateAgent, exportVps } from './api/client'
|
||||
import Header from './components/Header'
|
||||
@@ -16,6 +16,9 @@ import { useToast } from './components/ui/Toast'
|
||||
import { Button, EmptyState, Skeleton } from './components/ui/controls'
|
||||
import { copyText, downloadText } from './lib/clipboard'
|
||||
|
||||
// xterm.js pèse ~300 kB : chargé seulement à l'ouverture d'un terminal.
|
||||
const TerminalModal = lazy(() => import('./components/TerminalModal'))
|
||||
|
||||
const INTERVAL_OPTIONS = [
|
||||
{ label: '10 s', value: 10_000 },
|
||||
{ label: '30 s', value: 30_000 },
|
||||
@@ -60,6 +63,7 @@ export default function App() {
|
||||
const [page, setPage] = useState('main') // 'main' | 'profile' | 'admin'
|
||||
const [isFirstUser, setIsFirstUser] = useState(false)
|
||||
const [passkeyEnabled, setPasskeyEnabled] = useState(false)
|
||||
const [terminalEnabled, setTerminalEnabled] = useState(true)
|
||||
const [authChecked, setAuthChecked] = useState(false)
|
||||
|
||||
const [vpsList, setVpsList] = useState([])
|
||||
@@ -117,13 +121,15 @@ export default function App() {
|
||||
const [updateLoading, setUpdateLoading] = useState(false)
|
||||
|
||||
const [statsModal, setStatsModal] = useState(null) // { vpsId, vpsName }
|
||||
const [terminalModal, setTerminalModal] = useState(null) // { vps, container }
|
||||
|
||||
// Vérifie si des utilisateurs existent (pour afficher login ou register)
|
||||
useEffect(() => {
|
||||
authStatus()
|
||||
.then(({ has_users, passkey_enabled }) => {
|
||||
.then(({ has_users, passkey_enabled, terminal_enabled }) => {
|
||||
setIsFirstUser(!has_users)
|
||||
setPasskeyEnabled(!!passkey_enabled)
|
||||
setTerminalEnabled(terminal_enabled !== false)
|
||||
})
|
||||
.catch(() => setIsFirstUser(false))
|
||||
.finally(() => setAuthChecked(true))
|
||||
@@ -200,7 +206,7 @@ export default function App() {
|
||||
}, [token, username])
|
||||
|
||||
// Raccourcis clavier : « / » cible la recherche, « r » actualise.
|
||||
const modalOpen = !!(logsModal || updateModal || showAddVps || editVps || statsModal || deleteTarget)
|
||||
const modalOpen = !!(logsModal || updateModal || showAddVps || editVps || statsModal || deleteTarget || terminalModal)
|
||||
useEffect(() => {
|
||||
if (!token || page !== 'main' || modalOpen) return
|
||||
const onKeyDown = (e) => {
|
||||
@@ -369,6 +375,10 @@ export default function App() {
|
||||
)
|
||||
}
|
||||
|
||||
// Le terminal donne un shell root dans le conteneur : réservé aux admins, et
|
||||
// désactivable globalement depuis la page d'administration.
|
||||
const canUseTerminal = role === 'admin' && terminalEnabled
|
||||
|
||||
// Statistiques globales
|
||||
const totalOnline = vpsList.filter(v => v.online).length
|
||||
const totalContainers = vpsList.reduce((acc, v) => acc + v.containers.length, 0)
|
||||
@@ -511,6 +521,7 @@ export default function App() {
|
||||
onStats={(vpsId, vpsName) => setStatsModal({ vpsId, vpsName })}
|
||||
onUpdateAgent={handleUpdateAgent}
|
||||
onExport={handleExportVps}
|
||||
onTerminal={canUseTerminal ? (target, containerRow) => setTerminalModal({ vps: target, container: containerRow }) : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -563,6 +574,21 @@ export default function App() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Terminal interactif */}
|
||||
{terminalModal && (
|
||||
<Suspense fallback={
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm text-sm text-gray-400">
|
||||
Chargement du terminal…
|
||||
</div>
|
||||
}>
|
||||
<TerminalModal
|
||||
vps={terminalModal.vps}
|
||||
container={terminalModal.container}
|
||||
onClose={() => setTerminalModal(null)}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
{/* Confirmation de suppression */}
|
||||
{deleteTarget && (
|
||||
<ConfirmDialog
|
||||
|
||||
Reference in New Issue
Block a user