From 39c1ee2ca0f01a1254812a4a8fe8f6c2ed052fe0 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Tue, 13 Jan 2026 15:13:10 -0500 Subject: [PATCH] feat(config): add ccs config auth CLI subcommand Interactive CLI for managing dashboard authentication: - ccs config auth setup: Interactive wizard with bcrypt password hashing - ccs config auth show: Display current auth status and ENV overrides - ccs config auth disable: Disable auth with confirmation Follows modular facade pattern from auth-commands.ts. Uses InteractivePrompt for masked password input. Includes local documentation at docs/dashboard-auth-cli.md. Related: #319 --- docs/dashboard-auth-cli.md | 166 ++++++++++++++++++++ src/commands/config-auth/disable-command.ts | 75 +++++++++ src/commands/config-auth/index.ts | 94 +++++++++++ src/commands/config-auth/setup-command.ts | 133 ++++++++++++++++ src/commands/config-auth/show-command.ts | 89 +++++++++++ src/commands/config-auth/types.ts | 27 ++++ src/commands/config-command.ts | 16 +- src/commands/help-command.ts | 2 + 8 files changed, 601 insertions(+), 1 deletion(-) create mode 100644 docs/dashboard-auth-cli.md create mode 100644 src/commands/config-auth/disable-command.ts create mode 100644 src/commands/config-auth/index.ts create mode 100644 src/commands/config-auth/setup-command.ts create mode 100644 src/commands/config-auth/show-command.ts create mode 100644 src/commands/config-auth/types.ts diff --git a/docs/dashboard-auth-cli.md b/docs/dashboard-auth-cli.md new file mode 100644 index 00000000..0ea982d9 --- /dev/null +++ b/docs/dashboard-auth-cli.md @@ -0,0 +1,166 @@ +# Dashboard Authentication CLI + +CLI commands for managing CCS dashboard authentication. + +## Overview + +The CCS dashboard (`ccs config`) can be protected with username/password authentication. This is useful when running the dashboard on a network-accessible machine. + +Authentication is **disabled by default** for backward compatibility. Use the CLI to configure and enable it. + +## Commands + +### `ccs config auth setup` + +Interactive wizard to configure dashboard login. + +```bash +$ ccs config auth setup + +╭─────────────────────────────────╮ +│ Dashboard Auth Setup │ +╰─────────────────────────────────╯ + +[i] Configure username and password for dashboard access. + Password will be hashed with bcrypt before storage. + +Username +Enter username: admin + +Password + Minimum 8 characters +Enter password: ******** +Confirm password: ******** + +[i] Hashing password... + +[OK] Dashboard authentication configured + +[i] Settings saved to ~/.ccs/config.yaml +[i] Username: admin +[i] Session timeout: 24 hours + + Start dashboard: ccs config + Show status: ccs config auth show + Disable auth: ccs config auth disable +``` + +### `ccs config auth show` + +Display current authentication status. + +```bash +$ ccs config auth show + +╭─────────────────────────────────╮ +│ Dashboard Auth Status │ +╰─────────────────────────────────╯ + +Configuration +[OK] Authentication: Enabled +[OK] Username: admin +[i] Session timeout: 24 hours + +Commands + ccs config auth setup Configure authentication + ccs config auth disable Disable authentication + ccs config Open dashboard +``` + +### `ccs config auth disable` + +Disable dashboard authentication with confirmation. + +```bash +$ ccs config auth disable + +╭─────────────────────────────────╮ +│ Disable Dashboard Auth │ +╰─────────────────────────────────╯ + +[!] This will disable login protection for the dashboard. +[i] Anyone with network access will be able to view the dashboard. + +Disable authentication? [y/N]: y + +[OK] Dashboard authentication disabled + +[i] Credentials preserved - re-enable with: ccs config auth setup +``` + +### `ccs config auth --help` + +Display usage information. + +## Environment Variables + +Environment variables override `config.yaml` values: + +| Variable | Description | +|----------|-------------| +| `CCS_DASHBOARD_AUTH_ENABLED` | Enable/disable auth (`true`/`false`) | +| `CCS_DASHBOARD_USERNAME` | Username | +| `CCS_DASHBOARD_PASSWORD_HASH` | Bcrypt password hash | + +### Generating a Password Hash + +Use bcrypt to generate a hash: + +```bash +# Using Node.js +node -e "console.log(require('bcrypt').hashSync('your-password', 10))" + +# Using npx +npx bcrypt-cli hash "your-password" +``` + +## Configuration + +Settings are stored in `~/.ccs/config.yaml`: + +```yaml +# Dashboard Auth: Optional login protection for CCS dashboard +# Generate password hash: npx bcrypt-cli hash "your-password" +# ENV override: CCS_DASHBOARD_AUTH_ENABLED, CCS_DASHBOARD_USERNAME, CCS_DASHBOARD_PASSWORD_HASH +dashboard_auth: + enabled: true + username: "admin" + password_hash: "$2b$10$..." + session_timeout_hours: 24 +``` + +## Security Notes + +1. **Bcrypt hashing**: Passwords are hashed with bcrypt (10 rounds) before storage +2. **Session cookies**: Sessions use HTTP-only cookies (not accessible via JavaScript) +3. **Rate limiting**: Login attempts are rate-limited (5 per 15 minutes) +4. **File permissions**: Config file is created with 0o600 permissions + +## Troubleshooting + +### "Authentication not configured" + +Run `ccs config auth setup` to configure credentials. + +### Forgot password + +Run `ccs config auth setup` again to set a new password. + +### ENV override not working + +Ensure the variable is exported: + +```bash +export CCS_DASHBOARD_AUTH_ENABLED=true +export CCS_DASHBOARD_USERNAME=admin +export CCS_DASHBOARD_PASSWORD_HASH='$2b$10$...' +``` + +### Session expired immediately + +Check `session_timeout_hours` in config. Default is 24 hours. + +## See Also + +- [Dashboard Auth Feature](https://ccs.kaitran.ca/features/dashboard-auth) - Full documentation +- [Config Schema](https://ccs.kaitran.ca/reference/config-schema) - All config options diff --git a/src/commands/config-auth/disable-command.ts b/src/commands/config-auth/disable-command.ts new file mode 100644 index 00000000..fd66cb46 --- /dev/null +++ b/src/commands/config-auth/disable-command.ts @@ -0,0 +1,75 @@ +/** + * Config Auth Disable Command + * + * Disable dashboard authentication with confirmation. + */ + +import { InteractivePrompt } from '../../utils/prompt'; +import { + getDashboardAuthConfig, + loadOrCreateUnifiedConfig, + saveUnifiedConfig, +} from '../../config/unified-config-loader'; +import { initUI, header, ok, info, warn, dim } from '../../utils/ui'; + +/** + * Handle disable command - disable auth with confirmation + */ +export async function handleDisable(): Promise { + await initUI(); + + console.log(''); + console.log(header('Disable Dashboard Auth')); + console.log(''); + + const config = getDashboardAuthConfig(); + + // Check if already disabled + if (!config.enabled) { + console.log(info('Dashboard authentication is already disabled.')); + console.log(''); + console.log(dim(' To enable: ccs config auth setup')); + console.log(''); + return; + } + + // Check for ENV override + if (process.env.CCS_DASHBOARD_AUTH_ENABLED) { + console.log(warn('CCS_DASHBOARD_AUTH_ENABLED environment variable is set.')); + console.log(info('Disabling in config.yaml, but ENV var will still override.')); + console.log(''); + } + + // Confirm before disabling + console.log(warn('This will disable login protection for the dashboard.')); + console.log(info('Anyone with network access will be able to view the dashboard.')); + console.log(''); + + const confirmed = await InteractivePrompt.confirm('Disable authentication?', { + default: false, // Safe default + }); + + if (!confirmed) { + console.log(''); + console.log(info('Cancelled. Authentication remains enabled.')); + console.log(''); + return; + } + + // Disable auth + const fullConfig = loadOrCreateUnifiedConfig(); + fullConfig.dashboard_auth = { + enabled: false, + username: fullConfig.dashboard_auth?.username ?? '', + password_hash: fullConfig.dashboard_auth?.password_hash ?? '', + session_timeout_hours: fullConfig.dashboard_auth?.session_timeout_hours ?? 24, + }; + + saveUnifiedConfig(fullConfig); + + console.log(''); + console.log(ok('Dashboard authentication disabled')); + console.log(''); + console.log(info('Credentials preserved - re-enable with: ccs config auth setup')); + console.log(''); +} diff --git a/src/commands/config-auth/index.ts b/src/commands/config-auth/index.ts new file mode 100644 index 00000000..567ffed6 --- /dev/null +++ b/src/commands/config-auth/index.ts @@ -0,0 +1,94 @@ +/** + * Config Auth Commands (Facade) + * + * CLI interface for managing dashboard authentication. + * Commands: setup, show, disable + * + * Implementation follows auth-commands.ts pattern. + */ + +import { initUI, header, subheader, color, dim, fail } from '../../utils/ui'; + +// Import command handlers +import { handleSetup } from './setup-command'; +import { handleShow } from './show-command'; +import { handleDisable } from './disable-command'; + +/** + * Show help for config auth commands + */ +async function showHelp(): Promise { + await initUI(); + + console.log(''); + console.log(header('Dashboard Auth Management')); + console.log(''); + console.log(subheader('Usage')); + console.log(` ${color('ccs config auth', 'command')} `); + console.log(''); + console.log(subheader('Commands')); + console.log(` ${color('setup', 'command')} Configure username and password`); + console.log(` ${color('show', 'command')} Display current auth status`); + console.log(` ${color('disable', 'command')} Disable authentication`); + console.log(''); + console.log(subheader('Examples')); + console.log(` ${dim('# Interactive setup wizard')}`); + console.log(` ${color('ccs config auth setup', 'command')}`); + console.log(''); + console.log(` ${dim('# Check current status')}`); + console.log(` ${color('ccs config auth show', 'command')}`); + console.log(''); + console.log(` ${dim('# Disable authentication')}`); + console.log(` ${color('ccs config auth disable', 'command')}`); + console.log(''); + console.log(subheader('Environment Variables')); + console.log(' These override config.yaml values:'); + console.log( + ` ${color('CCS_DASHBOARD_AUTH_ENABLED', 'command')} Enable/disable (true/false)` + ); + console.log(` ${color('CCS_DASHBOARD_USERNAME', 'command')} Username`); + console.log(` ${color('CCS_DASHBOARD_PASSWORD_HASH', 'command')} Bcrypt hash`); + console.log(''); + console.log(subheader('Documentation')); + console.log(' https://ccs.kaitran.ca/features/dashboard-auth'); + console.log(''); +} + +/** + * Route config auth command to appropriate handler + */ +export async function handleConfigAuthCommand(args: string[]): Promise { + // Default to help if no subcommand + if (args.length === 0 || args[0] === '--help' || args[0] === '-h' || args[0] === 'help') { + await showHelp(); + return; + } + + const command = args[0]; + + switch (command) { + case 'setup': + await handleSetup(); + break; + + case 'show': + case 'status': + await handleShow(); + break; + + case 'disable': + await handleDisable(); + break; + + default: + await initUI(); + console.log(fail(`Unknown command: ${command}`)); + console.log(''); + console.log('Run for help:'); + console.log(` ${color('ccs config auth --help', 'command')}`); + process.exit(1); + } +} + +// Re-export types +export type { ConfigAuthContext, AuthSetupResult, AuthStatusInfo } from './types'; diff --git a/src/commands/config-auth/setup-command.ts b/src/commands/config-auth/setup-command.ts new file mode 100644 index 00000000..de40c32d --- /dev/null +++ b/src/commands/config-auth/setup-command.ts @@ -0,0 +1,133 @@ +/** + * Config Auth Setup Command + * + * Interactive wizard for configuring dashboard authentication. + * Prompts for username and password, hashes password with bcrypt, + * and saves to config.yaml. + */ + +import bcrypt from 'bcrypt'; +import { InteractivePrompt } from '../../utils/prompt'; +import { loadOrCreateUnifiedConfig, saveUnifiedConfig } from '../../config/unified-config-loader'; +import { initUI, header, subheader, ok, fail, info, warn, dim } from '../../utils/ui'; +import type { AuthSetupResult } from './types'; + +const BCRYPT_ROUNDS = 10; +const MIN_PASSWORD_LENGTH = 8; + +/** + * Validate username (non-empty, alphanumeric with underscores) + */ +function validateUsername(value: string): string | null { + if (!value || value.trim().length === 0) { + return 'Username cannot be empty'; + } + if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(value)) { + return 'Username must start with letter, contain only letters/numbers/underscores/hyphens'; + } + if (value.length < 3) { + return 'Username must be at least 3 characters'; + } + return null; +} + +/** + * Handle setup command - interactive wizard + */ +export async function handleSetup(): Promise { + await initUI(); + + console.log(''); + console.log(header('Dashboard Auth Setup')); + console.log(''); + console.log(info('Configure username and password for dashboard access.')); + console.log(dim(' Password will be hashed with bcrypt before storage.')); + console.log(''); + + // Check for ENV overrides + if ( + process.env.CCS_DASHBOARD_AUTH_ENABLED || + process.env.CCS_DASHBOARD_USERNAME || + process.env.CCS_DASHBOARD_PASSWORD_HASH + ) { + console.log(warn('Environment variables detected - they will override config values:')); + if (process.env.CCS_DASHBOARD_AUTH_ENABLED) { + console.log(` CCS_DASHBOARD_AUTH_ENABLED=${process.env.CCS_DASHBOARD_AUTH_ENABLED}`); + } + if (process.env.CCS_DASHBOARD_USERNAME) { + console.log(` CCS_DASHBOARD_USERNAME=${process.env.CCS_DASHBOARD_USERNAME}`); + } + if (process.env.CCS_DASHBOARD_PASSWORD_HASH) { + console.log(' CCS_DASHBOARD_PASSWORD_HASH=***'); + } + console.log(''); + } + + try { + // Prompt for username + console.log(subheader('Username')); + const username = await InteractivePrompt.input('Enter username', { + validate: validateUsername, + }); + + console.log(''); + + // Prompt for password + console.log(subheader('Password')); + console.log(dim(` Minimum ${MIN_PASSWORD_LENGTH} characters`)); + const password = await InteractivePrompt.password('Enter password'); + + // Validate password length + if (password.length < MIN_PASSWORD_LENGTH) { + console.log(''); + console.log(fail(`Password must be at least ${MIN_PASSWORD_LENGTH} characters`)); + return { success: false, error: 'Password too short' }; + } + + // Confirm password + const confirmPassword = await InteractivePrompt.password('Confirm password'); + + if (password !== confirmPassword) { + console.log(''); + console.log(fail('Passwords do not match')); + return { success: false, error: 'Password mismatch' }; + } + + console.log(''); + console.log(info('Hashing password...')); + + // Hash password + const passwordHash = await bcrypt.hash(password, BCRYPT_ROUNDS); + + // Load existing config and update + const config = loadOrCreateUnifiedConfig(); + config.dashboard_auth = { + enabled: true, + username, + password_hash: passwordHash, + session_timeout_hours: config.dashboard_auth?.session_timeout_hours ?? 24, + }; + + // Save config + saveUnifiedConfig(config); + + console.log(''); + console.log(ok('Dashboard authentication configured')); + console.log(''); + console.log(info('Settings saved to ~/.ccs/config.yaml')); + console.log(info(`Username: ${username}`)); + console.log(info(`Session timeout: ${config.dashboard_auth.session_timeout_hours} hours`)); + console.log(''); + console.log(dim(' Start dashboard: ccs config')); + console.log(dim(' Show status: ccs config auth show')); + console.log(dim(' Disable auth: ccs config auth disable')); + console.log(''); + + return { success: true, username }; + } catch (error) { + const err = error as Error; + console.log(''); + console.log(fail(`Setup failed: ${err.message}`)); + return { success: false, error: err.message }; + } +} diff --git a/src/commands/config-auth/show-command.ts b/src/commands/config-auth/show-command.ts new file mode 100644 index 00000000..8bbdc1bd --- /dev/null +++ b/src/commands/config-auth/show-command.ts @@ -0,0 +1,89 @@ +/** + * Config Auth Show Command + * + * Display current dashboard authentication status. + */ + +import { getDashboardAuthConfig } from '../../config/unified-config-loader'; +import { initUI, header, subheader, ok, info, warn, dim, color } from '../../utils/ui'; +import type { AuthStatusInfo } from './types'; + +/** + * Get auth status info with ENV override detection + */ +function getAuthStatus(): AuthStatusInfo { + const config = getDashboardAuthConfig(); + + return { + enabled: config.enabled, + configured: !!(config.username && config.password_hash), + username: config.username, + sessionTimeoutHours: config.session_timeout_hours ?? 24, + envOverride: { + enabled: process.env.CCS_DASHBOARD_AUTH_ENABLED !== undefined, + username: process.env.CCS_DASHBOARD_USERNAME !== undefined, + passwordHash: process.env.CCS_DASHBOARD_PASSWORD_HASH !== undefined, + }, + }; +} + +/** + * Handle show command - display current auth status + */ +export async function handleShow(): Promise { + await initUI(); + + console.log(''); + console.log(header('Dashboard Auth Status')); + console.log(''); + + const status = getAuthStatus(); + + // Status section + console.log(subheader('Configuration')); + + if (status.enabled) { + console.log(ok('Authentication: Enabled')); + } else { + console.log(info('Authentication: Disabled')); + } + + if (status.configured) { + console.log(ok(`Username: ${status.username}`)); + console.log(info(`Session timeout: ${status.sessionTimeoutHours} hours`)); + } else { + console.log(warn('Not configured - run `ccs config auth setup`')); + } + + console.log(''); + + // ENV override section + const hasEnvOverride = + status.envOverride.enabled || status.envOverride.username || status.envOverride.passwordHash; + + if (hasEnvOverride) { + console.log(subheader('Environment Overrides')); + console.log(warn('The following ENV vars override config.yaml:')); + + if (status.envOverride.enabled) { + const value = process.env.CCS_DASHBOARD_AUTH_ENABLED; + console.log(` ${color('CCS_DASHBOARD_AUTH_ENABLED', 'command')}=${value}`); + } + if (status.envOverride.username) { + const value = process.env.CCS_DASHBOARD_USERNAME; + console.log(` ${color('CCS_DASHBOARD_USERNAME', 'command')}=${value}`); + } + if (status.envOverride.passwordHash) { + console.log(` ${color('CCS_DASHBOARD_PASSWORD_HASH', 'command')}=***`); + } + + console.log(''); + } + + // Help section + console.log(subheader('Commands')); + console.log(dim(' ccs config auth setup Configure authentication')); + console.log(dim(' ccs config auth disable Disable authentication')); + console.log(dim(' ccs config Open dashboard')); + console.log(''); +} diff --git a/src/commands/config-auth/types.ts b/src/commands/config-auth/types.ts new file mode 100644 index 00000000..861dc3a6 --- /dev/null +++ b/src/commands/config-auth/types.ts @@ -0,0 +1,27 @@ +/** + * Config Auth Command Types + * + * Shared interfaces for dashboard authentication CLI commands. + */ + +export interface ConfigAuthContext { + verbose?: boolean; +} + +export interface AuthSetupResult { + success: boolean; + username?: string; + error?: string; +} + +export interface AuthStatusInfo { + enabled: boolean; + configured: boolean; + username: string; + sessionTimeoutHours: number; + envOverride: { + enabled: boolean; + username: boolean; + passwordHash: boolean; + }; +} diff --git a/src/commands/config-command.ts b/src/commands/config-command.ts index 1814fa06..e9132802 100644 --- a/src/commands/config-command.ts +++ b/src/commands/config-command.ts @@ -52,10 +52,16 @@ function parseArgs(args: string[]): ConfigOptions { */ function showHelp(): void { console.log(''); - console.log('Usage: ccs config [options]'); + console.log('Usage: ccs config [command] [options]'); console.log(''); console.log('Open web-based configuration dashboard'); console.log(''); + console.log('Commands:'); + console.log(' auth Manage dashboard authentication'); + console.log(' auth setup Configure username and password'); + console.log(' auth show Display current auth status'); + console.log(' auth disable Disable authentication'); + console.log(''); console.log('Options:'); console.log(' --port, -p PORT Specify server port (default: auto-detect)'); console.log(' --dev Development mode with Vite HMR'); @@ -65,6 +71,7 @@ function showHelp(): void { console.log(' ccs config Auto-detect available port'); console.log(' ccs config --port 3000 Use specific port'); console.log(' ccs config --dev Development mode with hot reload'); + console.log(' ccs config auth setup Configure dashboard login'); console.log(''); } @@ -72,6 +79,13 @@ function showHelp(): void { * Handle config command */ export async function handleConfigCommand(args: string[]): Promise { + // Route subcommands before dashboard launch + if (args[0] === 'auth') { + const { handleConfigAuthCommand } = await import('./config-auth'); + await handleConfigAuthCommand(args.slice(1)); + return; + } + await initUI(); const options = parseArgs(args); diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts index b443b5f2..29fc1c38 100644 --- a/src/commands/help-command.ts +++ b/src/commands/help-command.ts @@ -230,6 +230,8 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); ['ccs doctor', 'Run health check and diagnostics'], ['ccs cleanup', 'Remove old CLIProxy logs'], ['ccs config', 'Open web configuration dashboard'], + ['ccs config auth setup', 'Configure dashboard login'], + ['ccs config auth show', 'Show dashboard auth status'], ['ccs config --port 3000', 'Use specific port'], ['ccs sync', 'Sync delegation commands and skills'], ['ccs update', 'Update CCS to latest version'],