feat: add auto-login for dev mode and fix log box formatting (#567)

* feat: add auto-login for dev mode and fix log box formatting

Add AUTOMAKER_AUTO_LOGIN environment variable that, when set to 'true',
automatically creates a session for web mode users without requiring
them to enter the API key. Useful for development environments.

Also fix formatting issues in console log boxes:
- API Key box: add right border, show auto-login status and tips
- Claude auth warning: add separator line, fix emoji spacing
- Server info box: use consistent 71-char width, proper padding
- Port conflict error: use same width, proper dynamic padding

Environment variables:
- AUTOMAKER_AUTO_LOGIN=true: Skip login prompt, auto-create session
- AUTOMAKER_API_KEY: Use a fixed API key (existing)
- AUTOMAKER_HIDE_API_KEY=true: Hide the API key banner (existing)

* fix: add production safeguard to auto-login and extract log box constant

- Add NODE_ENV !== 'production' check to prevent auto-login in production
- Extract magic number 67 to BOX_CONTENT_WIDTH constant in auth.ts and index.ts
- Document AUTOMAKER_AUTO_LOGIN env var in CLAUDE.md and README.md
This commit is contained in:
Stefan de Vogelaere
2026-01-18 23:48:00 +01:00
committed by GitHub
parent c4652190eb
commit 55a34a9f1f
5 changed files with 145 additions and 52 deletions

View File

@@ -172,4 +172,5 @@ Use `resolveModelString()` from `@automaker/model-resolver` to convert model ali
- `DATA_DIR` - Data storage directory (default: ./data) - `DATA_DIR` - Data storage directory (default: ./data)
- `ALLOWED_ROOT_DIRECTORY` - Restrict file operations to specific directory - `ALLOWED_ROOT_DIRECTORY` - Restrict file operations to specific directory
- `AUTOMAKER_MOCK_AGENT=true` - Enable mock agent mode for CI testing - `AUTOMAKER_MOCK_AGENT=true` - Enable mock agent mode for CI testing
- `AUTOMAKER_AUTO_LOGIN=true` - Skip login prompt in development (disabled when NODE_ENV=production)
- `VITE_HOSTNAME` - Hostname for frontend API URLs (default: localhost) - `VITE_HOSTNAME` - Hostname for frontend API URLs (default: localhost)

View File

@@ -389,6 +389,7 @@ npm run lint
- `VITE_SKIP_ELECTRON` - Skip Electron in dev mode - `VITE_SKIP_ELECTRON` - Skip Electron in dev mode
- `OPEN_DEVTOOLS` - Auto-open DevTools in Electron - `OPEN_DEVTOOLS` - Auto-open DevTools in Electron
- `AUTOMAKER_SKIP_SANDBOX_WARNING` - Skip sandbox warning dialog (useful for dev/CI) - `AUTOMAKER_SKIP_SANDBOX_WARNING` - Skip sandbox warning dialog (useful for dev/CI)
- `AUTOMAKER_AUTO_LOGIN=true` - Skip login prompt in development (ignored when NODE_ENV=production)
### Authentication Setup ### Authentication Setup

View File

@@ -113,24 +113,37 @@ export function isRequestLoggingEnabled(): boolean {
return requestLoggingEnabled; return requestLoggingEnabled;
} }
// Width for log box content (excluding borders)
const BOX_CONTENT_WIDTH = 67;
// Check for required environment variables // Check for required environment variables
const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY; const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
if (!hasAnthropicKey) { if (!hasAnthropicKey) {
const wHeader = '⚠️ WARNING: No Claude authentication configured'.padEnd(BOX_CONTENT_WIDTH);
const w1 = 'The Claude Agent SDK requires authentication to function.'.padEnd(BOX_CONTENT_WIDTH);
const w2 = 'Set your Anthropic API key:'.padEnd(BOX_CONTENT_WIDTH);
const w3 = ' export ANTHROPIC_API_KEY="sk-ant-..."'.padEnd(BOX_CONTENT_WIDTH);
const w4 = 'Or use the setup wizard in Settings to configure authentication.'.padEnd(
BOX_CONTENT_WIDTH
);
logger.warn(` logger.warn(`
╔═══════════════════════════════════════════════════════════════════════ ╔═════════════════════════════════════════════════════════════════════╗
⚠️ WARNING: No Claude authentication configured ${wHeader}
║ ║ ╠═════════════════════════════════════════════════════════════════════╣
The Claude Agent SDK requires authentication to function.
${w1}
Set your Anthropic API key:
export ANTHROPIC_API_KEY="sk-ant-..." ${w2}
${w3}
Or use the setup wizard in Settings to configure authentication.
╚═══════════════════════════════════════════════════════════════════════╝ ${w4}
║ ║
╚═════════════════════════════════════════════════════════════════════╝
`); `);
} else { } else {
logger.info('✓ ANTHROPIC_API_KEY detected (API key auth)'); logger.info('✓ ANTHROPIC_API_KEY detected');
} }
// Initialize security // Initialize security
@@ -649,40 +662,74 @@ const startServer = (port: number, host: string) => {
? 'enabled (password protected)' ? 'enabled (password protected)'
: 'enabled' : 'enabled'
: 'disabled'; : 'disabled';
const portStr = port.toString().padEnd(4);
// Build URLs for display
const listenAddr = `${host}:${port}`;
const httpUrl = `http://${HOSTNAME}:${port}`;
const wsEventsUrl = `ws://${HOSTNAME}:${port}/api/events`;
const wsTerminalUrl = `ws://${HOSTNAME}:${port}/api/terminal/ws`;
const healthUrl = `http://${HOSTNAME}:${port}/api/health`;
const sHeader = '🚀 Automaker Backend Server'.padEnd(BOX_CONTENT_WIDTH);
const s1 = `Listening: ${listenAddr}`.padEnd(BOX_CONTENT_WIDTH);
const s2 = `HTTP API: ${httpUrl}`.padEnd(BOX_CONTENT_WIDTH);
const s3 = `WebSocket: ${wsEventsUrl}`.padEnd(BOX_CONTENT_WIDTH);
const s4 = `Terminal WS: ${wsTerminalUrl}`.padEnd(BOX_CONTENT_WIDTH);
const s5 = `Health: ${healthUrl}`.padEnd(BOX_CONTENT_WIDTH);
const s6 = `Terminal: ${terminalStatus}`.padEnd(BOX_CONTENT_WIDTH);
logger.info(` logger.info(`
╔═══════════════════════════════════════════════════════╗ ╔═════════════════════════════════════════════════════════════════════
Automaker Backend Server ${sHeader}
╠═══════════════════════════════════════════════════════╣ ╠═════════════════════════════════════════════════════════════════════
Listening: ${host}:${port}${' '.repeat(Math.max(0, 34 - host.length - port.toString().length))}
HTTP API: http://${HOSTNAME}:${portStr} ${s1}
WebSocket: ws://${HOSTNAME}:${portStr}/api/events ${s2}
Terminal: ws://${HOSTNAME}:${portStr}/api/terminal/ws ${s3}
Health: http://${HOSTNAME}:${portStr}/api/health ${s4}
Terminal: ${terminalStatus.padEnd(37)} ${s5}
╚═══════════════════════════════════════════════════════╝ ${s6}
║ ║
╚═════════════════════════════════════════════════════════════════════╝
`); `);
}); });
server.on('error', (error: NodeJS.ErrnoException) => { server.on('error', (error: NodeJS.ErrnoException) => {
if (error.code === 'EADDRINUSE') { if (error.code === 'EADDRINUSE') {
const portStr = port.toString();
const nextPortStr = (port + 1).toString();
const killCmd = `lsof -ti:${portStr} | xargs kill -9`;
const altCmd = `PORT=${nextPortStr} npm run dev:server`;
const eHeader = `❌ ERROR: Port ${portStr} is already in use`.padEnd(BOX_CONTENT_WIDTH);
const e1 = 'Another process is using this port.'.padEnd(BOX_CONTENT_WIDTH);
const e2 = 'To fix this, try one of:'.padEnd(BOX_CONTENT_WIDTH);
const e3 = '1. Kill the process using the port:'.padEnd(BOX_CONTENT_WIDTH);
const e4 = ` ${killCmd}`.padEnd(BOX_CONTENT_WIDTH);
const e5 = '2. Use a different port:'.padEnd(BOX_CONTENT_WIDTH);
const e6 = ` ${altCmd}`.padEnd(BOX_CONTENT_WIDTH);
const e7 = '3. Use the init.sh script which handles this:'.padEnd(BOX_CONTENT_WIDTH);
const e8 = ' ./init.sh'.padEnd(BOX_CONTENT_WIDTH);
logger.error(` logger.error(`
╔═══════════════════════════════════════════════════════╗ ╔═════════════════════════════════════════════════════════════════════
❌ ERROR: Port ${port} is already in use ${eHeader}
╠═══════════════════════════════════════════════════════╣ ╠═════════════════════════════════════════════════════════════════════
Another process is using this port.
${e1}
To fix this, try one of:
${e2}
1. Kill the process using the port:
lsof -ti:${port} | xargs kill -9 ${e3}
${e4}
2. Use a different port:
PORT=${port + 1} npm run dev:server ${e5}
${e6}
3. Use the init.sh script which handles this:
./init.sh ${e7}
╚═══════════════════════════════════════════════════════╝ ${e8}
║ ║
╚═════════════════════════════════════════════════════════════════════╝
`); `);
process.exit(1); process.exit(1);
} else { } else {

View File

@@ -130,21 +130,47 @@ function ensureApiKey(): string {
// API key - always generated/loaded on startup for CSRF protection // API key - always generated/loaded on startup for CSRF protection
const API_KEY = ensureApiKey(); const API_KEY = ensureApiKey();
// Width for log box content (excluding borders)
const BOX_CONTENT_WIDTH = 67;
// Print API key to console for web mode users (unless suppressed for production logging) // Print API key to console for web mode users (unless suppressed for production logging)
if (process.env.AUTOMAKER_HIDE_API_KEY !== 'true') { if (process.env.AUTOMAKER_HIDE_API_KEY !== 'true') {
const autoLoginEnabled = process.env.AUTOMAKER_AUTO_LOGIN === 'true';
const autoLoginStatus = autoLoginEnabled ? 'enabled (auto-login active)' : 'disabled';
// Build box lines with exact padding
const header = '🔐 API Key for Web Mode Authentication'.padEnd(BOX_CONTENT_WIDTH);
const line1 = "When accessing via browser, you'll be prompted to enter this key:".padEnd(
BOX_CONTENT_WIDTH
);
const line2 = API_KEY.padEnd(BOX_CONTENT_WIDTH);
const line3 = 'In Electron mode, authentication is handled automatically.'.padEnd(
BOX_CONTENT_WIDTH
);
const line4 = `Auto-login (AUTOMAKER_AUTO_LOGIN): ${autoLoginStatus}`.padEnd(BOX_CONTENT_WIDTH);
const tipHeader = '💡 Tips'.padEnd(BOX_CONTENT_WIDTH);
const line5 = 'Set AUTOMAKER_API_KEY env var to use a fixed key'.padEnd(BOX_CONTENT_WIDTH);
const line6 = 'Set AUTOMAKER_AUTO_LOGIN=true to skip the login prompt'.padEnd(BOX_CONTENT_WIDTH);
logger.info(` logger.info(`
╔═══════════════════════════════════════════════════════════════════════ ╔═════════════════════════════════════════════════════════════════════╗
🔐 API Key for Web Mode Authentication ${header}
╠═══════════════════════════════════════════════════════════════════════ ╠═════════════════════════════════════════════════════════════════════╣
║ ║
When accessing via browser, you'll be prompted to enter this key: ${line1}
║ ║
${API_KEY} ${line2}
║ ║
In Electron mode, authentication is handled automatically. ${line3}
║ ║
💡 Tip: Set AUTOMAKER_API_KEY env var to use a fixed key for dev ${line4}
╚═══════════════════════════════════════════════════════════════════════╝ ║ ║
╠═════════════════════════════════════════════════════════════════════╣
${tipHeader}
╠═════════════════════════════════════════════════════════════════════╣
${line5}
${line6}
╚═════════════════════════════════════════════════════════════════════╝
`); `);
} else { } else {
logger.info('API key banner hidden (AUTOMAKER_HIDE_API_KEY=true)'); logger.info('API key banner hidden (AUTOMAKER_HIDE_API_KEY=true)');

View File

@@ -117,9 +117,27 @@ export function createAuthRoutes(): Router {
* *
* Returns whether the current request is authenticated. * Returns whether the current request is authenticated.
* Used by the UI to determine if login is needed. * Used by the UI to determine if login is needed.
*
* If AUTOMAKER_AUTO_LOGIN=true is set, automatically creates a session
* for unauthenticated requests (useful for development).
*/ */
router.get('/status', (req, res) => { router.get('/status', async (req, res) => {
const authenticated = isRequestAuthenticated(req); let authenticated = isRequestAuthenticated(req);
// Auto-login for development: create session automatically if enabled
// Only works in non-production environments as a safeguard
if (
!authenticated &&
process.env.AUTOMAKER_AUTO_LOGIN === 'true' &&
process.env.NODE_ENV !== 'production'
) {
const sessionToken = await createSession();
const cookieOptions = getSessionCookieOptions();
const cookieName = getSessionCookieName();
res.cookie(cookieName, sessionToken, cookieOptions);
authenticated = true;
}
res.json({ res.json({
success: true, success: true,
authenticated, authenticated,