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
This commit is contained in:
kaitranntt
2026-01-13 15:13:10 -05:00
parent 37e3468d4d
commit 39c1ee2ca0
8 changed files with 601 additions and 1 deletions
+166
View File
@@ -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
@@ -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<void> {
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('');
}
+94
View File
@@ -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<void> {
await initUI();
console.log('');
console.log(header('Dashboard Auth Management'));
console.log('');
console.log(subheader('Usage'));
console.log(` ${color('ccs config auth', 'command')} <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<void> {
// 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';
+133
View File
@@ -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<AuthSetupResult> {
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 };
}
}
+89
View File
@@ -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<void> {
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('');
}
+27
View File
@@ -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;
};
}
+15 -1
View File
@@ -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<void> {
// 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);
+2
View File
@@ -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'],