diff --git a/admin/src/pages/Login.tsx b/admin/src/pages/Login.tsx index bba75467d..9a76c4baa 100644 --- a/admin/src/pages/Login.tsx +++ b/admin/src/pages/Login.tsx @@ -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 (
-
+
GBrain
-
- setToken(e.target.value)} - autoFocus - /> + +
+
+ 🔒 This is a protected dashboard +
+ Ask your AI agent for the admin login link: +
+ "Give me the GBrain admin login link" +
+
+ Your agent will generate a secure one-click URL that logs you in automatically. +
- - {error &&
{error}
} -
Find this token in your terminal output.
- + +
+ + Or paste token manually + +
+
+ setToken(e.target.value)} + /> +
+ + {error &&
{error}
} +
+
+
); } diff --git a/src/commands/serve-http.ts b/src/commands/serve-http.ts index 0c5e145a5..ec9777781 100644 --- a/src/commands/serve-http.ts +++ b/src/commands/serve-http.ts @@ -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/ + // 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)?.gbrain_admin;