UNPKG

@casoon/auditmysite

Version:

Professional website analysis suite with robust accessibility testing, Core Web Vitals performance monitoring, SEO analysis, and content optimization insights. Features isolated browser contexts, retry mechanisms, and comprehensive API endpoints for profe

623 lines 24.9 kB
"use strict"; /** * 🚀 AuditMySite REST API Server * * Express.js-based REST API for remote accessibility auditing. * Clean implementation for v1.8.0 with full TypeScript compatibility. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AuditAPIServer = void 0; const express_1 = __importDefault(require("express")); const cors_1 = __importDefault(require("cors")); const helmet_1 = __importDefault(require("helmet")); const express_rate_limit_1 = __importDefault(require("express-rate-limit")); const swagger_ui_express_1 = __importDefault(require("swagger-ui-express")); const uuid_1 = require("uuid"); const audit_sdk_1 = require("../sdk/audit-sdk"); // ============================================================================= // API Server Class // ============================================================================= class AuditAPIServer { constructor(config = {}) { this.server = null; this.config = this.mergeConfig(config); this.jobManager = { jobs: new Map(), runningJobs: new Set(), maxConcurrent: this.config.maxConcurrentJobs }; this.sdk = new audit_sdk_1.AuditSDK(); this.app = (0, express_1.default)(); this.setupMiddleware(); this.setupRoutes(); this.setupErrorHandling(); } /** * Start the API server */ async start() { return new Promise((resolve, reject) => { try { this.server = this.app.listen(this.config.port, this.config.host, () => { console.log(`🚀 AuditMySite API Server running at http://${this.config.host}:${this.config.port}`); resolve(); }); this.server.on('error', reject); } catch (error) { reject(error); } }); } /** * Get Express app instance (for testing) */ getApp() { return this.app; } /** * Shutdown server and clean up resources */ async shutdown() { if (this.server) { return new Promise((resolve) => { this.server.close(() => { this.server = null; resolve(); }); }); } } mergeConfig(config) { return { port: 3000, host: '0.0.0.0', apiKeyRequired: process.env.NODE_ENV === 'production', validApiKeys: process.env.API_KEYS?.split(',') || [], maxConcurrentJobs: 5, jobTimeout: 300000, // 5 minutes enableSwagger: true, corsOrigins: ['*'], ...config }; } setupMiddleware() { // Security middleware this.app.use((0, helmet_1.default)()); // CORS this.app.use((0, cors_1.default)({ origin: this.config.corsOrigins, credentials: true })); // Body parsing this.app.use(express_1.default.json({ limit: '10mb' })); this.app.use(express_1.default.urlencoded({ extended: true })); // Rate limiting const limiter = (0, express_rate_limit_1.default)({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit each IP to 100 requests per windowMs message: this.createErrorResponse('RATE_LIMIT_EXCEEDED', 'Too many requests'), standardHeaders: true, legacyHeaders: false }); this.app.use(limiter); // Request ID middleware const requestIdHandler = (req, res, next) => { req.requestId = (0, uuid_1.v4)(); res.setHeader('X-Request-ID', req.requestId); next(); }; this.app.use(requestIdHandler); } setupRoutes() { // Health check (no auth required) this.app.get('/health', (req, res) => { res.json({ success: true, data: { status: 'healthy', timestamp: new Date().toISOString(), version: '2.0.0-alpha.1', // Updated for v2.0 uptime: process.uptime(), jobs: { total: this.jobManager.jobs.size, running: this.jobManager.runningJobs.size } } }); }); // v2.0 API Routes (no auth required for now) const { createV2Router } = require('./routes/v2.routes'); this.app.use('/api/v2', createV2Router()); // Swagger UI for v2.0 API documentation if (this.config.enableSwagger) { const swaggerSpec = { openapi: '3.0.0', info: { title: 'AuditMySite API v2.0', version: '2.0.0-alpha.1', description: 'Modular API for website analysis using shared TypeScript types. Designed for Electron app integration.' }, servers: [ { url: '/api/v2', description: 'v2.0 API (modular)' }, { url: '/api/v1', description: 'v1.0 API (full site analysis)' } ], paths: { '/sitemap/{domain}': { get: { summary: 'Get sitemap URLs for domain', parameters: [{ name: 'domain', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': { description: 'SitemapResult with URLs and metadata' }, '500': { description: 'Sitemap parsing failed' } } } }, '/page/accessibility': { post: { summary: 'Analyze accessibility for single URL', requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', properties: { url: { type: 'string', description: 'URL to analyze' }, options: { type: 'object', properties: { pa11yStandard: { type: 'string', enum: ['WCAG2A', 'WCAG2AA', 'WCAG2AAA'] }, includeWarnings: { type: 'boolean' } } } }, required: ['url'] } } } }, responses: { '200': { description: 'AccessibilityResult with score and issues' }, '400': { description: 'Invalid request' }, '500': { description: 'Analysis failed' } } } }, '/schema': { get: { summary: 'API introspection for Electron app discovery', responses: { '200': { description: 'Available endpoints and type definitions' } } } } } }; this.app.use('/api-docs', swagger_ui_express_1.default.serve, swagger_ui_express_1.default.setup(swaggerSpec)); console.log('📚 API documentation available at /api-docs'); } // Apply API Key authentication to API endpoints only if (this.config.apiKeyRequired) { this.app.use('/api/v1/*', this.authenticateApiKey.bind(this)); } // API info this.app.get('/api/v1/info', (req, res) => { res.json({ success: true, data: { name: 'AuditMySite API', version: '1.8.8', description: 'REST API for comprehensive website analysis with enhanced accessibility, performance, SEO, and content weight testing', features: [ 'Enhanced Accessibility Analysis (ARIA, Focus, Color Contrast)', 'Core Web Vitals Performance Metrics', 'Advanced SEO Analysis', 'Content Weight Assessment', 'Multiple Report Formats (HTML, JSON, CSV)' ], endpoints: [ '/health', '/api/v1/audit', '/api/v1/audit/quick', '/api/v1/audit/performance', '/api/v1/audit/seo', '/api/v1/audit/content-weight' ], options: { 'accessibility': 'Enable enhanced accessibility analysis (default: true)', 'performance': 'Enable Core Web Vitals collection (default: true)', 'seo': 'Enable SEO analysis (default: true)', 'contentWeight': 'Enable content weight assessment (default: true)', 'reduced': 'Use reduced analysis mode (default: false)', 'includeRecommendations': 'Include actionable recommendations (default: true)', 'outputFormat': 'Output format: json, html, csv (default: json)' }, maxConcurrentJobs: this.config.maxConcurrentJobs } }); }); // Audit endpoints this.app.post('/api/v1/audit', this.handleCreateAudit.bind(this)); this.app.get('/api/v1/audit/:jobId', this.handleGetAudit.bind(this)); this.app.delete('/api/v1/audit/:jobId', this.handleCancelAudit.bind(this)); this.app.get('/api/v1/audits', this.handleListAudits.bind(this)); // Quick audit endpoint this.app.post('/api/v1/audit/quick', this.handleQuickAudit.bind(this)); // Specialized analysis endpoints this.app.post('/api/v1/audit/performance', this.handlePerformanceAudit.bind(this)); this.app.post('/api/v1/audit/seo', this.handleSeoAudit.bind(this)); this.app.post('/api/v1/audit/content-weight', this.handleContentWeightAudit.bind(this)); this.app.post('/api/v1/audit/accessibility', this.handleAccessibilityAudit.bind(this)); // Test connection endpoint this.app.post('/api/v1/test-connection', this.handleTestConnection.bind(this)); // Reports endpoints this.app.get('/api/v1/audit/:jobId/reports', this.handleGetReports.bind(this)); this.app.get('/api/v1/audit/:jobId/reports/:format', this.handleDownloadReport.bind(this)); // 404 handler this.app.use('*', (req, res) => { res.status(404).json(this.createErrorResponse('NOT_FOUND', 'Endpoint not found')); }); } setupErrorHandling() { this.app.use((error, req, res, next) => { console.error(`API Error [${req.requestId}]:`, error); const statusCode = error.statusCode || 500; const response = this.createErrorResponse(error.code || 'INTERNAL_ERROR', error.message || 'Internal server error'); res.status(statusCode).json(response); }); } async authenticateApiKey(req, res, next) { const apiKey = req.headers['x-api-key'] || req.query.apiKey; if (!apiKey) { res.status(401).json(this.createErrorResponse('AUTH_REQUIRED', 'API key required')); return; } if (!this.config.validApiKeys.includes(apiKey)) { res.status(401).json(this.createErrorResponse('INVALID_API_KEY', 'Invalid API key')); return; } req.apiKey = apiKey; next(); } async handleCreateAudit(req, res) { try { const jobRequest = req.body; const jobId = (0, uuid_1.v4)(); // Validate input if (!jobRequest.sitemapUrl) { res.status(400).json(this.createErrorResponse('INVALID_INPUT', 'sitemapUrl is required')); return; } // Check concurrent job limit if (this.jobManager.runningJobs.size >= this.config.maxConcurrentJobs) { res.status(429).json(this.createErrorResponse('TOO_MANY_JOBS', 'Maximum concurrent jobs reached')); return; } // Create job with enhanced defaults const enhancedOptions = { accessibility: true, performance: true, seo: true, contentWeight: true, includeRecommendations: true, reduced: false, ...(jobRequest.options || {}) }; const job = { id: jobId, status: 'pending', sitemapUrl: jobRequest.sitemapUrl, options: enhancedOptions, createdAt: new Date(), progress: { current: 0, total: 100, percentage: 0 } }; this.jobManager.jobs.set(jobId, job); // Start audit in background this.startAuditJob(jobId).catch(error => { console.error(`Job ${jobId} failed:`, error); const failedJob = this.jobManager.jobs.get(jobId); if (failedJob) { failedJob.status = 'failed'; failedJob.error = error.message; failedJob.completedAt = new Date(); } }); res.status(201).json({ success: true, data: { jobId, status: 'pending' } }); } catch (error) { res.status(500).json(this.createErrorResponse('INTERNAL_ERROR', 'Failed to create audit')); } } async handleGetAudit(req, res) { const { jobId } = req.params; const job = this.jobManager.jobs.get(jobId); if (!job) { res.status(404).json(this.createErrorResponse('JOB_NOT_FOUND', 'Audit job not found')); return; } res.json({ success: true, data: job }); } async handleCancelAudit(req, res) { const { jobId } = req.params; const job = this.jobManager.jobs.get(jobId); if (!job) { res.status(404).json(this.createErrorResponse('JOB_NOT_FOUND', 'Audit job not found')); return; } job.status = 'cancelled'; job.completedAt = new Date(); this.jobManager.runningJobs.delete(jobId); res.json({ success: true, data: { jobId, status: 'cancelled' } }); } async handleListAudits(req, res) { const { status, limit = '10', offset = '0' } = req.query; let jobs = Array.from(this.jobManager.jobs.values()); if (status) { jobs = jobs.filter(job => job.status === status); } const startIndex = parseInt(offset); const endIndex = startIndex + parseInt(limit); const paginatedJobs = jobs.slice(startIndex, endIndex); res.json({ success: true, data: { jobs: paginatedJobs, total: jobs.length, offset: startIndex, limit: parseInt(limit) } }); } async handleQuickAudit(req, res) { try { const { sitemapUrl, options } = req.body; if (!sitemapUrl) { res.status(400).json(this.createErrorResponse('INVALID_INPUT', 'sitemapUrl is required')); return; } // Merge options with new defaults: enhanced features are on by default const mergedOptions = { accessibility: true, performance: true, seo: true, contentWeight: true, includeRecommendations: true, reduced: false, ...(options || {}) }; const result = await this.sdk.quickAudit(sitemapUrl, mergedOptions); res.json({ success: true, data: result }); } catch (error) { res.status(500).json(this.createErrorResponse('AUDIT_ERROR', 'Audit failed')); } } async handlePerformanceAudit(req, res) { try { const { sitemapUrl, options } = req.body; if (!sitemapUrl) { res.status(400).json(this.createErrorResponse('INVALID_INPUT', 'sitemapUrl is required')); return; } const performanceOptions = { accessibility: false, performance: true, seo: false, contentWeight: false, includeRecommendations: true, reduced: false, ...(options || {}) }; const result = await this.sdk.quickAudit(sitemapUrl, performanceOptions); res.json({ success: true, data: { ...result, analysisType: 'performance', focus: 'Core Web Vitals and performance metrics' } }); } catch (error) { res.status(500).json(this.createErrorResponse('PERFORMANCE_AUDIT_ERROR', 'Performance audit failed')); } } async handleSeoAudit(req, res) { try { const { sitemapUrl, options } = req.body; if (!sitemapUrl) { res.status(400).json(this.createErrorResponse('INVALID_INPUT', 'sitemapUrl is required')); return; } const seoOptions = { accessibility: false, performance: false, seo: true, contentWeight: false, includeRecommendations: true, reduced: false, ...(options || {}) }; const result = await this.sdk.quickAudit(sitemapUrl, seoOptions); res.json({ success: true, data: { ...result, analysisType: 'seo', focus: 'Search engine optimization analysis' } }); } catch (error) { res.status(500).json(this.createErrorResponse('SEO_AUDIT_ERROR', 'SEO audit failed')); } } async handleContentWeightAudit(req, res) { try { const { sitemapUrl, options } = req.body; if (!sitemapUrl) { res.status(400).json(this.createErrorResponse('INVALID_INPUT', 'sitemapUrl is required')); return; } const contentOptions = { accessibility: false, performance: false, seo: false, contentWeight: true, includeRecommendations: true, reduced: false, ...(options || {}) }; const result = await this.sdk.quickAudit(sitemapUrl, contentOptions); res.json({ success: true, data: { ...result, analysisType: 'content-weight', focus: 'Content weight and optimization analysis' } }); } catch (error) { res.status(500).json(this.createErrorResponse('CONTENT_WEIGHT_AUDIT_ERROR', 'Content weight audit failed')); } } async handleAccessibilityAudit(req, res) { try { const { sitemapUrl, options } = req.body; if (!sitemapUrl) { res.status(400).json(this.createErrorResponse('INVALID_INPUT', 'sitemapUrl is required')); return; } const accessibilityOptions = { accessibility: true, performance: false, seo: false, contentWeight: false, includeRecommendations: true, reduced: false, ...(options || {}) }; const result = await this.sdk.quickAudit(sitemapUrl, accessibilityOptions); res.json({ success: true, data: { ...result, analysisType: 'accessibility', focus: 'Enhanced accessibility and WCAG compliance analysis' } }); } catch (error) { res.status(500).json(this.createErrorResponse('ACCESSIBILITY_AUDIT_ERROR', 'Accessibility audit failed')); } } async handleTestConnection(req, res) { try { const { sitemapUrl } = req.body; if (!sitemapUrl) { res.status(400).json(this.createErrorResponse('INVALID_INPUT', 'sitemapUrl is required')); return; } const result = await this.sdk.testConnection(sitemapUrl); res.json({ success: true, data: result }); } catch (error) { res.status(500).json(this.createErrorResponse('CONNECTION_ERROR', 'Connection test failed')); } } async handleGetReports(req, res) { const { jobId } = req.params; const job = this.jobManager.jobs.get(jobId); if (!job) { res.status(404).json(this.createErrorResponse('JOB_NOT_FOUND', 'Audit job not found')); return; } if (job.status !== 'completed' || !job.result) { res.status(400).json(this.createErrorResponse('JOB_NOT_COMPLETE', 'Job not completed yet')); return; } res.json({ success: true, data: { reports: job.result.reports || [] } }); } async handleDownloadReport(req, res) { const { jobId, format } = req.params; const job = this.jobManager.jobs.get(jobId); if (!job) { res.status(404).json(this.createErrorResponse('JOB_NOT_FOUND', 'Audit job not found')); return; } if (job.status !== 'completed' || !job.result) { res.status(404).json(this.createErrorResponse('REPORT_NOT_FOUND', 'Report not available')); return; } const report = job.result.reports?.find(r => r.format === format); if (!report) { res.status(404).json(this.createErrorResponse('REPORT_NOT_FOUND', `Report in ${format} format not found`)); return; } res.json({ success: true, data: report }); } async startAuditJob(jobId) { const job = this.jobManager.jobs.get(jobId); if (!job) return; job.status = 'running'; job.startedAt = new Date(); this.jobManager.runningJobs.add(jobId); try { const result = await this.sdk.quickAudit(job.sitemapUrl, job.options); job.status = 'completed'; job.result = result; job.completedAt = new Date(); job.progress = { current: 100, total: 100, percentage: 100 }; } catch (error) { job.status = 'failed'; job.error = error.message; job.completedAt = new Date(); } finally { this.jobManager.runningJobs.delete(jobId); } } createErrorResponse(code, message, details) { return { success: false, error: { code, message, details }, data: null }; } } exports.AuditAPIServer = AuditAPIServer; //# sourceMappingURL=server.js.map