2025-10-28 13:05:42 +00:00
|
|
|
/**
|
|
|
|
|
* HTTP Server for Onebox
|
|
|
|
|
*
|
|
|
|
|
* Serves REST API and Angular UI
|
|
|
|
|
*/
|
|
|
|
|
|
2025-11-18 00:03:24 +00:00
|
|
|
import * as plugins from '../plugins.ts';
|
|
|
|
|
import { logger } from '../logging.ts';
|
|
|
|
|
import type { Onebox } from './onebox.ts';
|
|
|
|
|
import type { IApiResponse } from '../types.ts';
|
2025-10-28 13:05:42 +00:00
|
|
|
|
|
|
|
|
export class OneboxHttpServer {
|
|
|
|
|
private oneboxRef: Onebox;
|
|
|
|
|
private server: Deno.HttpServer | null = null;
|
|
|
|
|
private port = 3000;
|
2025-11-18 14:16:27 +00:00
|
|
|
private wsClients: Set<WebSocket> = new Set();
|
2025-10-28 13:05:42 +00:00
|
|
|
|
|
|
|
|
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 {
|
2025-11-18 14:16:27 +00:00
|
|
|
// WebSocket upgrade
|
|
|
|
|
if (path === '/api/ws' && req.headers.get('upgrade') === 'websocket') {
|
|
|
|
|
return this.handleWebSocketUpgrade(req);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 01:31:15 +00:00
|
|
|
// Docker Registry v2 API (no auth required - registry handles it)
|
|
|
|
|
if (path.startsWith('/v2/')) {
|
|
|
|
|
return await this.oneboxRef.registry.handleRequest(req);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-28 13:05:42 +00:00
|
|
|
// API routes
|
|
|
|
|
if (path.startsWith('/api/')) {
|
|
|
|
|
return await this.handleApiRequest(req, path);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-18 00:03:24 +00:00
|
|
|
// Serve Angular UI
|
|
|
|
|
return await this.serveStaticFile(path);
|
2025-10-28 13:05:42 +00:00
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Request error: ${error.message}`);
|
|
|
|
|
return this.jsonResponse({ success: false, error: error.message }, 500);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-18 00:03:24 +00:00
|
|
|
/**
|
|
|
|
|
* 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';
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-28 13:05:42 +00:00
|
|
|
/**
|
|
|
|
|
* 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
|
2025-11-18 00:03:24 +00:00
|
|
|
if (path === '/api/auth/login' && method === 'POST') {
|
|
|
|
|
return await this.handleLoginRequest(req);
|
|
|
|
|
} else if (path === '/api/status' && method === 'GET') {
|
2025-10-28 13:05:42 +00:00
|
|
|
return await this.handleStatusRequest();
|
2025-11-18 00:03:24 +00:00
|
|
|
} else if (path === '/api/settings' && method === 'GET') {
|
|
|
|
|
return await this.handleGetSettingsRequest();
|
2025-11-18 14:16:27 +00:00
|
|
|
} else if (path === '/api/settings' && (method === 'PUT' || method === 'POST')) {
|
2025-11-18 00:03:24 +00:00
|
|
|
return await this.handleUpdateSettingsRequest(req);
|
2025-10-28 13:05:42 +00:00
|
|
|
} 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);
|
2025-11-24 01:31:15 +00:00
|
|
|
} else if (path.match(/^\/api\/services\/[^/]+$/) && method === 'PUT') {
|
|
|
|
|
const name = path.split('/').pop()!;
|
|
|
|
|
return await this.handleUpdateServiceRequest(name, req);
|
2025-10-28 13:05:42 +00:00
|
|
|
} 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);
|
2025-11-18 19:34:26 +00:00
|
|
|
} else if (path === '/api/ssl/obtain' && method === 'POST') {
|
|
|
|
|
return await this.handleObtainCertificateRequest(req);
|
|
|
|
|
} else if (path === '/api/ssl/list' && method === 'GET') {
|
|
|
|
|
return await this.handleListCertificatesRequest();
|
|
|
|
|
} else if (path.match(/^\/api\/ssl\/[^/]+$/) && method === 'GET') {
|
|
|
|
|
const domain = path.split('/').pop()!;
|
|
|
|
|
return await this.handleGetCertificateRequest(domain);
|
|
|
|
|
} else if (path.match(/^\/api\/ssl\/[^/]+\/renew$/) && method === 'POST') {
|
|
|
|
|
const domain = path.split('/')[3];
|
|
|
|
|
return await this.handleRenewCertificateRequest(domain);
|
2025-11-18 19:36:08 +00:00
|
|
|
} else if (path === '/api/domains' && method === 'GET') {
|
|
|
|
|
return await this.handleGetDomainsRequest();
|
|
|
|
|
} else if (path === '/api/domains/sync' && method === 'POST') {
|
|
|
|
|
return await this.handleSyncDomainsRequest();
|
|
|
|
|
} else if (path.match(/^\/api\/domains\/[^/]+$/) && method === 'GET') {
|
|
|
|
|
const domainName = path.split('/').pop()!;
|
|
|
|
|
return await this.handleGetDomainDetailRequest(domainName);
|
2025-11-24 01:31:15 +00:00
|
|
|
} else if (path === '/api/dns' && method === 'GET') {
|
|
|
|
|
return await this.handleGetDnsRecordsRequest();
|
|
|
|
|
} else if (path === '/api/dns' && method === 'POST') {
|
|
|
|
|
return await this.handleCreateDnsRecordRequest(req);
|
|
|
|
|
} else if (path.match(/^\/api\/dns\/[^/]+$/) && method === 'DELETE') {
|
|
|
|
|
const domain = path.split('/').pop()!;
|
|
|
|
|
return await this.handleDeleteDnsRecordRequest(domain);
|
|
|
|
|
} else if (path === '/api/dns/sync' && method === 'POST') {
|
|
|
|
|
return await this.handleSyncDnsRecordsRequest();
|
|
|
|
|
} else if (path.match(/^\/api\/registry\/tags\/[^/]+$/)) {
|
|
|
|
|
const serviceName = path.split('/').pop()!;
|
|
|
|
|
return await this.handleGetRegistryTagsRequest(serviceName);
|
|
|
|
|
} else if (path.match(/^\/api\/registry\/token\/[^/]+$/)) {
|
|
|
|
|
const serviceName = path.split('/').pop()!;
|
|
|
|
|
return await this.handleGetRegistryTokenRequest(serviceName);
|
2025-10-28 13:05:42 +00:00
|
|
|
} else {
|
|
|
|
|
return this.jsonResponse({ success: false, error: 'Not found' }, 404);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// API Handlers
|
|
|
|
|
|
2025-11-18 00:03:24 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-28 13:05:42 +00:00
|
|
|
private async handleStatusRequest(): Promise<Response> {
|
2025-11-18 14:16:27 +00:00
|
|
|
try {
|
|
|
|
|
const status = await this.oneboxRef.getSystemStatus();
|
|
|
|
|
return this.jsonResponse({ success: true, data: status });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to get system status: ${error.message}`);
|
|
|
|
|
return this.jsonResponse({ success: false, error: error.message || 'Failed to get system status' }, 500);
|
|
|
|
|
}
|
2025-10-28 13:05:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleListServicesRequest(): Promise<Response> {
|
2025-11-18 14:16:27 +00:00
|
|
|
try {
|
|
|
|
|
const services = this.oneboxRef.services.listServices();
|
|
|
|
|
return this.jsonResponse({ success: true, data: services });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to list services: ${error.message}`);
|
|
|
|
|
return this.jsonResponse({ success: false, error: error.message || 'Failed to list services' }, 500);
|
|
|
|
|
}
|
2025-10-28 13:05:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleDeployServiceRequest(req: Request): Promise<Response> {
|
2025-11-18 14:16:27 +00:00
|
|
|
try {
|
|
|
|
|
const body = await req.json();
|
|
|
|
|
const service = await this.oneboxRef.services.deployService(body);
|
|
|
|
|
|
|
|
|
|
// Broadcast service created
|
|
|
|
|
this.broadcastServiceUpdate(service.name, 'created', service);
|
|
|
|
|
|
|
|
|
|
return this.jsonResponse({ success: true, data: service });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to deploy service: ${error.message}`);
|
|
|
|
|
return this.jsonResponse({ success: false, error: error.message || 'Failed to deploy service' }, 500);
|
|
|
|
|
}
|
2025-10-28 13:05:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleGetServiceRequest(name: string): Promise<Response> {
|
2025-11-18 14:16:27 +00:00
|
|
|
try {
|
|
|
|
|
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 });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to get service ${name}: ${error.message}`);
|
|
|
|
|
return this.jsonResponse({ success: false, error: error.message || 'Failed to get service' }, 500);
|
2025-10-28 13:05:42 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 01:31:15 +00:00
|
|
|
private async handleUpdateServiceRequest(name: string, req: Request): Promise<Response> {
|
|
|
|
|
try {
|
|
|
|
|
const body = await req.json();
|
|
|
|
|
const updates: {
|
|
|
|
|
image?: string;
|
|
|
|
|
registry?: string;
|
|
|
|
|
port?: number;
|
|
|
|
|
domain?: string;
|
|
|
|
|
envVars?: Record<string, string>;
|
|
|
|
|
} = {};
|
|
|
|
|
|
|
|
|
|
// Extract valid update fields
|
|
|
|
|
if (body.image !== undefined) updates.image = body.image;
|
|
|
|
|
if (body.registry !== undefined) updates.registry = body.registry;
|
|
|
|
|
if (body.port !== undefined) updates.port = body.port;
|
|
|
|
|
if (body.domain !== undefined) updates.domain = body.domain;
|
|
|
|
|
if (body.envVars !== undefined) updates.envVars = body.envVars;
|
|
|
|
|
|
|
|
|
|
const service = await this.oneboxRef.services.updateService(name, updates);
|
|
|
|
|
|
|
|
|
|
// Broadcast service updated
|
|
|
|
|
this.broadcastServiceUpdate(name, 'updated', service);
|
|
|
|
|
|
|
|
|
|
return this.jsonResponse({ success: true, data: service });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to update service ${name}: ${error.message}`);
|
|
|
|
|
return this.jsonResponse({ success: false, error: error.message || 'Failed to update service' }, 500);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-28 13:05:42 +00:00
|
|
|
private async handleDeleteServiceRequest(name: string): Promise<Response> {
|
2025-11-18 14:16:27 +00:00
|
|
|
try {
|
|
|
|
|
await this.oneboxRef.services.removeService(name);
|
|
|
|
|
|
|
|
|
|
// Broadcast service deleted
|
|
|
|
|
this.broadcastServiceUpdate(name, 'deleted');
|
|
|
|
|
|
|
|
|
|
return this.jsonResponse({ success: true, message: 'Service removed' });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to delete service ${name}: ${error.message}`);
|
|
|
|
|
return this.jsonResponse({ success: false, error: error.message || 'Failed to delete service' }, 500);
|
|
|
|
|
}
|
2025-10-28 13:05:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleStartServiceRequest(name: string): Promise<Response> {
|
2025-11-18 14:16:27 +00:00
|
|
|
try {
|
|
|
|
|
await this.oneboxRef.services.startService(name);
|
|
|
|
|
|
|
|
|
|
// Broadcast service started
|
|
|
|
|
this.broadcastServiceUpdate(name, 'started');
|
|
|
|
|
|
|
|
|
|
return this.jsonResponse({ success: true, message: 'Service started' });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to start service ${name}: ${error.message}`);
|
|
|
|
|
return this.jsonResponse({ success: false, error: error.message || 'Failed to start service' }, 500);
|
|
|
|
|
}
|
2025-10-28 13:05:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleStopServiceRequest(name: string): Promise<Response> {
|
2025-11-18 14:16:27 +00:00
|
|
|
try {
|
|
|
|
|
await this.oneboxRef.services.stopService(name);
|
|
|
|
|
|
|
|
|
|
// Broadcast service stopped
|
|
|
|
|
this.broadcastServiceUpdate(name, 'stopped');
|
|
|
|
|
|
|
|
|
|
return this.jsonResponse({ success: true, message: 'Service stopped' });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to stop service ${name}: ${error.message}`);
|
|
|
|
|
return this.jsonResponse({ success: false, error: error.message || 'Failed to stop service' }, 500);
|
|
|
|
|
}
|
2025-10-28 13:05:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleRestartServiceRequest(name: string): Promise<Response> {
|
2025-11-18 14:16:27 +00:00
|
|
|
try {
|
|
|
|
|
await this.oneboxRef.services.restartService(name);
|
|
|
|
|
|
|
|
|
|
// Broadcast service updated
|
|
|
|
|
this.broadcastServiceUpdate(name, 'updated');
|
|
|
|
|
|
|
|
|
|
return this.jsonResponse({ success: true, message: 'Service restarted' });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to restart service ${name}: ${error.message}`);
|
|
|
|
|
return this.jsonResponse({ success: false, error: error.message || 'Failed to restart service' }, 500);
|
|
|
|
|
}
|
2025-10-28 13:05:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleGetLogsRequest(name: string): Promise<Response> {
|
2025-11-18 14:16:27 +00:00
|
|
|
try {
|
|
|
|
|
const logs = await this.oneboxRef.services.getServiceLogs(name);
|
|
|
|
|
return this.jsonResponse({ success: true, data: logs });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to get logs for service ${name}: ${error.message}`);
|
|
|
|
|
return this.jsonResponse({ success: false, error: error.message || 'Failed to get logs' }, 500);
|
|
|
|
|
}
|
2025-10-28 13:05:42 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-18 00:03:24 +00:00
|
|
|
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
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-18 14:16:27 +00:00
|
|
|
// Handle three formats:
|
|
|
|
|
// 1. Single setting: { key: "settingName", value: "settingValue" }
|
|
|
|
|
// 2. Array format: [{ key: "name1", value: "val1" }, ...]
|
|
|
|
|
// 3. Object format: { settingName1: "value1", settingName2: "value2", ... }
|
|
|
|
|
|
|
|
|
|
if (Array.isArray(body)) {
|
|
|
|
|
// Array format from UI
|
|
|
|
|
for (const item of body) {
|
|
|
|
|
if (item.key && typeof item.value === 'string') {
|
|
|
|
|
this.oneboxRef.database.setSetting(item.key, item.value);
|
|
|
|
|
logger.info(`Setting updated: ${item.key} = ${item.value}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (body.key && body.value !== undefined) {
|
|
|
|
|
// Single setting format: { key: "name", value: "val" }
|
|
|
|
|
if (typeof body.value === 'string') {
|
|
|
|
|
this.oneboxRef.database.setSetting(body.key, body.value);
|
|
|
|
|
logger.info(`Setting updated: ${body.key} = ${body.value}`);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Object format: { name1: "val1", name2: "val2", ... }
|
|
|
|
|
for (const [key, value] of Object.entries(body)) {
|
|
|
|
|
if (typeof value === 'string') {
|
|
|
|
|
this.oneboxRef.database.setSetting(key, value);
|
|
|
|
|
logger.info(`Setting updated: ${key} = ${value}`);
|
|
|
|
|
}
|
2025-11-18 00:03:24 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-18 19:34:26 +00:00
|
|
|
private async handleObtainCertificateRequest(req: Request): Promise<Response> {
|
|
|
|
|
try {
|
|
|
|
|
const body = await req.json();
|
|
|
|
|
const { domain, includeWildcard } = body;
|
|
|
|
|
|
|
|
|
|
if (!domain) {
|
|
|
|
|
return this.jsonResponse(
|
|
|
|
|
{ success: false, error: 'Domain is required' },
|
|
|
|
|
400
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.oneboxRef.ssl.obtainCertificate(domain, includeWildcard || false);
|
|
|
|
|
|
|
|
|
|
return this.jsonResponse({
|
|
|
|
|
success: true,
|
|
|
|
|
message: `Certificate obtained for ${domain}`,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to obtain certificate: ${error.message}`);
|
|
|
|
|
return this.jsonResponse({ success: false, error: error.message || 'Failed to obtain certificate' }, 500);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleListCertificatesRequest(): Promise<Response> {
|
|
|
|
|
try {
|
|
|
|
|
const certificates = this.oneboxRef.ssl.listCertificates();
|
|
|
|
|
return this.jsonResponse({ success: true, data: certificates });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to list certificates: ${error.message}`);
|
|
|
|
|
return this.jsonResponse({ success: false, error: error.message || 'Failed to list certificates' }, 500);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleGetCertificateRequest(domain: string): Promise<Response> {
|
|
|
|
|
try {
|
|
|
|
|
const certificate = this.oneboxRef.ssl.getCertificate(domain);
|
|
|
|
|
if (!certificate) {
|
|
|
|
|
return this.jsonResponse({ success: false, error: 'Certificate not found' }, 404);
|
|
|
|
|
}
|
|
|
|
|
return this.jsonResponse({ success: true, data: certificate });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to get certificate for ${domain}: ${error.message}`);
|
|
|
|
|
return this.jsonResponse({ success: false, error: error.message || 'Failed to get certificate' }, 500);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleRenewCertificateRequest(domain: string): Promise<Response> {
|
|
|
|
|
try {
|
|
|
|
|
await this.oneboxRef.ssl.renewCertificate(domain);
|
|
|
|
|
return this.jsonResponse({
|
|
|
|
|
success: true,
|
|
|
|
|
message: `Certificate renewed for ${domain}`,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to renew certificate for ${domain}: ${error.message}`);
|
|
|
|
|
return this.jsonResponse({ success: false, error: error.message || 'Failed to renew certificate' }, 500);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-18 19:37:06 +00:00
|
|
|
private async handleGetDomainsRequest(): Promise<Response> {
|
|
|
|
|
try {
|
|
|
|
|
const domains = this.oneboxRef.database.getAllDomains();
|
|
|
|
|
const certManager = this.oneboxRef.certRequirementManager;
|
|
|
|
|
|
|
|
|
|
// Build domain views with certificate and service information
|
|
|
|
|
const domainViews = domains.map((domain) => {
|
|
|
|
|
const certificates = this.oneboxRef.database.getCertificatesByDomain(domain.id!);
|
|
|
|
|
const requirements = this.oneboxRef.database.getCertRequirementsByDomain(domain.id!);
|
|
|
|
|
|
|
|
|
|
// Count services using this domain
|
|
|
|
|
const allServices = this.oneboxRef.database.getAllServices();
|
|
|
|
|
const serviceCount = allServices.filter((service) => {
|
|
|
|
|
if (!service.domain) return false;
|
|
|
|
|
// Extract base domain from service domain
|
|
|
|
|
const baseDomain = service.domain.split('.').slice(-2).join('.');
|
|
|
|
|
return baseDomain === domain.domain;
|
|
|
|
|
}).length;
|
|
|
|
|
|
|
|
|
|
// Determine certificate status
|
|
|
|
|
let certificateStatus: 'valid' | 'expiring-soon' | 'expired' | 'pending' | 'none' =
|
|
|
|
|
'none';
|
|
|
|
|
let daysRemaining: number | null = null;
|
|
|
|
|
|
|
|
|
|
const validCerts = certificates.filter((cert) => cert.isValid && cert.expiryDate > Date.now());
|
|
|
|
|
if (validCerts.length > 0) {
|
|
|
|
|
// Find cert with furthest expiry
|
|
|
|
|
const latestCert = validCerts.reduce((latest, cert) =>
|
|
|
|
|
cert.expiryDate > latest.expiryDate ? cert : latest
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
daysRemaining = Math.floor((latestCert.expiryDate - Date.now()) / (24 * 60 * 60 * 1000));
|
|
|
|
|
|
|
|
|
|
if (daysRemaining <= 30) {
|
|
|
|
|
certificateStatus = 'expiring-soon';
|
|
|
|
|
} else {
|
|
|
|
|
certificateStatus = 'valid';
|
|
|
|
|
}
|
|
|
|
|
} else if (certificates.some((cert) => !cert.isValid)) {
|
|
|
|
|
certificateStatus = 'expired';
|
|
|
|
|
} else if (requirements.some((req) => req.status === 'pending')) {
|
|
|
|
|
certificateStatus = 'pending';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
domain,
|
|
|
|
|
certificates,
|
|
|
|
|
requirements,
|
|
|
|
|
serviceCount,
|
|
|
|
|
certificateStatus,
|
|
|
|
|
daysRemaining,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return this.jsonResponse({ success: true, data: domainViews });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to get domains: ${error.message}`);
|
|
|
|
|
return this.jsonResponse({
|
|
|
|
|
success: false,
|
|
|
|
|
error: error.message || 'Failed to get domains',
|
|
|
|
|
}, 500);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleSyncDomainsRequest(): Promise<Response> {
|
|
|
|
|
try {
|
|
|
|
|
if (!this.oneboxRef.cloudflareDomainSync) {
|
|
|
|
|
return this.jsonResponse({
|
|
|
|
|
success: false,
|
|
|
|
|
error: 'Cloudflare domain sync not configured',
|
|
|
|
|
}, 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.oneboxRef.cloudflareDomainSync.syncZones();
|
|
|
|
|
|
|
|
|
|
return this.jsonResponse({
|
|
|
|
|
success: true,
|
|
|
|
|
message: 'Cloudflare zones synced successfully',
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to sync Cloudflare zones: ${error.message}`);
|
|
|
|
|
return this.jsonResponse({
|
|
|
|
|
success: false,
|
|
|
|
|
error: error.message || 'Failed to sync Cloudflare zones',
|
|
|
|
|
}, 500);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleGetDomainDetailRequest(domainName: string): Promise<Response> {
|
|
|
|
|
try {
|
|
|
|
|
const domain = this.oneboxRef.database.getDomainByName(domainName);
|
|
|
|
|
|
|
|
|
|
if (!domain) {
|
|
|
|
|
return this.jsonResponse({ success: false, error: 'Domain not found' }, 404);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const certificates = this.oneboxRef.database.getCertificatesByDomain(domain.id!);
|
|
|
|
|
const requirements = this.oneboxRef.database.getCertRequirementsByDomain(domain.id!);
|
|
|
|
|
|
|
|
|
|
// Get services using this domain
|
|
|
|
|
const allServices = this.oneboxRef.database.getAllServices();
|
|
|
|
|
const services = allServices.filter((service) => {
|
|
|
|
|
if (!service.domain) return false;
|
|
|
|
|
const baseDomain = service.domain.split('.').slice(-2).join('.');
|
|
|
|
|
return baseDomain === domain.domain;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Build detailed view
|
|
|
|
|
const domainDetail = {
|
|
|
|
|
domain,
|
|
|
|
|
certificates: certificates.map((cert) => ({
|
|
|
|
|
...cert,
|
|
|
|
|
isExpired: cert.expiryDate <= Date.now(),
|
|
|
|
|
daysRemaining: Math.floor((cert.expiryDate - Date.now()) / (24 * 60 * 60 * 1000)),
|
|
|
|
|
})),
|
|
|
|
|
requirements: requirements.map((req) => {
|
|
|
|
|
const service = allServices.find((s) => s.id === req.serviceId);
|
|
|
|
|
return {
|
|
|
|
|
...req,
|
|
|
|
|
serviceName: service?.name || 'Unknown',
|
|
|
|
|
};
|
|
|
|
|
}),
|
|
|
|
|
services: services.map((s) => ({
|
|
|
|
|
id: s.id,
|
|
|
|
|
name: s.name,
|
|
|
|
|
domain: s.domain,
|
|
|
|
|
status: s.status,
|
|
|
|
|
})),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return this.jsonResponse({ success: true, data: domainDetail });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to get domain detail for ${domainName}: ${error.message}`);
|
|
|
|
|
return this.jsonResponse({
|
|
|
|
|
success: false,
|
|
|
|
|
error: error.message || 'Failed to get domain detail',
|
|
|
|
|
}, 500);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 01:31:15 +00:00
|
|
|
private async handleGetDnsRecordsRequest(): Promise<Response> {
|
|
|
|
|
try {
|
|
|
|
|
const records = this.oneboxRef.dns.listDNSRecords();
|
|
|
|
|
return this.jsonResponse({ success: true, data: records });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to get DNS records: ${error.message}`);
|
|
|
|
|
return this.jsonResponse({
|
|
|
|
|
success: false,
|
|
|
|
|
error: error.message || 'Failed to get DNS records',
|
|
|
|
|
}, 500);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleCreateDnsRecordRequest(req: Request): Promise<Response> {
|
|
|
|
|
try {
|
|
|
|
|
const body = await req.json();
|
|
|
|
|
const { domain, ip } = body;
|
|
|
|
|
|
|
|
|
|
if (!domain) {
|
|
|
|
|
return this.jsonResponse(
|
|
|
|
|
{ success: false, error: 'Domain is required' },
|
|
|
|
|
400
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.oneboxRef.dns.addDNSRecord(domain, ip);
|
|
|
|
|
|
|
|
|
|
return this.jsonResponse({
|
|
|
|
|
success: true,
|
|
|
|
|
message: `DNS record created for ${domain}`,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to create DNS record: ${error.message}`);
|
|
|
|
|
return this.jsonResponse({
|
|
|
|
|
success: false,
|
|
|
|
|
error: error.message || 'Failed to create DNS record',
|
|
|
|
|
}, 500);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleDeleteDnsRecordRequest(domain: string): Promise<Response> {
|
|
|
|
|
try {
|
|
|
|
|
await this.oneboxRef.dns.removeDNSRecord(domain);
|
|
|
|
|
|
|
|
|
|
return this.jsonResponse({
|
|
|
|
|
success: true,
|
|
|
|
|
message: `DNS record deleted for ${domain}`,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to delete DNS record for ${domain}: ${error.message}`);
|
|
|
|
|
return this.jsonResponse({
|
|
|
|
|
success: false,
|
|
|
|
|
error: error.message || 'Failed to delete DNS record',
|
|
|
|
|
}, 500);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleSyncDnsRecordsRequest(): Promise<Response> {
|
|
|
|
|
try {
|
|
|
|
|
if (!this.oneboxRef.dns.isConfigured()) {
|
|
|
|
|
return this.jsonResponse({
|
|
|
|
|
success: false,
|
|
|
|
|
error: 'DNS manager not configured',
|
|
|
|
|
}, 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.oneboxRef.dns.syncFromCloudflare();
|
|
|
|
|
|
|
|
|
|
return this.jsonResponse({
|
|
|
|
|
success: true,
|
|
|
|
|
message: 'DNS records synced from Cloudflare',
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to sync DNS records: ${error.message}`);
|
|
|
|
|
return this.jsonResponse({
|
|
|
|
|
success: false,
|
|
|
|
|
error: error.message || 'Failed to sync DNS records',
|
|
|
|
|
}, 500);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-18 14:16:27 +00:00
|
|
|
/**
|
|
|
|
|
* Handle WebSocket upgrade
|
|
|
|
|
*/
|
|
|
|
|
private handleWebSocketUpgrade(req: Request): Response {
|
|
|
|
|
const { socket, response } = Deno.upgradeWebSocket(req);
|
|
|
|
|
|
|
|
|
|
socket.onopen = () => {
|
|
|
|
|
this.wsClients.add(socket);
|
|
|
|
|
logger.info(`WebSocket client connected (${this.wsClients.size} total)`);
|
|
|
|
|
|
|
|
|
|
// Send initial connection message
|
|
|
|
|
socket.send(JSON.stringify({
|
|
|
|
|
type: 'connected',
|
|
|
|
|
message: 'Connected to Onebox server',
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
}));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
socket.onclose = () => {
|
|
|
|
|
this.wsClients.delete(socket);
|
|
|
|
|
logger.info(`WebSocket client disconnected (${this.wsClients.size} remaining)`);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
socket.onerror = (error) => {
|
|
|
|
|
logger.error(`WebSocket error: ${error}`);
|
|
|
|
|
this.wsClients.delete(socket);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return response;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Broadcast message to all connected WebSocket clients
|
|
|
|
|
*/
|
|
|
|
|
broadcast(message: Record<string, any>): void {
|
|
|
|
|
const data = JSON.stringify(message);
|
|
|
|
|
let successCount = 0;
|
|
|
|
|
let failCount = 0;
|
|
|
|
|
|
|
|
|
|
for (const client of this.wsClients) {
|
|
|
|
|
try {
|
|
|
|
|
if (client.readyState === WebSocket.OPEN) {
|
|
|
|
|
client.send(data);
|
|
|
|
|
successCount++;
|
|
|
|
|
} else {
|
|
|
|
|
this.wsClients.delete(client);
|
|
|
|
|
failCount++;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to send to WebSocket client: ${error.message}`);
|
|
|
|
|
this.wsClients.delete(client);
|
|
|
|
|
failCount++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (successCount > 0) {
|
|
|
|
|
logger.debug(`Broadcast to ${successCount} clients (${failCount} failed)`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Broadcast service update
|
|
|
|
|
*/
|
|
|
|
|
broadcastServiceUpdate(serviceName: string, action: 'created' | 'updated' | 'deleted' | 'started' | 'stopped', data?: any): void {
|
|
|
|
|
this.broadcast({
|
|
|
|
|
type: 'service_update',
|
|
|
|
|
action,
|
|
|
|
|
serviceName,
|
|
|
|
|
data,
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Broadcast service status update
|
|
|
|
|
*/
|
|
|
|
|
broadcastServiceStatus(serviceName: string, status: string): void {
|
|
|
|
|
this.broadcast({
|
|
|
|
|
type: 'service_status',
|
|
|
|
|
serviceName,
|
|
|
|
|
status,
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Broadcast system status update
|
|
|
|
|
*/
|
|
|
|
|
broadcastSystemStatus(status: any): void {
|
|
|
|
|
this.broadcast({
|
|
|
|
|
type: 'system_status',
|
|
|
|
|
data: status,
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 01:31:15 +00:00
|
|
|
// ============ Registry Endpoints ============
|
|
|
|
|
|
|
|
|
|
private async handleGetRegistryTagsRequest(serviceName: string): Promise<Response> {
|
|
|
|
|
try {
|
|
|
|
|
const tags = await this.oneboxRef.registry.getImageTags(serviceName);
|
|
|
|
|
return this.jsonResponse({ success: true, data: tags });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to get registry tags for ${serviceName}: ${error.message}`);
|
|
|
|
|
return this.jsonResponse({
|
|
|
|
|
success: false,
|
|
|
|
|
error: error.message || 'Failed to get registry tags',
|
|
|
|
|
}, 500);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleGetRegistryTokenRequest(serviceName: string): Promise<Response> {
|
|
|
|
|
try {
|
|
|
|
|
// Get the service to verify it exists
|
|
|
|
|
const service = this.oneboxRef.database.getServiceByName(serviceName);
|
|
|
|
|
if (!service) {
|
|
|
|
|
return this.jsonResponse({
|
|
|
|
|
success: false,
|
|
|
|
|
error: 'Service not found',
|
|
|
|
|
}, 404);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If service already has a token, return it
|
|
|
|
|
if (service.registryToken) {
|
|
|
|
|
return this.jsonResponse({
|
|
|
|
|
success: true,
|
|
|
|
|
data: {
|
|
|
|
|
token: service.registryToken,
|
|
|
|
|
repository: serviceName,
|
|
|
|
|
baseUrl: this.oneboxRef.registry.getBaseUrl(),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate new token
|
|
|
|
|
const token = await this.oneboxRef.registry.createServiceToken(serviceName);
|
|
|
|
|
|
|
|
|
|
// Save token to database
|
|
|
|
|
this.oneboxRef.database.updateService(service.id!, {
|
|
|
|
|
registryToken: token,
|
|
|
|
|
registryRepository: serviceName,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return this.jsonResponse({
|
|
|
|
|
success: true,
|
|
|
|
|
data: {
|
|
|
|
|
token: token,
|
|
|
|
|
repository: serviceName,
|
|
|
|
|
baseUrl: this.oneboxRef.registry.getBaseUrl(),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to get registry token for ${serviceName}: ${error.message}`);
|
|
|
|
|
return this.jsonResponse({
|
|
|
|
|
success: false,
|
|
|
|
|
error: error.message || 'Failed to get registry token',
|
|
|
|
|
}, 500);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-28 13:05:42 +00:00
|
|
|
/**
|
|
|
|
|
* Helper to create JSON response
|
|
|
|
|
*/
|
|
|
|
|
private jsonResponse(data: IApiResponse, status = 200): Response {
|
|
|
|
|
return new Response(JSON.stringify(data), {
|
|
|
|
|
status,
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|