feat: Implement web-based SSH terminal with session management, command logging, and host interaction.
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
This commit is contained in:
+22
-10
@@ -202,9 +202,8 @@ async def _verify_history_access(
|
||||
if current_user:
|
||||
return True
|
||||
|
||||
token = _get_session_token_from_request(request, host_id, token=request.query_params.get("token"))
|
||||
if not token:
|
||||
return False
|
||||
# Try query param first (generic)
|
||||
q_token = request.query_params.get("token")
|
||||
|
||||
session_repo = TerminalSessionRepository(db_session)
|
||||
# Try by ID first
|
||||
@@ -218,7 +217,9 @@ async def _verify_history_access(
|
||||
sessions = await session_repo.list_active_for_host(host.id)
|
||||
|
||||
for s in sessions:
|
||||
if terminal_service.verify_token(token, s.token_hash):
|
||||
# Check if token is in query param OR in the session-specific cookie
|
||||
token = q_token or _get_session_token_from_request(request, s.id)
|
||||
if token and terminal_service.verify_token(token, s.token_hash):
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -1111,7 +1112,7 @@ async def get_terminal_connect_page(
|
||||
|
||||
// ---- Panel open/close ----
|
||||
function toggleHistory() {
|
||||
if (historyPanelOpen && !historyPanelPinned) { closeHistoryPanel(); return; }
|
||||
if (historyPanelOpen) { closeHistoryPanel(true); return; }
|
||||
const panel = document.getElementById('terminalHistoryPanel');
|
||||
const btn = document.getElementById('btnHistory');
|
||||
panel.style.display = 'flex'; panel.classList.add('open'); btn.classList.add('active');
|
||||
@@ -1120,8 +1121,8 @@ async def get_terminal_connect_page(
|
||||
if (si) { si.focus(); si.select(); }
|
||||
if (historyData.length === 0) loadHistory();
|
||||
}
|
||||
function closeHistoryPanel() {
|
||||
if (historyPanelPinned) return;
|
||||
function closeHistoryPanel(force = false) {
|
||||
if (historyPanelPinned && !force) return;
|
||||
const panel = document.getElementById('terminalHistoryPanel');
|
||||
const btn = document.getElementById('btnHistory');
|
||||
panel.classList.remove('open'); btn.classList.remove('active');
|
||||
@@ -1174,8 +1175,9 @@ async def get_terminal_connect_page(
|
||||
const cmd = historyData[i];
|
||||
if (!cmd || !cmd.command_hash) return;
|
||||
const newPinned = !cmd.is_pinned;
|
||||
const hId = cmd.host_id || HOST_ID;
|
||||
try {
|
||||
const res = await fetch(`/api/terminal/${HOST_ID}/command-history/${cmd.command_hash}/pin?token=${encodeURIComponent(TOKEN)}`, {
|
||||
const res = await fetch(`/api/terminal/${hId}/command-history/${cmd.command_hash}/pin?token=${encodeURIComponent(TOKEN)}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + TOKEN },
|
||||
body: JSON.stringify({ is_pinned: newPinned })
|
||||
@@ -1214,10 +1216,10 @@ async def get_terminal_connect_page(
|
||||
const list=document.getElementById('terminalHistoryList'),q=historySearchQuery||'';
|
||||
if(!historyData.length){list.innerHTML=`<div class="terminal-history-empty"><i class="fas fa-terminal"></i><span>${q?`Aucun résultat pour "${escH(q)}"`:historyPinnedOnly?'Aucune commande épinglée':'Aucune commande'}</span></div>`;return;}
|
||||
list.innerHTML=historyData.map((cmd,i)=>{
|
||||
const c=cmd.command||'',ta=relTime(cmd.last_used||cmd.created_at),ec=cmd.execution_count||1,sel=i===historySelectedIndex,pinned=cmd.is_pinned;
|
||||
const c=cmd.command||'',ta=relTime(cmd.last_used||cmd.created_at),ec=cmd.execution_count||1,sel=i===historySelectedIndex,pinned=cmd.is_pinned,hn=cmd.host_name||'';
|
||||
let dc=escH(c.length>80?c.substring(0,80)+'...':c);
|
||||
if(q)dc=dc.replace(new RegExp(`(${escRE(q)})`,'gi'),'<mark>$1</mark>');
|
||||
return `<div class="terminal-history-item${sel?' selected':''}" data-index="${i}" onclick="execH(${i})" title="${escH(c)}"><div class="terminal-history-cmd"><code>${pinned?'<i class="fas fa-thumbtack" style="color:#fbbf24;margin-right:4px;"></i>':''}${dc}</code></div><div class="terminal-history-meta"><span class="terminal-history-time">${ta}</span>${ec>1?`<span class="terminal-history-count">×${ec}</span>`:''}</div><div class="terminal-history-actions-inline"><button class="terminal-history-action" onclick="event.stopPropagation();togglePinH(${i})" title="${pinned?'Désépingler':'Épingler'}"><i class="fas fa-thumbtack" style="${pinned?'color:#fbbf24':''}"></i></button><button class="terminal-history-action" onclick="event.stopPropagation();copyH(${i})" title="Copier"><i class="fas fa-copy"></i></button><button class="terminal-history-action terminal-history-action-execute" onclick="event.stopPropagation();execH(${i})" title="Exécuter"><i class="fas fa-play"></i></button></div></div>`;
|
||||
return `<div class="terminal-history-item${sel?' selected':''}" data-index="${i}" onclick="execH(${i})" title="${escH(c)}"><div class="terminal-history-cmd"><code>${pinned?'<i class="fas fa-thumbtack" style="color:#fbbf24;margin-right:4px;"></i>':''}${dc}</code></div><div class="terminal-history-meta"><span class="terminal-history-time">${ta}</span>${ec>1?`<span class="terminal-history-count">×${ec}</span>`:''}${hn?`<span class="terminal-history-host">${escH(hn)}</span>`:''}</div><div class="terminal-history-actions-inline"><button class="terminal-history-action" onclick="event.stopPropagation();togglePinH(${i})" title="${pinned?'Désépingler':'Épingler'}"><i class="fas fa-thumbtack" style="${pinned?'color:#fbbf24':''}"></i></button><button class="terminal-history-action" onclick="event.stopPropagation();copyH(${i})" title="Copier"><i class="fas fa-copy"></i></button><button class="terminal-history-action terminal-history-action-execute" onclick="event.stopPropagation();execH(${i})" title="Exécuter"><i class="fas fa-play"></i></button></div></div>`;
|
||||
}).join('');
|
||||
if(historySelectedIndex>=0){const s=list.querySelector('.selected');if(s)s.scrollIntoView({block:'nearest',behavior:'smooth'});}
|
||||
}
|
||||
@@ -1420,9 +1422,12 @@ async def get_host_command_history(
|
||||
CommandHistoryItem(
|
||||
id=log.id,
|
||||
command=log.command,
|
||||
command_hash=log.command_hash,
|
||||
created_at=log.created_at,
|
||||
host_id=log.host_id,
|
||||
host_name=log.host_name,
|
||||
username=log.username,
|
||||
is_pinned=log.is_pinned,
|
||||
)
|
||||
for log in logs
|
||||
]
|
||||
@@ -1597,6 +1602,8 @@ async def toggle_command_pin(
|
||||
async def get_global_command_history(
|
||||
request: Request,
|
||||
query: Optional[str] = None,
|
||||
time_filter: str = "all",
|
||||
pinned_only: bool = False,
|
||||
host_id: Optional[str] = None,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
@@ -1623,6 +1630,8 @@ async def get_global_command_history(
|
||||
cmd_repo = TerminalCommandLogRepository(db_session)
|
||||
logs = await cmd_repo.list_global(
|
||||
query=query,
|
||||
time_filter=time_filter,
|
||||
pinned_only=pinned_only,
|
||||
host_id=host_id,
|
||||
user_id=str(user_id) if user_id else None,
|
||||
limit=min(limit, 100),
|
||||
@@ -1633,9 +1642,12 @@ async def get_global_command_history(
|
||||
CommandHistoryItem(
|
||||
id=log.id,
|
||||
command=log.command,
|
||||
command_hash=log.command_hash,
|
||||
created_at=log.created_at,
|
||||
host_id=log.host_id,
|
||||
host_name=log.host_name,
|
||||
username=log.username,
|
||||
is_pinned=log.is_pinned,
|
||||
)
|
||||
for log in logs
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user