/** * Authentication commands for VC-SYS CLI * Handles: vcsys auth login, status, logout */ import { Command } from 'commander'; import chalk from 'chalk'; import ora from 'ora'; import { SupabaseAuth } from '../lib/auth/supabase-auth'; import { SupabaseManagementClient, SupabaseManagementError } from '../lib/supabase/management-client'; import { Logger } from '../lib/core/logger'; const logger = new Logger('AuthCommands'); /** * Create auth command with subcommands */ export function createAuthCommand(): Command { const authCommand = new Command('auth'); authCommand.description('Manage Supabase authentication'); // vcsys auth login authCommand .command('login') .description('Authenticate with Supabase using OAuth') .option('--force', 'Force re-authentication even if already logged in') .action(async (options) => { await handleLogin(options); }); // vcsys auth status authCommand .command('status') .description('Show current authentication status') .option('--json', 'Output status as JSON') .action(async (options) => { await handleStatus(options); }); // vcsys auth logout authCommand .command('logout') .description('Clear authentication and log out') .option('--force', 'Skip confirmation prompt') .action(async (options) => { await handleLogout(options); }); return authCommand; } /** * Handle auth login command */ async function handleLogin(options: { force?: boolean }): Promise { const auth = new SupabaseAuth(); try { // Check if already authenticated const isAuthenticated = await auth.isAuthenticated(); if (isAuthenticated && !options.force) { console.log(chalk.green('āœ… Already authenticated with Supabase')); const authInfo = await auth.getAuthInfo(); if (authInfo.supabaseUserId) { console.log(chalk.dim(` User ID: ${authInfo.supabaseUserId}`)); } if (authInfo.expiresAt) { const timeUntilExpiry = authInfo.expiresAt.getTime() - Date.now(); const hoursUntilExpiry = Math.floor(timeUntilExpiry / (1000 * 60 * 60)); console.log(chalk.dim(` Token expires in: ${hoursUntilExpiry} hours`)); } console.log(chalk.dim('\nUse --force to re-authenticate or "vcsys auth logout" to sign out first')); return; } if (options.force && isAuthenticated) { console.log(chalk.yellow('šŸ”„ Force re-authentication requested')); await auth.logout(); } // Perform authentication const result = await auth.authenticate(); if (!result.success) { console.error(chalk.red(`āŒ Authentication failed: ${result.message}`)); process.exit(1); } // Skip API test to avoid confusion - authentication has already succeeded console.log(chalk.green('\nāœ… Supabase account connected successfully!')); console.log(chalk.green('\nšŸŽÆ Next steps:')); console.log(chalk.dim(' • vcsys auth status - Check authentication details')); console.log(chalk.dim(' • vcsys provision - Create your first Supabase project')); logger.info('Login successful', { userId: result.userId }); } catch (error) { logger.error('Login command failed', error); console.error(chalk.red(`āŒ Login failed: ${error instanceof Error ? error.message : 'Unknown error'}`)); process.exit(1); } } /** * Handle auth status command */ async function handleStatus(options: { json?: boolean }): Promise { const auth = new SupabaseAuth(); try { const authInfo = await auth.getAuthInfo(); if (options.json) { // JSON output for programmatic usage const jsonOutput = { authenticated: authInfo.isAuthenticated, userId: authInfo.supabaseUserId || null, expiresAt: authInfo.expiresAt?.toISOString() || null, tokenPath: authInfo.tokenPath }; console.log(JSON.stringify(jsonOutput, null, 2)); return; } // Human-readable output console.log(chalk.bold('šŸ” Supabase Authentication Status\n')); if (authInfo.isAuthenticated) { console.log(chalk.green('āœ… Status: Authenticated')); if (authInfo.supabaseUserId) { console.log(chalk.dim(` User ID: ${authInfo.supabaseUserId}`)); } if (authInfo.expiresAt) { const now = new Date(); const timeUntilExpiry = authInfo.expiresAt.getTime() - now.getTime(); if (timeUntilExpiry > 0) { const hoursUntilExpiry = Math.floor(timeUntilExpiry / (1000 * 60 * 60)); const minutesUntilExpiry = Math.floor((timeUntilExpiry % (1000 * 60 * 60)) / (1000 * 60)); if (hoursUntilExpiry > 24) { const daysUntilExpiry = Math.floor(hoursUntilExpiry / 24); console.log(chalk.dim(` Token expires: In ${daysUntilExpiry} days`)); } else if (hoursUntilExpiry > 0) { console.log(chalk.dim(` Token expires: In ${hoursUntilExpiry}h ${minutesUntilExpiry}m`)); } else { console.log(chalk.yellow(` Token expires: In ${minutesUntilExpiry} minutes`)); } console.log(chalk.dim(` Expires at: ${authInfo.expiresAt.toLocaleString()}`)); } else { console.log(chalk.red(' Token: EXPIRED')); } } console.log(chalk.dim(` Token storage: ${authInfo.tokenPath}`)); // Test API access console.log('\n' + chalk.bold('🌐 API Connection Test')); const apiSpinner = ora('Testing Supabase Management API...').start(); try { const client = new SupabaseManagementClient(); const connectionTest = await client.testConnection(); if (connectionTest.isAuthenticated) { apiSpinner.succeed(`API connection working! Found ${connectionTest.organizationCount} organization(s)`); if (connectionTest.organizationCount === 0) { console.log(chalk.yellow(' āš ļø No organizations found - make sure you have Supabase access')); } } else { apiSpinner.fail('API connection failed'); console.log(chalk.red(` Error: ${connectionTest.error}`)); } } catch (error) { apiSpinner.fail('API test failed'); if (error instanceof SupabaseManagementError && error.requiresReauth) { console.log(chalk.yellow(' Authentication may have expired - try: vcsys auth login')); } else { console.log(chalk.red(` Error: ${error instanceof Error ? error.message : 'Unknown error'}`)); } } console.log(chalk.dim('\nNext steps:')); console.log(chalk.dim(' • vcsys provision - Create Supabase project')); console.log(chalk.dim(' • vcsys auth logout - Sign out')); } else { console.log(chalk.red('āŒ Status: Not authenticated')); console.log(chalk.dim('\nTo get started:')); console.log(chalk.dim(' • vcsys auth login - Sign in to Supabase')); } } catch (error) { logger.error('Status command failed', error); console.error(chalk.red(`āŒ Failed to check status: ${error instanceof Error ? error.message : 'Unknown error'}`)); process.exit(1); } } /** * Handle auth logout command */ async function handleLogout(options: { force?: boolean }): Promise { const auth = new SupabaseAuth(); try { const isAuthenticated = await auth.isAuthenticated(); if (!isAuthenticated) { console.log(chalk.yellow('āš ļø Not currently authenticated')); return; } if (!options.force) { // Show confirmation prompt const { default: inquirer } = await import('inquirer'); const { confirmed } = await inquirer.prompt([ { type: 'confirm', name: 'confirmed', message: 'Are you sure you want to log out?', default: false } ]); if (!confirmed) { console.log(chalk.dim('Logout cancelled')); return; } } const spinner = ora('Clearing authentication data...').start(); const success = await auth.logout(); if (success) { spinner.succeed('Logged out successfully'); console.log(chalk.green('āœ… You have been logged out of Supabase')); console.log(chalk.dim('\nTo sign back in:')); console.log(chalk.dim(' • vcsys auth login - Sign in to Supabase')); } else { spinner.fail('Logout failed'); console.error(chalk.red('āŒ Failed to clear authentication data')); process.exit(1); } logger.info('Logout successful'); } catch (error) { logger.error('Logout command failed', error); console.error(chalk.red(`āŒ Logout failed: ${error instanceof Error ? error.message : 'Unknown error'}`)); process.exit(1); } } /** * Utility function to check if user is authenticated (for other commands) */ export async function requireAuthentication(): Promise { const auth = new SupabaseAuth(); const isAuthenticated = await auth.isAuthenticated(); if (!isAuthenticated) { console.error(chalk.red('āŒ Not authenticated')); console.log(chalk.dim('Please sign in first:')); console.log(chalk.dim(' vcsys auth login')); process.exit(1); } return auth; } /** * Utility function to get access token for API calls */ export async function getAccessToken(): Promise { const auth = new SupabaseAuth(); const token = await auth.ensureValidToken(); if (!token) { console.error(chalk.red('āŒ No valid authentication token')); console.log(chalk.dim('Please sign in:')); console.log(chalk.dim(' vcsys auth login')); process.exit(1); } return token; }