UNPKG

mcp-quiz-server

Version:

🧠 AI-Powered Quiz Management via Model Context Protocol (MCP) - Create, manage, and take quizzes directly from VS Code, Claude, and other AI agents.

328 lines (327 loc) • 13.6 kB
"use strict"; /** * @fileoverview Enhanced Minimal Setup Wizard - DigitalOcean deployment focused * @version 1.0.0 * @since 2025-07-31 * @module MinimalSetupWizard * @description Minimal interactive setup for DigitalOcean deployment with Jenkins-style password generation * @contributors Claude Code Agent * @requirements REQ_SETUP_001-005 (DigitalOcean deployment) */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MinimalSetupWizard = void 0; const fs_1 = require("fs"); const path_1 = require("path"); const readline_1 = require("readline"); const crypto_1 = require("crypto"); const AsciiAnimations_1 = __importDefault(require("./AsciiAnimations")); /** * Minimal Setup Wizard for DigitalOcean deployment * * @description Simple, fast setup with focus on security and ease of deployment. * Jenkins-style random password generation with file output. */ class MinimalSetupWizard { constructor() { this.setupDir = (0, path_1.join)(process.cwd(), '.setup'); this.configDir = (0, path_1.join)(process.cwd(), 'config'); this.ensureDirectories(); } ensureDirectories() { if (!(0, fs_1.existsSync)(this.setupDir)) { (0, fs_1.mkdirSync)(this.setupDir, { recursive: true }); } if (!(0, fs_1.existsSync)(this.configDir)) { (0, fs_1.mkdirSync)(this.configDir, { recursive: true }); } } /** * Check if setup is already complete */ async isSetupComplete() { const setupCompleteFile = (0, path_1.join)(this.setupDir, 'setup-complete.json'); if (!(0, fs_1.existsSync)(setupCompleteFile)) { return false; } try { const setupData = JSON.parse((0, fs_1.readFileSync)(setupCompleteFile, 'utf8')); return setupData.completed && setupData.version; } catch (_a) { return false; } } /** * Force setup to run again */ shouldForceSetup() { return process.env.FORCE_SETUP === 'true' || process.argv.includes('--force-setup'); } /** * Main setup execution - the entry point */ async runIfNeeded() { // Check if setup is needed if ((await this.isSetupComplete()) && !this.shouldForceSetup()) { return null; } // Clear screen and show welcome console.clear(); AsciiAnimations_1.default.displayWelcomeBanner(); console.log('\nšŸš€ DigitalOcean Deployment Setup'); console.log('====================================='); console.log('Quick setup for production deployment (3 questions, ~30 seconds)'); console.log(''); // Show progress steps const steps = [ 'Environment validation', 'Admin user creation', 'Server configuration', 'Security setup', 'Final configuration', ]; AsciiAnimations_1.default.displayStepProgress(steps, 0); try { const result = await this.runMinimalFlow(); await this.saveSetupResult(result); await this.displaySuccessMessage(result); return result; } catch (error) { AsciiAnimations_1.default.displayError(`Setup failed: ${error.message}`); throw error; } } /** * Run the minimal setup flow for DigitalOcean */ async runMinimalFlow() { this.rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout, }); try { // Step 1: Environment validation AsciiAnimations_1.default.displayStageGraphic('server'); const stopValidation = AsciiAnimations_1.default.startSpinner('Validating environment...'); await this.validateEnvironment(); stopValidation(); console.log('āœ… Environment validated\n'); // Step 2: Admin user setup AsciiAnimations_1.default.displayStageGraphic('security'); const adminConfig = await this.setupAdminUser(); // Step 3: Server configuration const serverConfig = await this.setupServerConfig(); // Step 4: Security configuration const securityConfig = await this.setupSecurity(); // Step 5: Feature configuration (use DigitalOcean defaults) AsciiAnimations_1.default.displayStageGraphic('final'); const features = this.getDigitalOceanDefaults(); const result = { admin: adminConfig, server: serverConfig, security: securityConfig, features, setupTimestamp: new Date(), }; return result; } finally { this.rl.close(); } } /** * Validate the environment for DigitalOcean deployment */ async validateEnvironment() { // Check Node.js version const nodeVersion = process.version; const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0]); if (majorVersion < 16) { throw new Error(`Node.js 16+ required, found ${nodeVersion}`); } // Check write permissions try { const testFile = (0, path_1.join)(this.setupDir, 'test-write'); (0, fs_1.writeFileSync)(testFile, 'test'); require('fs').unlinkSync(testFile); } catch (_a) { throw new Error('Insufficient file system permissions'); } // Add small delay for animation effect await new Promise(resolve => setTimeout(resolve, 1500)); } /** * Setup admin user with Jenkins-style password generation */ async setupAdminUser() { console.log('\nšŸ‘¤ Admin User Setup'); console.log('==================='); const username = (await this.prompt('Admin username [admin]: ')) || 'admin'; const email = (await this.prompt('Admin email [admin@localhost]: ')) || 'admin@localhost'; console.log('\nšŸ” Password Options:'); console.log('1. Generate secure password (recommended)'); console.log('2. Enter custom password'); const passwordChoice = (await this.prompt('Choose option [1]: ')) || '1'; let password; if (passwordChoice === '1') { console.log('\nšŸŽ² Generating Jenkins-style secure password...'); password = await AsciiAnimations_1.default.animatePasswordGeneration({ duration: 2000 }); console.log('\nāš ļø IMPORTANT: Save this password securely! It will be saved to initial-admin-password.txt'); } else { password = await this.promptPassword('Enter admin password: '); if (password.length < 8) { throw new Error('Password must be at least 8 characters long'); } } return { username, email, password }; } /** * Setup server configuration */ async setupServerConfig() { console.log('\n🌐 Server Configuration'); console.log('======================='); const port = parseInt((await this.prompt('Server port [3000]: ')) || '3000'); if (port < 1024 || port > 65535) { throw new Error('Port must be between 1024 and 65535'); } const host = (await this.prompt('Bind to host [0.0.0.0]: ')) || '0.0.0.0'; const environment = (await this.prompt('Environment [production]: ')) || 'production'; return { port, host, environment }; } /** * Setup security configuration */ async setupSecurity() { console.log('\nšŸ”’ Security Configuration'); console.log('========================='); const stopSpinner = AsciiAnimations_1.default.startSpinner('Generating JWT secrets and security keys...'); // Generate secure JWT secret const jwtSecret = (0, crypto_1.randomBytes)(64).toString('hex'); // Add delay for animation await new Promise(resolve => setTimeout(resolve, 2000)); stopSpinner(); console.log('āœ… Security keys generated\n'); return { jwtSecret, sessionTimeout: 30, // 30 minutes maxSessions: 3, enableRotation: true, }; } /** * Get DigitalOcean optimized feature defaults */ getDigitalOceanDefaults() { return { userAuth: true, // Always enabled in production cloudSync: false, // Disabled for MVP multiUser: false, // Disabled for MVP analytics: false, // Disabled for MVP localQuizzes: true, // Core MVP feature timerFeature: true, // Core MVP feature mcpProtocol: true, // Core MVP feature }; } /** * Save setup result to files */ async saveSetupResult(result) { // Save completion flag const setupCompleteFile = (0, path_1.join)(this.setupDir, 'setup-complete.json'); (0, fs_1.writeFileSync)(setupCompleteFile, JSON.stringify({ completed: true, timestamp: result.setupTimestamp.toISOString(), version: '1.0.0', }, null, 2)); // Save admin password to secure file (Jenkins-style) const passwordFile = (0, path_1.join)(this.setupDir, 'initial-admin-password.txt'); const passwordContent = `MCP Quiz Server - Initial Admin Password\n` + `Generated: ${result.setupTimestamp.toISOString()}\n\n` + `Username: ${result.admin.username}\n` + `Password: ${result.admin.password}\n` + `Email: ${result.admin.email}\n\n` + `IMPORTANT: Change this password after first login!\n` + `Access the admin panel at: http://your-server:${result.server.port}/admin\n`; (0, fs_1.writeFileSync)(passwordFile, passwordContent, { mode: 0o600 }); // Save full configuration const configFile = (0, path_1.join)(this.configDir, 'setup-config.json'); (0, fs_1.writeFileSync)(configFile, JSON.stringify(result, null, 2), { mode: 0o600 }); } /** * Display setup success message */ async displaySuccessMessage(result) { console.clear(); AsciiAnimations_1.default.displaySuccess('Setup completed successfully!'); console.log('šŸŽ‰ MCP Quiz Server Setup Complete!'); console.log('==================================\n'); console.log('šŸ“‹ Configuration Summary:'); console.log(` • Admin User: ${result.admin.username}`); console.log(` • Server: ${result.server.host}:${result.server.port}`); console.log(` • Environment: ${result.server.environment}`); console.log(` • Security: JWT tokens enabled`); console.log(` • Features: ${Object.entries(result.features) .filter(([, v]) => v) .map(([k]) => k) .join(', ')}\n`); console.log('šŸ” Admin Credentials (SAVE SECURELY):'); console.log(` Username: ${result.admin.username}`); console.log(` Password: ${result.admin.password}`); console.log(` Email: ${result.admin.email}\n`); console.log('šŸ“ Files Created:'); console.log(` • ${(0, path_1.join)(this.setupDir, 'initial-admin-password.txt')} (read with: cat .setup/initial-admin-password.txt)`); console.log(` • ${(0, path_1.join)(this.configDir, 'setup-config.json')}`); console.log(` • ${(0, path_1.join)(this.setupDir, 'setup-complete.json')}\n`); console.log('šŸš€ Next Steps:'); console.log(' 1. Save admin credentials in your password manager'); console.log(' 2. Configure firewall: ufw allow 3000/tcp'); console.log(' 3. Start server: npm start'); console.log(` 4. Access admin panel: http://your-droplet-ip:${result.server.port}/admin\n`); console.log('āš ļø Important: The initial password file will be automatically deleted after first successful login.'); } /** * Prompt user for input */ async prompt(question) { return new Promise(resolve => { this.rl.question(question, (answer) => { resolve(answer.trim()); }); }); } /** * Prompt for password (with hidden input simulation) */ async promptPassword(question) { // For simplicity, we'll use regular prompt but warn about visibility console.log('āš ļø Password will be visible while typing (SSH limitation)'); return this.prompt(question); } /** * Generate secure password using Jenkins-style algorithm */ static generateJenkinsStylePassword(length = 16) { // Jenkins uses characters that avoid confusion (no 0, O, 1, l, I) const charset = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789'; let password = ''; for (let i = 0; i < length; i++) { const randomIndex = (0, crypto_1.randomBytes)(1)[0] % charset.length; password += charset[randomIndex]; } return password; } /** * Hash password for storage */ static hashPassword(password) { return (0, crypto_1.createHash)('sha256').update(password).digest('hex'); } } exports.MinimalSetupWizard = MinimalSetupWizard;