feat: add interactive terminal support for containers via WebSocket
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:
jeanotx32
2026-08-01 01:49:26 -04:00
parent f37b639226
commit e277e99155
15 changed files with 732 additions and 24 deletions

View File

@@ -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