mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
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:
committed by
GitHub
parent
c4652190eb
commit
55a34a9f1f
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)');
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user