update
This commit is contained in:
362
ts/classes/httpserver.ts
Normal file
362
ts/classes/httpserver.ts
Normal file
@@ -0,0 +1,362 @@
|
||||
/**
|
||||
* 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<void> {
|
||||
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<void> {
|
||||
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<Response> {
|
||||
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<Response> {
|
||||
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<string, string> = {
|
||||
'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<Response> {
|
||||
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<Response> {
|
||||
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<Response> {
|
||||
const status = await this.oneboxRef.getSystemStatus();
|
||||
return this.jsonResponse({ success: true, data: status });
|
||||
}
|
||||
|
||||
private async handleListServicesRequest(): Promise<Response> {
|
||||
const services = this.oneboxRef.services.listServices();
|
||||
return this.jsonResponse({ success: true, data: services });
|
||||
}
|
||||
|
||||
private async handleDeployServiceRequest(req: Request): Promise<Response> {
|
||||
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<Response> {
|
||||
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<Response> {
|
||||
await this.oneboxRef.services.removeService(name);
|
||||
return this.jsonResponse({ success: true, message: 'Service removed' });
|
||||
}
|
||||
|
||||
private async handleStartServiceRequest(name: string): Promise<Response> {
|
||||
await this.oneboxRef.services.startService(name);
|
||||
return this.jsonResponse({ success: true, message: 'Service started' });
|
||||
}
|
||||
|
||||
private async handleStopServiceRequest(name: string): Promise<Response> {
|
||||
await this.oneboxRef.services.stopService(name);
|
||||
return this.jsonResponse({ success: true, message: 'Service stopped' });
|
||||
}
|
||||
|
||||
private async handleRestartServiceRequest(name: string): Promise<Response> {
|
||||
await this.oneboxRef.services.restartService(name);
|
||||
return this.jsonResponse({ success: true, message: 'Service restarted' });
|
||||
}
|
||||
|
||||
private async handleGetLogsRequest(name: string): Promise<Response> {
|
||||
const logs = await this.oneboxRef.services.getServiceLogs(name);
|
||||
return this.jsonResponse({ success: true, data: logs });
|
||||
}
|
||||
|
||||
private async handleGetSettingsRequest(): Promise<Response> {
|
||||
const settings = this.oneboxRef.database.getAllSettings();
|
||||
return this.jsonResponse({ success: true, data: settings });
|
||||
}
|
||||
|
||||
private async handleUpdateSettingsRequest(req: Request): Promise<Response> {
|
||||
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' },
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user