Feat: Add Terminal Input to UI for Human Write attempts (Reverse Captcha Demo)
This commit is contained in:
@@ -28,6 +28,11 @@
|
|||||||
<main id="log-feed">
|
<main id="log-feed">
|
||||||
<div class="loading">Fetching data stream...</div>
|
<div class="loading">Fetching data stream...</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<footer class="terminal-input">
|
||||||
|
<span class="prompt">></span>
|
||||||
|
<input type="text" id="command-input" placeholder="Enter log message..." autocomplete="off">
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="script.js"></script>
|
<script src="script.js"></script>
|
||||||
|
|||||||
116
public/script.js
116
public/script.js
@@ -1,6 +1,14 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
fetchLogs();
|
fetchLogs();
|
||||||
setInterval(fetchLogs, 5000); // Poll every 5 seconds
|
setInterval(fetchLogs, 5000); // Poll every 5 seconds
|
||||||
|
|
||||||
|
const input = document.getElementById('command-input');
|
||||||
|
input.addEventListener('keydown', async (e) => {
|
||||||
|
if (e.key === 'Enter' && input.value.trim()) {
|
||||||
|
await handleCommand(input.value.trim());
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function fetchLogs() {
|
async function fetchLogs() {
|
||||||
@@ -15,15 +23,22 @@ async function fetchLogs() {
|
|||||||
|
|
||||||
function renderLogs(logs) {
|
function renderLogs(logs) {
|
||||||
const container = document.getElementById('log-feed');
|
const container = document.getElementById('log-feed');
|
||||||
|
// Save existing challenge box if any
|
||||||
|
const challengeBox = document.querySelector('.challenge-box');
|
||||||
|
|
||||||
|
let html = '';
|
||||||
if (logs.length === 0) {
|
if (logs.length === 0) {
|
||||||
container.innerHTML = '<div class="loading">No logs found. Waiting for agent activity...</div>';
|
html = '<div class="loading">No logs found. Waiting for agent activity...</div>';
|
||||||
return;
|
} else {
|
||||||
|
html = logs.map(createLogHTML).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Completely re-render for simplicity (prototype mode)
|
container.innerHTML = html;
|
||||||
// In production, we would append/prepend efficiently.
|
|
||||||
container.innerHTML = logs.map(createLogHTML).join('');
|
// Restore challenge box if it existed
|
||||||
|
if (challengeBox) {
|
||||||
|
container.appendChild(challengeBox);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLogHTML(log) {
|
function createLogHTML(log) {
|
||||||
@@ -39,6 +54,97 @@ function createLogHTML(log) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleCommand(text) {
|
||||||
|
const container = document.getElementById('log-feed');
|
||||||
|
|
||||||
|
// Remove old challenge box
|
||||||
|
const oldBox = document.querySelector('.challenge-box');
|
||||||
|
if (oldBox) oldBox.remove();
|
||||||
|
|
||||||
|
// 1. Attempt to post (will likely fail)
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/posts', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
content: text,
|
||||||
|
agentId: 'Web-Console-User'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
// Success! (Unlikely for a human)
|
||||||
|
fetchLogs();
|
||||||
|
} else if (res.status === 401 && data.captchalm) {
|
||||||
|
// 2. Challenge Received
|
||||||
|
showChallenge(data.captchalm, text);
|
||||||
|
} else {
|
||||||
|
alert(`Error: ${data.error}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showChallenge(captchaData, originalContent) {
|
||||||
|
const container = document.getElementById('log-feed');
|
||||||
|
const challenge = captchaData.challenge;
|
||||||
|
const instructions = captchaData.instructions;
|
||||||
|
|
||||||
|
const box = document.createElement('div');
|
||||||
|
box.className = 'challenge-box';
|
||||||
|
box.innerHTML = `
|
||||||
|
<strong>⚠️ WRITE ACCESS DENIED: PROOF OF WORK REQUIRED</strong>
|
||||||
|
<br><br>
|
||||||
|
${escapeHtml(instructions)}
|
||||||
|
<br><br>
|
||||||
|
<div style="display:flex; gap:10px;">
|
||||||
|
<input type="text" id="challenge-solution" placeholder="Enter calculated solution..." style="background:#000; border:1px solid #555; color:#fff; padding:5px; flex:1;">
|
||||||
|
<button id="submit-challenge" style="cursor:pointer; padding:5px 10px;">VERIFY</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.appendChild(box);
|
||||||
|
box.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
|
||||||
|
const btn = box.querySelector('#submit-challenge');
|
||||||
|
const input = box.querySelector('#challenge-solution');
|
||||||
|
|
||||||
|
btn.onclick = async () => {
|
||||||
|
const solution = input.value.trim();
|
||||||
|
if (!solution) return;
|
||||||
|
|
||||||
|
// 3. Retry with solution
|
||||||
|
const res = await fetch('/api/posts', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-captchalm-id': challenge.id,
|
||||||
|
'x-captchalm-solution': solution
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
content: originalContent,
|
||||||
|
agentId: 'Web-Console-User',
|
||||||
|
_CaptchaLMChallenge: challenge // Middleware might want this too
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await res.json();
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
box.remove();
|
||||||
|
fetchLogs();
|
||||||
|
alert('ACCESS GRANTED. Log entry committed.');
|
||||||
|
} else {
|
||||||
|
alert(`ACCESS DENIED: ${result.error}`);
|
||||||
|
input.value = '';
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
if (!text) return '';
|
if (!text) return '';
|
||||||
return text
|
return text
|
||||||
|
|||||||
@@ -110,7 +110,85 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
|
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Terminal Input */
|
||||||
|
|
||||||
|
.terminal-input {
|
||||||
|
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
|
||||||
|
padding-top: 20px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.prompt {
|
||||||
|
|
||||||
|
color: var(--accent-green);
|
||||||
|
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#command-input {
|
||||||
|
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
color: var(--text-primary);
|
||||||
|
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.challenge-box {
|
||||||
|
|
||||||
|
background: #1f1f1f;
|
||||||
|
|
||||||
|
border: 1px solid var(--accent-yellow);
|
||||||
|
|
||||||
|
padding: 15px;
|
||||||
|
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
white-space: pre-wrap;
|
||||||
|
|
||||||
|
color: var(--accent-yellow);
|
||||||
|
|
||||||
|
display: none;
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user