feat(ssl): Add domain & certificate management, Cloudflare sync, SQLite cert manager, WebSocket realtime updates, and HTTP API SSL endpoints
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
101
ui/src/app/core/services/websocket.service.ts
Normal file
101
ui/src/app/core/services/websocket.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user