feat(ssl): Add domain & certificate management, Cloudflare sync, SQLite cert manager, WebSocket realtime updates, and HTTP API SSL endpoints

This commit is contained in:
2025-11-18 19:34:26 +00:00
parent 44267bbb27
commit b94aa17eee
16 changed files with 1707 additions and 1344 deletions

View File

@@ -1,5 +1,6 @@
import { Component } from '@angular/core';
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { WebSocketService } from './core/services/websocket.service';
@Component({
selector: 'app-root',
@@ -7,4 +8,16 @@ import { RouterOutlet } from '@angular/router';
imports: [RouterOutlet],
template: `<router-outlet></router-outlet>`,
})
export class AppComponent {}
export class AppComponent implements OnInit, OnDestroy {
private wsService = inject(WebSocketService);
ngOnInit(): void {
// Connect to WebSocket when app starts
this.wsService.connect();
}
ngOnDestroy(): void {
// Disconnect when app is destroyed
this.wsService.disconnect();
}
}

View File

@@ -35,9 +35,17 @@ export interface SystemStatus {
running: boolean;
version: any;
};
nginx: {
status: string;
installed: boolean;
reverseProxy: {
http: {
running: boolean;
port: number;
};
https: {
running: boolean;
port: number;
certificates: number;
};
routes: number;
};
dns: {
configured: boolean;

View File

@@ -0,0 +1,101 @@
import { Injectable } from '@angular/core';
import { Subject, Observable } from 'rxjs';
export interface WebSocketMessage {
type: string;
action?: string;
serviceName?: string;
data?: any;
status?: string;
timestamp: number;
message?: string;
}
@Injectable({
providedIn: 'root'
})
export class WebSocketService {
private ws: WebSocket | null = null;
private messageSubject = new Subject<WebSocketMessage>();
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private reconnectDelay = 3000;
private reconnectTimer: any = null;
constructor() {}
connect(): void {
if (this.ws?.readyState === WebSocket.OPEN) {
console.log('WebSocket already connected');
return;
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/api/ws`;
console.log('Connecting to WebSocket:', wsUrl);
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => {
console.log('✓ WebSocket connected');
this.reconnectAttempts = 0;
};
this.ws.onmessage = (event) => {
try {
const message: WebSocketMessage = JSON.parse(event.data);
console.log('📨 WebSocket message:', message);
this.messageSubject.next(message);
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
};
this.ws.onerror = (error) => {
console.error('✖ WebSocket error:', error);
};
this.ws.onclose = () => {
console.log('⚠ WebSocket closed');
this.ws = null;
this.attemptReconnect();
};
}
private attemptReconnect(): void {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Max WebSocket reconnect attempts reached');
return;
}
this.reconnectAttempts++;
const delay = this.reconnectDelay * this.reconnectAttempts;
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
this.reconnectTimer = setTimeout(() => {
this.connect();
}, delay);
}
disconnect(): void {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
getMessages(): Observable<WebSocketMessage> {
return this.messageSubject.asObservable();
}
isConnected(): boolean {
return this.ws?.readyState === WebSocket.OPEN;
}
}

View File

@@ -110,22 +110,26 @@ import { ApiService, SystemStatus } from '../../core/services/api.service';
</div>
</div>
<!-- Nginx -->
<!-- Reverse Proxy -->
<div class="card">
<h3 class="text-lg font-medium text-gray-900 mb-4">Nginx</h3>
<h3 class="text-lg font-medium text-gray-900 mb-4">Reverse Proxy</h3>
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-sm text-gray-600">Status</span>
<span [ngClass]="status()!.nginx.status === 'running' ? 'badge-success' : 'badge-danger'" class="badge">
{{ status()!.nginx.status }}
<span class="text-sm text-gray-600">HTTP (Port {{ status()!.reverseProxy.http.port }})</span>
<span [ngClass]="status()!.reverseProxy.http.running ? 'badge-success' : 'badge-danger'" class="badge">
{{ status()!.reverseProxy.http.running ? 'Running' : 'Stopped' }}
</span>
</div>
<div class="flex justify-between">
<span class="text-sm text-gray-600">Installed</span>
<span [ngClass]="status()!.nginx.installed ? 'badge-success' : 'badge-danger'" class="badge">
{{ status()!.nginx.installed ? 'Yes' : 'No' }}
<span class="text-sm text-gray-600">HTTPS (Port {{ status()!.reverseProxy.https.port }})</span>
<span [ngClass]="status()!.reverseProxy.https.running ? 'badge-success' : 'badge-danger'" class="badge">
{{ status()!.reverseProxy.https.running ? 'Running' : 'Stopped' }}
</span>
</div>
<div class="flex justify-between">
<span class="text-sm text-gray-600">SSL Certificates</span>
<span class="badge badge-info">{{ status()!.reverseProxy.https.certificates }}</span>
</div>
</div>
</div>

View File

@@ -1,7 +1,9 @@
import { Component, OnInit, inject, signal } from '@angular/core';
import { Component, OnInit, OnDestroy, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
import { ApiService, Service } from '../../core/services/api.service';
import { WebSocketService } from '../../core/services/websocket.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-services-list',
@@ -89,14 +91,42 @@ import { ApiService, Service } from '../../core/services/api.service';
</div>
`,
})
export class ServicesListComponent implements OnInit {
export class ServicesListComponent implements OnInit, OnDestroy {
private apiService = inject(ApiService);
private wsService = inject(WebSocketService);
private wsSubscription?: Subscription;
services = signal<Service[]>([]);
loading = signal(true);
ngOnInit(): void {
// Initial load
this.loadServices();
// Subscribe to WebSocket updates
this.wsSubscription = this.wsService.getMessages().subscribe((message) => {
this.handleWebSocketMessage(message);
});
}
ngOnDestroy(): void {
this.wsSubscription?.unsubscribe();
}
private handleWebSocketMessage(message: any): void {
if (message.type === 'service_update') {
// Reload the full service list on any service update
this.loadServices();
} else if (message.type === 'service_status') {
// Update individual service status
const currentServices = this.services();
const updatedServices = currentServices.map(s =>
s.name === message.serviceName
? { ...s, status: message.status }
: s
);
this.services.set(updatedServices);
}
}
loadServices(): void {
@@ -117,7 +147,7 @@ export class ServicesListComponent implements OnInit {
startService(service: Service): void {
this.apiService.startService(service.name).subscribe({
next: () => {
this.loadServices();
// WebSocket will handle the update
},
});
}
@@ -125,7 +155,7 @@ export class ServicesListComponent implements OnInit {
stopService(service: Service): void {
this.apiService.stopService(service.name).subscribe({
next: () => {
this.loadServices();
// WebSocket will handle the update
},
});
}
@@ -133,7 +163,7 @@ export class ServicesListComponent implements OnInit {
restartService(service: Service): void {
this.apiService.restartService(service.name).subscribe({
next: () => {
this.loadServices();
// WebSocket will handle the update
},
});
}
@@ -142,7 +172,7 @@ export class ServicesListComponent implements OnInit {
if (confirm(`Are you sure you want to delete ${service.name}?`)) {
this.apiService.deleteService(service.name).subscribe({
next: () => {
this.loadServices();
// WebSocket will handle the update
},
});
}