feat(admin): magic link login — ask your agent for the URL

New flow:
1. User opens /admin → sees 'This is a protected dashboard'
2. UI tells them: 'Ask your AI agent for the admin login link'
3. Agent generates: https://host:port/admin/auth/<token>
4. User clicks the link → auto-authenticates → redirects to dashboard
5. Session lasts 7 days (magic link) vs 24h (manual token paste)

Server: GET /admin/auth/:token validates the bootstrap token, sets
HttpOnly cookie, redirects to /admin/. Invalid tokens get a plain
text error telling them to ask their agent for a fresh link.

Login page: primary UX is the 'ask your agent' prompt with example.
Manual token paste collapsed under a <details> disclosure.
This commit is contained in:
Wintermute
2026-05-03 14:44:23 +00:00
parent 070efb7655
commit 922abb1833
2 changed files with 78 additions and 17 deletions
+53 -17
View File
@@ -14,7 +14,6 @@ export function LoginPage({ onLogin }: { onLogin: () => void }) {
api.login(saved).then(() => {
onLogin();
}).catch(() => {
// Saved token expired or server restarted with new token
localStorage.removeItem('gbrain_admin_token');
setAutoLogging(false);
});
@@ -32,7 +31,7 @@ export function LoginPage({ onLogin }: { onLogin: () => void }) {
localStorage.setItem('gbrain_admin_token', token);
onLogin();
} catch (err) {
setError('Invalid token. Check your terminal output.');
setError('Invalid token.');
} finally {
setLoading(false);
}
@@ -51,23 +50,60 @@ export function LoginPage({ onLogin }: { onLogin: () => void }) {
return (
<div className="login-page">
<form className="login-box" onSubmit={handleSubmit}>
<div className="login-box">
<div className="login-logo">GBrain</div>
<div style={{ marginBottom: 16 }}>
<input
type="password"
placeholder="Admin Token"
value={token}
onChange={e => setToken(e.target.value)}
autoFocus
/>
<div style={{
background: 'rgba(136, 170, 255, 0.08)',
border: '1px solid rgba(136, 170, 255, 0.2)',
borderRadius: 8,
padding: '14px 16px',
marginBottom: 20,
fontSize: 13,
lineHeight: 1.5,
color: 'var(--text-secondary)',
}}>
<div style={{ fontWeight: 600, color: 'var(--text-primary)', marginBottom: 6 }}>
🔒 This is a protected dashboard
</div>
Ask your AI agent for the admin login link:
<div style={{
background: 'rgba(0,0,0,0.3)',
borderRadius: 6,
padding: '8px 12px',
marginTop: 8,
fontFamily: 'var(--font-mono)',
fontSize: 12,
color: '#88aaff',
wordBreak: 'break-all',
}}>
"Give me the GBrain admin login link"
</div>
<div style={{ marginTop: 8, fontSize: 12, color: 'var(--text-muted)' }}>
Your agent will generate a secure one-click URL that logs you in automatically.
</div>
</div>
<button className="btn btn-primary" style={{ width: '100%' }} disabled={loading}>
{loading ? 'Authenticating...' : 'Submit'}
</button>
{error && <div className="login-error">{error}</div>}
<div className="login-hint">Find this token in your terminal output.</div>
</form>
<details style={{ marginBottom: 16 }}>
<summary style={{ cursor: 'pointer', fontSize: 13, color: 'var(--text-muted)' }}>
Or paste token manually
</summary>
<form onSubmit={handleSubmit} style={{ marginTop: 12 }}>
<div style={{ marginBottom: 12 }}>
<input
type="password"
placeholder="Admin Token"
value={token}
onChange={e => setToken(e.target.value)}
/>
</div>
<button className="btn btn-primary" style={{ width: '100%' }} disabled={loading}>
{loading ? 'Authenticating...' : 'Submit'}
</button>
{error && <div className="login-error">{error}</div>}
</form>
</details>
</div>
</div>
);
}
+25
View File
@@ -191,6 +191,7 @@ export async function runServeHttp(engine: BrainEngine, options: ServeHttpOption
// ---------------------------------------------------------------------------
// Admin authentication (cookie-based)
// ---------------------------------------------------------------------------
// POST /admin/login — JSON body with token (for programmatic/UI login)
app.post('/admin/login', express.json(), (req, res) => {
const token = req.body?.token;
if (!token) {
@@ -217,6 +218,30 @@ export async function runServeHttp(engine: BrainEngine, options: ServeHttpOption
res.json({ status: 'authenticated' });
});
// GET /admin/auth/:token — magic link login (one-click from agent)
// The agent generates: https://host:port/admin/auth/<bootstrapToken>
// Browser hits it, sets cookie, redirects to dashboard.
app.get('/admin/auth/:token', (req, res) => {
const token = req.params.token;
const tokenHash = createHash('sha256').update(token).digest('hex');
if (tokenHash !== bootstrapHash) {
res.status(401).send('Invalid admin link. Ask your agent for a fresh one.');
return;
}
const sessionId = randomBytes(32).toString('hex');
const expiresAt = Date.now() + 7 * 24 * 60 * 60 * 1000; // 7 days for magic link
adminSessions.set(sessionId, expiresAt);
res.cookie('gbrain_admin', sessionId, {
httpOnly: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000,
path: '/admin',
});
res.redirect('/admin/');
});
// Admin auth middleware
function requireAdmin(req: express.Request, res: express.Response, next: express.NextFunction) {
const sessionId = (req.cookies as Record<string, string>)?.gbrain_admin;