feat: integrate toast notifications in settings and layout components

- Added ToastService for managing toast notifications.
- Replaced alert in settings component with toast notifications for success and error messages.
- Included ToastComponent in layout for displaying notifications.
- Created loading spinner component for better user experience.
- Implemented domain detail component with detailed views for certificates, requirements, and services.
- Added functionality to manage and display SSL certificates and their statuses.
- Introduced a registry manager class for handling Docker registry operations.
This commit is contained in:
2025-11-24 01:31:15 +00:00
parent b6ac4f209a
commit c9beae93c8
23 changed files with 2475 additions and 130 deletions

View File

@@ -76,6 +76,11 @@ export class OneboxHttpServer {
return this.handleWebSocketUpgrade(req);
}
// 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);
@@ -199,6 +204,9 @@ export class OneboxHttpServer {
} 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);
@@ -231,6 +239,21 @@ export class OneboxHttpServer {
} 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.match(/^\/api\/registry\/token\/[^/]+$/)) {
const serviceName = path.split('/').pop()!;
return await this.handleGetRegistryTokenRequest(serviceName);
} else {
return this.jsonResponse({ success: false, error: 'Not found' }, 404);
}
@@ -338,6 +361,36 @@ export class OneboxHttpServer {
}
}
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);
}
}
private async handleDeleteServiceRequest(name: string): Promise<Response> {
try {
await this.oneboxRef.services.removeService(name);
@@ -659,6 +712,87 @@ export class OneboxHttpServer {
}
}
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);
}
}
/**
* Handle WebSocket upgrade
*/
@@ -755,6 +889,70 @@ export class OneboxHttpServer {
});
}
// ============ 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);
}
}
/**
* Helper to create JSON response
*/