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
JavaScript
;
/**
* @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;