Files
onebox/ts/classes/httpserver.ts
2025-11-26 12:16:50 +00:00

1732 lines
59 KiB
TypeScript

/**
* HTTP Server for Onebox
*
* Serves REST API and Angular UI
*/
import * as plugins from '../plugins.ts';
import { logger } from '../logging.ts';
import { getErrorMessage } from '../utils/error.ts';
import type { Onebox } from './onebox.ts';
import type { IApiResponse, ICreateRegistryTokenRequest, IRegistryTokenView, TPlatformServiceType } from '../types.ts';
export class OneboxHttpServer {
private oneboxRef: Onebox;
private server: Deno.HttpServer | null = null;
private port = 3000;
private wsClients: Set<WebSocket> = new Set();
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: ${getErrorMessage(error)}`);
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: ${getErrorMessage(error)}`);
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 {
// WebSocket upgrade
if (path === '/api/ws' && req.headers.get('upgrade') === 'websocket') {
return this.handleWebSocketUpgrade(req);
}
// Log streaming WebSocket
if (path.startsWith('/api/services/') && path.endsWith('/logs/stream') && req.headers.get('upgrade') === 'websocket') {
const serviceName = path.split('/')[3];
return this.handleLogStreamUpgrade(req, serviceName);
}
// Network access logs WebSocket
if (path === '/api/network/logs/stream' && req.headers.get('upgrade') === 'websocket') {
return this.handleNetworkLogStreamUpgrade(req, new URL(req.url));
}
// Docker Registry v2 Token endpoint (for OCI authentication)
if (path === '/v2/token') {
return await this.handleRegistryTokenRequest(req, url);
}
// Docker Registry v2 API (no auth required - registry handles it)
if (path.startsWith('/v2/')) {
return await this.oneboxRef.registry.handleRequest(req);
}
// 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: ${getErrorMessage(error)}`);
return this.jsonResponse({ success: false, error: getErrorMessage(error) }, 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/ui/browser${filePath}`;
// Read file
const file = await Deno.readFile(fullPath);
// Determine content type
const contentType = this.getContentType(filePath);
// Prevent stale bundles in dev (no hashed filenames) while allowing long-lived caching for hashed prod assets
const isHashedAsset = /\.[a-f0-9]{8,}\./i.test(filePath);
const cacheControl =
filePath === '/index.html' || !isHashedAsset
? 'no-cache'
: 'public, max-age=31536000, immutable';
return new Response(file, {
headers: {
'Content-Type': contentType,
'Cache-Control': cacheControl,
},
});
} 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/ui/browser/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' || method === 'POST')) {
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 === 'PUT') {
const name = path.split('/').pop()!;
return await this.handleUpdateServiceRequest(name, req);
} 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 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);
} 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);
} 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 === '/api/registry/tokens' && method === 'GET') {
return await this.handleListRegistryTokensRequest(req);
} else if (path === '/api/registry/tokens' && method === 'POST') {
return await this.handleCreateRegistryTokenRequest(req);
} else if (path.match(/^\/api\/registry\/tokens\/\d+$/) && method === 'DELETE') {
const tokenId = Number(path.split('/').pop());
return await this.handleDeleteRegistryTokenRequest(tokenId);
// Platform Services endpoints
} else if (path === '/api/platform-services' && method === 'GET') {
return await this.handleListPlatformServicesRequest();
} else if (path.match(/^\/api\/platform-services\/(mongodb|minio|redis|postgresql|rabbitmq)$/) && method === 'GET') {
const type = path.split('/').pop()! as TPlatformServiceType;
return await this.handleGetPlatformServiceRequest(type);
} else if (path.match(/^\/api\/platform-services\/(mongodb|minio|redis|postgresql|rabbitmq)\/start$/) && method === 'POST') {
const type = path.split('/')[3] as TPlatformServiceType;
return await this.handleStartPlatformServiceRequest(type);
} else if (path.match(/^\/api\/platform-services\/(mongodb|minio|redis|postgresql|rabbitmq)\/stop$/) && method === 'POST') {
const type = path.split('/')[3] as TPlatformServiceType;
return await this.handleStopPlatformServiceRequest(type);
} else if (path.match(/^\/api\/services\/[^/]+\/platform-resources$/) && method === 'GET') {
const serviceName = path.split('/')[3];
return await this.handleGetServicePlatformResourcesRequest(serviceName);
// Network endpoints
} else if (path === '/api/network/targets' && method === 'GET') {
return await this.handleGetNetworkTargetsRequest();
} else if (path === '/api/network/stats' && method === 'GET') {
return await this.handleGetNetworkStatsRequest();
} 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: ${getErrorMessage(error)}`);
return this.jsonResponse({ success: false, error: 'Login failed' }, 500);
}
}
private async handleStatusRequest(): Promise<Response> {
try {
const status = await this.oneboxRef.getSystemStatus();
return this.jsonResponse({ success: true, data: status });
} catch (error) {
logger.error(`Failed to get system status: ${getErrorMessage(error)}`);
return this.jsonResponse({ success: false, error: getErrorMessage(error) || 'Failed to get system status' }, 500);
}
}
private async handleListServicesRequest(): Promise<Response> {
try {
const services = this.oneboxRef.services.listServices();
return this.jsonResponse({ success: true, data: services });
} catch (error) {
logger.error(`Failed to list services: ${getErrorMessage(error)}`);
return this.jsonResponse({ success: false, error: getErrorMessage(error) || 'Failed to list services' }, 500);
}
}
private async handleDeployServiceRequest(req: Request): Promise<Response> {
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: ${getErrorMessage(error)}`);
return this.jsonResponse({ success: false, error: getErrorMessage(error) || 'Failed to deploy service' }, 500);
}
}
private async handleGetServiceRequest(name: string): Promise<Response> {
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}: ${getErrorMessage(error)}`);
return this.jsonResponse({ success: false, error: getErrorMessage(error) || 'Failed to get service' }, 500);
}
}
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}: ${getErrorMessage(error)}`);
return this.jsonResponse({ success: false, error: getErrorMessage(error) || 'Failed to update service' }, 500);
}
}
private async handleDeleteServiceRequest(name: string): Promise<Response> {
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}: ${getErrorMessage(error)}`);
return this.jsonResponse({ success: false, error: getErrorMessage(error) || 'Failed to delete service' }, 500);
}
}
private async handleStartServiceRequest(name: string): Promise<Response> {
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}: ${getErrorMessage(error)}`);
return this.jsonResponse({ success: false, error: getErrorMessage(error) || 'Failed to start service' }, 500);
}
}
private async handleStopServiceRequest(name: string): Promise<Response> {
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}: ${getErrorMessage(error)}`);
return this.jsonResponse({ success: false, error: getErrorMessage(error) || 'Failed to stop service' }, 500);
}
}
private async handleRestartServiceRequest(name: string): Promise<Response> {
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}: ${getErrorMessage(error)}`);
return this.jsonResponse({ success: false, error: getErrorMessage(error) || 'Failed to restart service' }, 500);
}
}
private async handleGetLogsRequest(name: string): Promise<Response> {
try {
const logs = await this.oneboxRef.services.getServiceLogs(name);
logger.debug(`handleGetLogsRequest: logs type = ${typeof logs}, constructor = ${logs?.constructor?.name}`);
logger.debug(`handleGetLogsRequest: logs value = ${String(logs).slice(0, 100)}`);
return this.jsonResponse({ success: true, data: logs });
} catch (error) {
logger.error(`Failed to get logs for service ${name}: ${getErrorMessage(error)}`);
return this.jsonResponse({ success: false, error: getErrorMessage(error) || 'Failed to get logs' }, 500);
}
}
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
);
}
// 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}`);
}
}
}
return this.jsonResponse({
success: true,
message: 'Settings updated successfully'
});
} catch (error) {
logger.error(`Failed to update settings: ${getErrorMessage(error)}`);
return this.jsonResponse({ success: false, error: 'Failed to update settings' }, 500);
}
}
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: ${getErrorMessage(error)}`);
return this.jsonResponse({ success: false, error: getErrorMessage(error) || '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: ${getErrorMessage(error)}`);
return this.jsonResponse({ success: false, error: getErrorMessage(error) || '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}: ${getErrorMessage(error)}`);
return this.jsonResponse({ success: false, error: getErrorMessage(error) || '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}: ${getErrorMessage(error)}`);
return this.jsonResponse({ success: false, error: getErrorMessage(error) || 'Failed to renew certificate' }, 500);
}
}
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: ${getErrorMessage(error)}`);
return this.jsonResponse({
success: false,
error: getErrorMessage(error) || '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: ${getErrorMessage(error)}`);
return this.jsonResponse({
success: false,
error: getErrorMessage(error) || '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}: ${getErrorMessage(error)}`);
return this.jsonResponse({
success: false,
error: getErrorMessage(error) || 'Failed to get domain detail',
}, 500);
}
}
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: ${getErrorMessage(error)}`);
return this.jsonResponse({
success: false,
error: getErrorMessage(error) || '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: ${getErrorMessage(error)}`);
return this.jsonResponse({
success: false,
error: getErrorMessage(error) || '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}: ${getErrorMessage(error)}`);
return this.jsonResponse({
success: false,
error: getErrorMessage(error) || '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: ${getErrorMessage(error)}`);
return this.jsonResponse({
success: false,
error: getErrorMessage(error) || 'Failed to sync DNS records',
}, 500);
}
}
/**
* 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;
}
/**
* Handle WebSocket upgrade for log streaming
*/
private handleLogStreamUpgrade(req: Request, serviceName: string): Response {
const { socket, response } = Deno.upgradeWebSocket(req);
socket.onopen = async () => {
logger.info(`Log stream WebSocket connected for service: ${serviceName}`);
try {
// Get the service from database
const service = this.oneboxRef.database.getServiceByName(serviceName);
if (!service) {
socket.send(JSON.stringify({ error: 'Service not found' }));
socket.close();
return;
}
// Get the container (handle both direct container IDs and service IDs)
logger.info(`Looking up container for service ${serviceName}, containerID: ${service.containerID}`);
let container = await this.oneboxRef.docker.getContainerById(service.containerID!);
logger.info(`Direct lookup result: ${container ? 'found' : 'null'}`);
// If not found, it might be a service ID - try to get the actual container ID
if (!container) {
logger.info('Listing all containers to find matching service...');
const containers = await this.oneboxRef.docker.listAllContainers();
logger.info(`Found ${containers.length} containers`);
const serviceContainer = containers.find((c: any) => {
const labels = c.Labels || {};
return labels['com.docker.swarm.service.id'] === service.containerID;
});
if (serviceContainer) {
logger.info(`Found matching container: ${serviceContainer.Id}`);
container = await this.oneboxRef.docker.getContainerById(serviceContainer.Id);
logger.info(`Second lookup result: ${container ? 'found' : 'null'}`);
} else {
logger.error(`No container found with service label matching ${service.containerID}`);
}
}
if (!container) {
logger.error(`Container not found for service ${serviceName}, containerID: ${service.containerID}`);
socket.send(JSON.stringify({ error: 'Container not found' }));
socket.close();
return;
}
// Start streaming logs
const logStream = await container.streamLogs({
stdout: true,
stderr: true,
timestamps: true,
tail: 100, // Start with last 100 lines
});
// Send initial connection message
socket.send(JSON.stringify({
type: 'connected',
serviceName: service.name,
}));
// Demultiplex and pipe log data to WebSocket
// Docker streams use 8-byte headers: [STREAM_TYPE, 0, 0, 0, SIZE_BYTE1, SIZE_BYTE2, SIZE_BYTE3, SIZE_BYTE4]
let buffer = new Uint8Array(0);
logStream.on('data', (chunk: Uint8Array) => {
if (socket.readyState !== WebSocket.OPEN) return;
// Append new data to buffer
const newBuffer = new Uint8Array(buffer.length + chunk.length);
newBuffer.set(buffer);
newBuffer.set(chunk, buffer.length);
buffer = newBuffer;
// Process complete frames
while (buffer.length >= 8) {
// Read frame size from header (bytes 4-7, big-endian)
const frameSize = (buffer[4] << 24) | (buffer[5] << 16) | (buffer[6] << 8) | buffer[7];
// Check if we have the complete frame
if (buffer.length < 8 + frameSize) {
break; // Wait for more data
}
// Extract the frame data (skip 8-byte header)
const frameData = buffer.slice(8, 8 + frameSize);
// Send the clean log line
socket.send(new TextDecoder().decode(frameData));
// Remove processed frame from buffer
buffer = buffer.slice(8 + frameSize);
}
});
logStream.on('error', (error: Error) => {
logger.error(`Log stream error for ${serviceName}: ${getErrorMessage(error)}`);
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ error: getErrorMessage(error) }));
}
});
logStream.on('end', () => {
logger.info(`Log stream ended for ${serviceName}`);
socket.close();
});
// Clean up on close
socket.onclose = () => {
logger.info(`Log stream WebSocket closed for ${serviceName}`);
logStream.destroy();
};
} catch (error) {
logger.error(`Failed to start log stream for ${serviceName}: ${getErrorMessage(error)}`);
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ error: getErrorMessage(error) }));
socket.close();
}
}
};
socket.onerror = (error) => {
logger.error(`Log stream WebSocket error: ${error}`);
};
return response;
}
/**
* Handle WebSocket upgrade for network access log streaming
*/
private handleNetworkLogStreamUpgrade(req: Request, url: URL): Response {
const { socket, response } = Deno.upgradeWebSocket(req);
// Extract filter from query params
const filterDomain = url.searchParams.get('domain');
// Generate unique client ID
const clientId = crypto.randomUUID();
socket.onopen = () => {
logger.info(`Network log stream WebSocket connected (client: ${clientId})`);
// Register with CaddyLogReceiver
const filter = filterDomain ? { domain: filterDomain } : {};
this.oneboxRef.caddyLogReceiver.addClient(clientId, socket, filter);
// Send initial connection message
socket.send(JSON.stringify({
type: 'connected',
clientId,
filter,
}));
};
socket.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
// Handle filter updates from client
if (message.type === 'set_filter') {
const newFilter = {
domain: message.domain || undefined,
sampleRate: message.sampleRate || undefined,
};
this.oneboxRef.caddyLogReceiver.updateClientFilter(clientId, newFilter);
socket.send(JSON.stringify({
type: 'filter_updated',
filter: newFilter,
}));
}
} catch (error) {
logger.debug(`Network log stream message parse error: ${getErrorMessage(error)}`);
}
};
socket.onclose = () => {
logger.info(`Network log stream WebSocket closed (client: ${clientId})`);
this.oneboxRef.caddyLogReceiver.removeClient(clientId);
};
socket.onerror = (error) => {
logger.error(`Network log stream WebSocket error: ${error}`);
this.oneboxRef.caddyLogReceiver.removeClient(clientId);
};
return response;
}
// ============ Network Endpoints ============
/**
* Get all traffic targets (services, registry, platform services)
*/
private async handleGetNetworkTargetsRequest(): Promise<Response> {
try {
const targets: Array<{
type: 'service' | 'registry' | 'platform';
name: string;
domain: string | null;
targetHost: string;
targetPort: number;
status: string;
}> = [];
// Add services
const services = this.oneboxRef.services.listServices();
for (const service of services) {
targets.push({
type: 'service',
name: service.name,
domain: service.domain || null,
targetHost: service.containerIP || 'unknown',
targetPort: service.port || 80,
status: service.status,
});
}
// Add registry if running
const registryStatus = this.oneboxRef.registry.getStatus();
if (registryStatus.running) {
targets.push({
type: 'registry',
name: 'onebox-registry',
domain: null, // Registry is internal
targetHost: 'localhost',
targetPort: registryStatus.port,
status: 'running',
});
}
// Add platform services
const platformServices = this.oneboxRef.platformServices.getAllPlatformServices();
for (const ps of platformServices) {
// Get provider info for display name
const provider = this.oneboxRef.platformServices.getProvider(ps.type);
targets.push({
type: 'platform',
name: provider?.displayName || ps.type,
domain: null, // Platform services are internal
targetHost: 'localhost',
targetPort: this.getPlatformServicePort(ps.type),
status: ps.status,
});
}
return this.jsonResponse({ success: true, data: targets });
} catch (error) {
logger.error(`Failed to get network targets: ${getErrorMessage(error)}`);
return this.jsonResponse({
success: false,
error: getErrorMessage(error) || 'Failed to get network targets',
}, 500);
}
}
/**
* Get default port for a platform service type
*/
private getPlatformServicePort(type: TPlatformServiceType): number {
const ports: Record<TPlatformServiceType, number> = {
mongodb: 27017,
minio: 9000,
redis: 6379,
postgresql: 5432,
rabbitmq: 5672,
};
return ports[type] || 0;
}
/**
* Get Caddy/network stats
*/
private async handleGetNetworkStatsRequest(): Promise<Response> {
try {
const proxyStatus = this.oneboxRef.reverseProxy.getStatus();
const logReceiverStats = this.oneboxRef.caddyLogReceiver.getStats();
return this.jsonResponse({
success: true,
data: {
proxy: {
running: proxyStatus.running,
httpPort: proxyStatus.httpPort,
httpsPort: proxyStatus.httpsPort,
routes: proxyStatus.routes,
certificates: proxyStatus.certificates,
},
logReceiver: {
running: logReceiverStats.running,
port: logReceiverStats.port,
clients: logReceiverStats.clients,
connections: logReceiverStats.connections,
sampleRate: logReceiverStats.sampleRate,
recentLogsCount: logReceiverStats.recentLogsCount,
},
},
});
} catch (error) {
logger.error(`Failed to get network stats: ${getErrorMessage(error)}`);
return this.jsonResponse({
success: false,
error: getErrorMessage(error) || 'Failed to get network stats',
}, 500);
}
}
/**
* 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: ${getErrorMessage(error)}`);
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(),
});
}
// ============ Platform Services Endpoints ============
private async handleListPlatformServicesRequest(): Promise<Response> {
try {
const platformServices = this.oneboxRef.platformServices.getAllPlatformServices();
const providers = this.oneboxRef.platformServices.getAllProviders();
// Build response with provider info
const result = providers.map((provider) => {
const service = platformServices.find((s) => s.type === provider.type);
return {
type: provider.type,
displayName: provider.displayName,
resourceTypes: provider.resourceTypes,
status: service?.status || 'not-deployed',
containerId: service?.containerId,
createdAt: service?.createdAt,
updatedAt: service?.updatedAt,
};
});
return this.jsonResponse({ success: true, data: result });
} catch (error) {
logger.error(`Failed to list platform services: ${getErrorMessage(error)}`);
return this.jsonResponse({
success: false,
error: getErrorMessage(error) || 'Failed to list platform services',
}, 500);
}
}
private async handleGetPlatformServiceRequest(type: TPlatformServiceType): Promise<Response> {
try {
const provider = this.oneboxRef.platformServices.getProvider(type);
if (!provider) {
return this.jsonResponse({
success: false,
error: `Unknown platform service type: ${type}`,
}, 404);
}
const service = this.oneboxRef.database.getPlatformServiceByType(type);
// Get resource count
const allResources = service?.id
? this.oneboxRef.database.getPlatformResourcesByPlatformService(service.id)
: [];
return this.jsonResponse({
success: true,
data: {
type: provider.type,
displayName: provider.displayName,
resourceTypes: provider.resourceTypes,
status: service?.status || 'not-deployed',
containerId: service?.containerId,
config: provider.getDefaultConfig(),
resourceCount: allResources.length,
createdAt: service?.createdAt,
updatedAt: service?.updatedAt,
},
});
} catch (error) {
logger.error(`Failed to get platform service ${type}: ${getErrorMessage(error)}`);
return this.jsonResponse({
success: false,
error: getErrorMessage(error) || 'Failed to get platform service',
}, 500);
}
}
private async handleStartPlatformServiceRequest(type: TPlatformServiceType): Promise<Response> {
try {
const provider = this.oneboxRef.platformServices.getProvider(type);
if (!provider) {
return this.jsonResponse({
success: false,
error: `Unknown platform service type: ${type}`,
}, 404);
}
logger.info(`Starting platform service: ${type}`);
const service = await this.oneboxRef.platformServices.ensureRunning(type);
return this.jsonResponse({
success: true,
message: `Platform service ${provider.displayName} started`,
data: {
type: service.type,
status: service.status,
containerId: service.containerId,
},
});
} catch (error) {
logger.error(`Failed to start platform service ${type}: ${getErrorMessage(error)}`);
return this.jsonResponse({
success: false,
error: getErrorMessage(error) || 'Failed to start platform service',
}, 500);
}
}
private async handleStopPlatformServiceRequest(type: TPlatformServiceType): Promise<Response> {
try {
const provider = this.oneboxRef.platformServices.getProvider(type);
if (!provider) {
return this.jsonResponse({
success: false,
error: `Unknown platform service type: ${type}`,
}, 404);
}
logger.info(`Stopping platform service: ${type}`);
await this.oneboxRef.platformServices.stopPlatformService(type);
return this.jsonResponse({
success: true,
message: `Platform service ${provider.displayName} stopped`,
});
} catch (error) {
logger.error(`Failed to stop platform service ${type}: ${getErrorMessage(error)}`);
return this.jsonResponse({
success: false,
error: getErrorMessage(error) || 'Failed to stop platform service',
}, 500);
}
}
private async handleGetServicePlatformResourcesRequest(serviceName: string): Promise<Response> {
try {
const service = this.oneboxRef.services.getService(serviceName);
if (!service) {
return this.jsonResponse({
success: false,
error: 'Service not found',
}, 404);
}
const resources = await this.oneboxRef.services.getServicePlatformResources(serviceName);
// Format resources for API response (mask sensitive credentials)
const formattedResources = resources.map((r) => ({
id: r.resource.id,
resourceType: r.resource.resourceType,
resourceName: r.resource.resourceName,
platformService: {
type: r.platformService.type,
name: r.platformService.name,
status: r.platformService.status,
},
// Include env var mappings (show keys, not values)
envVars: Object.keys(r.credentials).reduce((acc, key) => {
// Mask sensitive values
const value = r.credentials[key];
if (key.toLowerCase().includes('password') || key.toLowerCase().includes('secret')) {
acc[key] = '********';
} else {
acc[key] = value;
}
return acc;
}, {} as Record<string, string>),
createdAt: r.resource.createdAt,
}));
return this.jsonResponse({
success: true,
data: formattedResources,
});
} catch (error) {
logger.error(`Failed to get platform resources for service ${serviceName}: ${getErrorMessage(error)}`);
return this.jsonResponse({
success: false,
error: getErrorMessage(error) || 'Failed to get platform resources',
}, 500);
}
}
// ============ Registry Endpoints ============
/**
* Handle Docker registry token request (OCI token authentication)
* Docker calls this endpoint to get a bearer token for registry operations
*
* Query params:
* - service: The registry service name
* - scope: Permission scope (e.g., "repository:hello-world:push,pull")
* - account: Optional account name (for basic auth)
*/
private async handleRegistryTokenRequest(req: Request, url: URL): Promise<Response> {
try {
const service = url.searchParams.get('service') || 'onebox-registry';
const scope = url.searchParams.get('scope');
const account = url.searchParams.get('account') || 'anonymous';
logger.info(`Registry token request: service=${service}, scope=${scope}, account=${account}`);
// Parse scope to extract repository and actions
// Format: repository:name:action1,action2 (e.g., "repository:hello-world:push,pull")
let scopes: string[] = [];
if (scope) {
const scopeParts = scope.split(':');
if (scopeParts.length >= 3 && scopeParts[0] === 'repository') {
const repository = scopeParts[1];
// For now, grant both push and pull for any repository request
// This allows anonymous push to the local registry
// TODO: Add authentication and authorization to restrict access
scopes = [
`oci:repository:${repository}:push`,
`oci:repository:${repository}:pull`,
];
}
}
// If no scope specified, grant basic access
if (scopes.length === 0) {
scopes = ['oci:repository:*:pull'];
}
logger.info(`Creating OCI token with scopes: ${scopes.join(', ')}`);
// Use the registry's auth manager to create a token
// smartregistry v2.0.0 returns proper JWT format (header.payload.signature)
const authManager = this.oneboxRef.registry.getAuthManager();
const token = await authManager.createOciToken(account, scopes, 3600);
logger.info(`Token created (JWT length: ${token.length})`);
// Return in Docker-expected format
return new Response(JSON.stringify({
token,
access_token: token,
expires_in: 3600,
issued_at: new Date().toISOString(),
}), {
status: 200,
headers: {
'Content-Type': 'application/json',
},
});
} catch (error) {
logger.error(`Registry token error: ${getErrorMessage(error)}`);
return new Response(JSON.stringify({
error: 'token_error',
error_description: getErrorMessage(error),
}), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
}
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}: ${getErrorMessage(error)}`);
return this.jsonResponse({
success: false,
error: getErrorMessage(error) || 'Failed to get registry tags',
}, 500);
}
}
// ============ Registry Token Management Endpoints ============
private async handleListRegistryTokensRequest(req: Request): Promise<Response> {
try {
const tokens = this.oneboxRef.database.getAllRegistryTokens();
// Convert to view format (mask token hash, add computed fields)
const tokenViews: IRegistryTokenView[] = tokens.map(token => {
const now = Date.now();
const isExpired = token.expiresAt !== null && token.expiresAt < now;
// Generate scope display string
let scopeDisplay: string;
if (token.scope === 'all') {
scopeDisplay = 'All services';
} else if (Array.isArray(token.scope)) {
scopeDisplay = token.scope.length === 1
? token.scope[0]
: `${token.scope.length} services`;
} else {
scopeDisplay = 'Unknown';
}
return {
id: token.id!,
name: token.name,
type: token.type,
scope: token.scope,
scopeDisplay,
expiresAt: token.expiresAt,
createdAt: token.createdAt,
lastUsedAt: token.lastUsedAt,
createdBy: token.createdBy,
isExpired,
};
});
return this.jsonResponse({ success: true, data: tokenViews });
} catch (error) {
logger.error(`Failed to list registry tokens: ${getErrorMessage(error)}`);
return this.jsonResponse({
success: false,
error: getErrorMessage(error) || 'Failed to list registry tokens',
}, 500);
}
}
private async handleCreateRegistryTokenRequest(req: Request): Promise<Response> {
try {
const body = await req.json() as ICreateRegistryTokenRequest;
// Validate request
if (!body.name || !body.type || !body.scope || !body.expiresIn) {
return this.jsonResponse({
success: false,
error: 'Missing required fields: name, type, scope, expiresIn',
}, 400);
}
if (body.type !== 'global' && body.type !== 'ci') {
return this.jsonResponse({
success: false,
error: 'Invalid token type. Must be "global" or "ci"',
}, 400);
}
// Validate scope
if (body.scope !== 'all' && !Array.isArray(body.scope)) {
return this.jsonResponse({
success: false,
error: 'Scope must be "all" or an array of service names',
}, 400);
}
// If scope is array of services, validate they exist
if (Array.isArray(body.scope)) {
for (const serviceName of body.scope) {
const service = this.oneboxRef.database.getServiceByName(serviceName);
if (!service) {
return this.jsonResponse({
success: false,
error: `Service not found: ${serviceName}`,
}, 400);
}
}
}
// Calculate expiration timestamp
const now = Date.now();
let expiresAt: number | null = null;
if (body.expiresIn !== 'never') {
const daysMap: Record<string, number> = {
'30d': 30,
'90d': 90,
'365d': 365,
};
const days = daysMap[body.expiresIn];
if (days) {
expiresAt = now + (days * 24 * 60 * 60 * 1000);
}
}
// Generate token (random 32 bytes as hex)
const plainToken = crypto.randomUUID() + crypto.randomUUID();
// Hash the token for storage (using simple hash for now)
const encoder = new TextEncoder();
const data = encoder.encode(plainToken);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const tokenHash = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
// Get username from auth token
const authHeader = req.headers.get('Authorization');
let createdBy = 'system';
if (authHeader && authHeader.startsWith('Bearer ')) {
try {
const decoded = atob(authHeader.slice(7));
createdBy = decoded.split(':')[0];
} catch {
// Keep default
}
}
// Create token in database
const token = this.oneboxRef.database.createRegistryToken({
name: body.name,
tokenHash,
type: body.type,
scope: body.scope,
expiresAt,
createdAt: now,
lastUsedAt: null,
createdBy,
});
// Build view response
let scopeDisplay: string;
if (token.scope === 'all') {
scopeDisplay = 'All services';
} else if (Array.isArray(token.scope)) {
scopeDisplay = token.scope.length === 1
? token.scope[0]
: `${token.scope.length} services`;
} else {
scopeDisplay = 'Unknown';
}
const tokenView: IRegistryTokenView = {
id: token.id!,
name: token.name,
type: token.type,
scope: token.scope,
scopeDisplay,
expiresAt: token.expiresAt,
createdAt: token.createdAt,
lastUsedAt: token.lastUsedAt,
createdBy: token.createdBy,
isExpired: false,
};
return this.jsonResponse({
success: true,
data: {
token: tokenView,
plainToken, // Only returned once at creation
},
});
} catch (error) {
logger.error(`Failed to create registry token: ${getErrorMessage(error)}`);
return this.jsonResponse({
success: false,
error: getErrorMessage(error) || 'Failed to create registry token',
}, 500);
}
}
private async handleDeleteRegistryTokenRequest(tokenId: number): Promise<Response> {
try {
// Check if token exists
const token = this.oneboxRef.database.getRegistryTokenById(tokenId);
if (!token) {
return this.jsonResponse({
success: false,
error: 'Token not found',
}, 404);
}
// Delete the token
this.oneboxRef.database.deleteRegistryToken(tokenId);
return this.jsonResponse({
success: true,
message: 'Token deleted successfully',
});
} catch (error) {
logger.error(`Failed to delete registry token ${tokenId}: ${getErrorMessage(error)}`);
return this.jsonResponse({
success: false,
error: getErrorMessage(error) || 'Failed to delete registry token',
}, 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' },
});
}
}