diff --git a/src/main/frontend/src/api/index.js b/src/main/frontend/src/api/index.js index 00e2dda..66db25a 100644 --- a/src/main/frontend/src/api/index.js +++ b/src/main/frontend/src/api/index.js @@ -1,13 +1,18 @@ import axios from 'axios' -let authToken = null +let authToken = localStorage.getItem('jyso_token') export function setAuthToken(token) { authToken = token + if (token) { + localStorage.setItem('jyso_token', token) + } else { + localStorage.removeItem('jyso_token') + } } export function getAuthToken() { - return authToken + return authToken || localStorage.getItem('jyso_token') } const api = axios.create({ @@ -27,6 +32,7 @@ api.interceptors.response.use( error => { if (error.response && error.response.status === 401) { authToken = null + localStorage.removeItem('jyso_token') } return Promise.reject(error) } @@ -72,4 +78,16 @@ export function getLogs(count) { return api.get('/logs', { params: { count } }) } +export function getFiles() { + return api.get('/files') +} + +export function downloadFile(name) { + return api.get('/files/download', { params: { name }, responseType: 'blob' }) +} + +export function deleteFile(name) { + return api.post('/files/delete', { name }) +} + export default api diff --git a/src/main/frontend/src/index.css b/src/main/frontend/src/index.css index d66e5b6..b55bf85 100644 --- a/src/main/frontend/src/index.css +++ b/src/main/frontend/src/index.css @@ -825,6 +825,87 @@ select { .login-inner { padding: 36px 28px 32px; } } +.wiki-btn { + display: inline-flex; + align-items: center; + padding: 6px 14px; + border-radius: var(--btn-radius); + background: var(--bg-input); + border: 1px solid var(--border-color); + color: var(--text-secondary); + font-size: 12px; + font-weight: 600; + text-decoration: none; + cursor: pointer; + transition: all var(--transition-fast); + text-transform: uppercase; + letter-spacing: 0.3px; +} +.wiki-btn:hover { + background: var(--accent-glow); + border-color: var(--accent); + color: var(--accent); +} + +.file-list { + max-height: 360px; + overflow-y: auto; +} +.file-row { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + border-bottom: 1px solid var(--border-color); + font-size: 12px; + transition: background var(--transition-fast); +} +.file-row:last-child { border-bottom: none; } +.file-row:hover { background: var(--bg-primary); } +.file-name { + flex: 1; + font-family: var(--font-mono); + color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.file-size { + font-size: 10px; + color: var(--text-muted); + flex-shrink: 0; + min-width: 45px; + text-align: right; +} +.file-btn { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border: none; + border-radius: 6px; + background: transparent; + color: var(--text-muted); + cursor: pointer; + transition: all var(--transition-fast); +} +.file-btn:hover { + background: var(--accent-glow); + color: var(--accent); +} +.file-btn-del:hover { + background: var(--danger-bg); + color: var(--danger); +} +.file-empty { + text-align: center; + padding: 28px 0; + color: var(--text-muted); + font-size: 13px; + font-family: 'Inter', sans-serif; +} + @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: 0ms !important; diff --git a/src/main/frontend/src/pages/Dashboard.jsx b/src/main/frontend/src/pages/Dashboard.jsx index 2b85852..1158c7d 100644 --- a/src/main/frontend/src/pages/Dashboard.jsx +++ b/src/main/frontend/src/pages/Dashboard.jsx @@ -3,7 +3,8 @@ import { useNavigate } from 'react-router-dom' import { useTheme } from '../context/ThemeContext' import { getStatus, toggleServer, getGadgets, - generatePayload as apiGenerate, updateConfig, setAuthToken, getLogs + generatePayload as apiGenerate, updateConfig, setAuthToken, getLogs, + getFiles, downloadFile, deleteFile as apiDeleteFile } from '../api' export default function Dashboard() { @@ -50,6 +51,8 @@ export default function Dashboard() { const [logLines, setLogLines] = useState([]) const [logLoading, setLogLoading] = useState(false) const logEndRef = useRef(null) + const [files, setFiles] = useState([]) + const [filesLoading, setFilesLoading] = useState(false) const ROUTING_OPTIONS = ['Basic', 'ELProcessor', 'Groovy', 'jdbcBypass1', 'jdbcBypass2', 'ldap2rmi', 'SnakeYaml', 'XStream', 'MemoryXXE'] @@ -140,6 +143,7 @@ export default function Dashboard() { if (res.data.success) { setPayloadResult(res.data.message || 'Payload generated successfully') setMsg({ success: 'Payload generated', error: '' }) + if (res.data.saved) fetchFiles() } else { setPayloadResult('Error: ' + (res.data.error || 'unknown')) setMsg({ success: '', error: res.data.error || 'Generation failed' }) @@ -243,6 +247,38 @@ export default function Dashboard() { useEffect(() => { loadStatus(); loadGadgets() }, []) + useEffect(() => { + if (mode !== 'gadget') return + fetchFiles() + }, [mode]) + + async function fetchFiles() { + try { + const res = await getFiles() + setFiles(res.data) + } catch { /* ignore */ } + } + + async function handleDownloadFile(name) { + try { + const res = await downloadFile(name) + const url = window.URL.createObjectURL(new Blob([res.data])) + const a = document.createElement('a') + a.href = url + a.download = name + a.click() + window.URL.revokeObjectURL(url) + } catch { /* ignore */ } + } + + async function handleDeleteFile(name) { + if (!confirm(`Delete ${name}?`)) return + try { + await apiDeleteFile(name) + fetchFiles() + } catch { /* ignore */ } + } + useEffect(() => { if (activeJndiTab !== 'logs') return fetchLogs() @@ -280,6 +316,9 @@ export default function Dashboard() {