/** * HTTP Server for Onebox * * Serves REST API and Angular UI */ import * as plugins from '../plugins.ts'; import { logger } from '../logging.ts'; import type { Onebox } from './onebox.ts'; import type { IApiResponse } from '../types.ts'; export class OneboxHttpServer { private oneboxRef: Onebox; private server: Deno.HttpServer | null = null; private port = 3000; constructor(oneboxRef: Onebox) { this.oneboxRef = oneboxRef; } /** * Start HTTP server */ async start(port?: number): Promise { try { if (this.server) { logger.warn('HTTP server already running'); return; } this.port = port || 3000; logger.info(`Starting HTTP server on port ${this.port}...`); this.server = Deno.serve({ port: this.port }, (req) => this.handleRequest(req)); logger.success(`HTTP server started on http://localhost:${this.port}`); } catch (error) { logger.error(`Failed to start HTTP server: ${error.message}`); throw error; } } /** * Stop HTTP server */ async stop(): Promise { try { if (!this.server) { return; } logger.info('Stopping HTTP server...'); await this.server.shutdown(); this.server = null; logger.success('HTTP server stopped'); } catch (error) { logger.error(`Failed to stop HTTP server: ${error.message}`); throw error; } } /** * Handle HTTP request */ private async handleRequest(req: Request): Promise { const url = new URL(req.url); const path = url.pathname; logger.debug(`${req.method} ${path}`); try { // API routes if (path.startsWith('/api/')) { return await this.handleApiRequest(req, path); } // Serve Angular UI return await this.serveStaticFile(path); } catch (error) { logger.error(`Request error: ${error.message}`); return this.jsonResponse({ success: false, error: error.message }, 500); } } /** * Serve static files from ui/dist */ private async serveStaticFile(path: string): Promise { try { // Default to index.html for root and non-file paths let filePath = path === '/' ? '/index.html' : path; // For Angular routing - serve index.html for non-asset paths if (!filePath.includes('.') && filePath !== '/index.html') { filePath = '/index.html'; } const fullPath = `./ui/dist${filePath}`; // Read file const file = await Deno.readFile(fullPath); // Determine content type const contentType = this.getContentType(filePath); return new Response(file, { headers: { 'Content-Type': contentType, 'Cache-Control': filePath === '/index.html' ? 'no-cache' : 'public, max-age=3600', }, }); } catch (error) { // File not found - serve index.html for Angular routing if (error instanceof Deno.errors.NotFound) { try { const indexFile = await Deno.readFile('./ui/dist/index.html'); return new Response(indexFile, { headers: { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache', }, }); } catch { return new Response('UI not built. Run: cd ui && npm run build', { status: 404, headers: { 'Content-Type': 'text/plain' }, }); } } return new Response('File not found', { status: 404, headers: { 'Content-Type': 'text/plain' }, }); } } /** * Get content type for file */ private getContentType(path: string): string { const ext = path.split('.').pop()?.toLowerCase(); const mimeTypes: Record = { 'html': 'text/html', 'css': 'text/css', 'js': 'application/javascript', 'json': 'application/json', 'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'gif': 'image/gif', 'svg': 'image/svg+xml', 'ico': 'image/x-icon', 'woff': 'font/woff', 'woff2': 'font/woff2', 'ttf': 'font/ttf', 'eot': 'application/vnd.ms-fontobject', }; return mimeTypes[ext || ''] || 'application/octet-stream'; } /** * Handle API requests */ private async handleApiRequest(req: Request, path: string): Promise { const method = req.method; // Auth check (simplified - should use proper JWT middleware) // Skip auth for login endpoint if (path !== '/api/auth/login') { const authHeader = req.headers.get('Authorization'); if (!authHeader || !authHeader.startsWith('Bearer ')) { return this.jsonResponse({ success: false, error: 'Unauthorized' }, 401); } } // Route to appropriate handler if (path === '/api/auth/login' && method === 'POST') { return await this.handleLoginRequest(req); } else if (path === '/api/status' && method === 'GET') { return await this.handleStatusRequest(); } else if (path === '/api/settings' && method === 'GET') { return await this.handleGetSettingsRequest(); } else if (path === '/api/settings' && method === 'PUT') { return await this.handleUpdateSettingsRequest(req); } else if (path === '/api/services' && method === 'GET') { return await this.handleListServicesRequest(); } else if (path === '/api/services' && method === 'POST') { return await this.handleDeployServiceRequest(req); } else if (path.match(/^\/api\/services\/[^/]+$/) && method === 'GET') { const name = path.split('/').pop()!; return await this.handleGetServiceRequest(name); } else if (path.match(/^\/api\/services\/[^/]+$/) && method === 'DELETE') { const name = path.split('/').pop()!; return await this.handleDeleteServiceRequest(name); } else if (path.match(/^\/api\/services\/[^/]+\/start$/) && method === 'POST') { const name = path.split('/')[3]; return await this.handleStartServiceRequest(name); } else if (path.match(/^\/api\/services\/[^/]+\/stop$/) && method === 'POST') { const name = path.split('/')[3]; return await this.handleStopServiceRequest(name); } else if (path.match(/^\/api\/services\/[^/]+\/restart$/) && method === 'POST') { const name = path.split('/')[3]; return await this.handleRestartServiceRequest(name); } else if (path.match(/^\/api\/services\/[^/]+\/logs$/) && method === 'GET') { const name = path.split('/')[3]; return await this.handleGetLogsRequest(name); } else { return this.jsonResponse({ success: false, error: 'Not found' }, 404); } } // API Handlers private async handleLoginRequest(req: Request): Promise { try { const body = await req.json(); const { username, password } = body; logger.info(`Login attempt for user: ${username}`); if (!username || !password) { return this.jsonResponse( { success: false, error: 'Username and password required' }, 400 ); } // Get user from database const user = this.oneboxRef.database.getUserByUsername(username); if (!user) { logger.info(`User not found: ${username}`); return this.jsonResponse({ success: false, error: 'Invalid credentials' }, 401); } logger.info(`User found: ${username}, checking password...`); // Verify password (simple base64 comparison for now) const passwordHash = btoa(password); logger.info(`Password hash: ${passwordHash}, stored hash: ${user.passwordHash}`); if (passwordHash !== user.passwordHash) { logger.info(`Password mismatch for user: ${username}`); return this.jsonResponse({ success: false, error: 'Invalid credentials' }, 401); } // Generate simple token (in production, use proper JWT) const token = btoa(`${user.username}:${Date.now()}`); return this.jsonResponse({ success: true, data: { token, user: { username: user.username, role: user.role, }, }, }); } catch (error) { logger.error(`Login error: ${error.message}`); return this.jsonResponse({ success: false, error: 'Login failed' }, 500); } } private async handleStatusRequest(): Promise { const status = await this.oneboxRef.getSystemStatus(); return this.jsonResponse({ success: true, data: status }); } private async handleListServicesRequest(): Promise { const services = this.oneboxRef.services.listServices(); return this.jsonResponse({ success: true, data: services }); } private async handleDeployServiceRequest(req: Request): Promise { const body = await req.json(); const service = await this.oneboxRef.services.deployService(body); return this.jsonResponse({ success: true, data: service }); } private async handleGetServiceRequest(name: string): Promise { const service = this.oneboxRef.services.getService(name); if (!service) { return this.jsonResponse({ success: false, error: 'Service not found' }, 404); } return this.jsonResponse({ success: true, data: service }); } private async handleDeleteServiceRequest(name: string): Promise { await this.oneboxRef.services.removeService(name); return this.jsonResponse({ success: true, message: 'Service removed' }); } private async handleStartServiceRequest(name: string): Promise { await this.oneboxRef.services.startService(name); return this.jsonResponse({ success: true, message: 'Service started' }); } private async handleStopServiceRequest(name: string): Promise { await this.oneboxRef.services.stopService(name); return this.jsonResponse({ success: true, message: 'Service stopped' }); } private async handleRestartServiceRequest(name: string): Promise { await this.oneboxRef.services.restartService(name); return this.jsonResponse({ success: true, message: 'Service restarted' }); } private async handleGetLogsRequest(name: string): Promise { const logs = await this.oneboxRef.services.getServiceLogs(name); return this.jsonResponse({ success: true, data: logs }); } private async handleGetSettingsRequest(): Promise { const settings = this.oneboxRef.database.getAllSettings(); return this.jsonResponse({ success: true, data: settings }); } private async handleUpdateSettingsRequest(req: Request): Promise { try { const body = await req.json(); if (!body || typeof body !== 'object') { return this.jsonResponse( { success: false, error: 'Invalid request body' }, 400 ); } // Update each setting for (const [key, value] of Object.entries(body)) { if (typeof value === 'string') { this.oneboxRef.database.setSetting(key, value); logger.info(`Setting updated: ${key}`); } } return this.jsonResponse({ success: true, message: 'Settings updated successfully' }); } catch (error) { logger.error(`Failed to update settings: ${error.message}`); return this.jsonResponse({ success: false, error: 'Failed to update settings' }, 500); } } /** * Helper to create JSON response */ private jsonResponse(data: IApiResponse, status = 200): Response { return new Response(JSON.stringify(data), { status, headers: { 'Content-Type': 'application/json' }, }); } }