refactor: complete opsserver migration
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> 🚀 Self-hosted Docker Swarm platform with Caddy reverse proxy, automatic SSL, and real-time WebSocket updates
|
> 🚀 Self-hosted Docker Swarm platform with Caddy reverse proxy, automatic SSL, and real-time WebSocket updates
|
||||||
|
|
||||||
**Onebox** transforms any Linux server into a powerful container hosting platform. Deploy Docker Swarm services with automatic HTTPS, DNS configuration, and Caddy reverse proxy running as a Docker service - all managed through a beautiful Angular web interface with real-time updates.
|
**Onebox** transforms any Linux server into a powerful container hosting platform. Deploy Docker Swarm services with automatic HTTPS, DNS configuration, and Caddy reverse proxy running as a Docker service - all managed through a modern web interface with real-time updates.
|
||||||
|
|
||||||
## Issue Reporting and Security
|
## Issue Reporting and Security
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|||||||
- **Private Registry Included** - Built-in Docker registry with token-based auth and auto-deploy on push
|
- **Private Registry Included** - Built-in Docker registry with token-based auth and auto-deploy on push
|
||||||
- **Zero Config SSL** - Automatic Let's Encrypt certificates with inline `load_pem` (no volume mounts needed)
|
- **Zero Config SSL** - Automatic Let's Encrypt certificates with inline `load_pem` (no volume mounts needed)
|
||||||
- **Cloudflare Integration** - Automatic DNS record management and zone synchronization
|
- **Cloudflare Integration** - Automatic DNS record management and zone synchronization
|
||||||
- **Modern Stack** - Deno runtime + SQLite database + Angular 19 UI
|
- **Modern Stack** - Deno runtime + SQLite database + typed web UI
|
||||||
|
|
||||||
## Features ✨
|
## Features ✨
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|||||||
|
|
||||||
- 📊 **Metrics Collection** - Historical CPU, memory, and network stats (every 60s)
|
- 📊 **Metrics Collection** - Historical CPU, memory, and network stats (every 60s)
|
||||||
- 📝 **Centralized Logging** - Container logs with streaming and retention policies
|
- 📝 **Centralized Logging** - Container logs with streaming and retention policies
|
||||||
- 🎨 **Angular Web UI** - Modern, responsive interface with real-time updates
|
- 🎨 **Web UI** - Modern, responsive interface with real-time updates
|
||||||
- 👥 **Multi-user Support** - Role-based access control (admin/user)
|
- 👥 **Multi-user Support** - Role-based access control (admin/user)
|
||||||
- 💾 **SQLite Database** - Embedded, zero-configuration storage
|
- 💾 **SQLite Database** - Embedded, zero-configuration storage
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|||||||
- 🚀 **Auto-update on Push** - Push to registry and services update automatically
|
- 🚀 **Auto-update on Push** - Push to registry and services update automatically
|
||||||
- 🔐 **Private Registry Support** - Use Docker Hub, Gitea, or custom registries
|
- 🔐 **Private Registry Support** - Use Docker Hub, Gitea, or custom registries
|
||||||
- 🔄 **Systemd Integration** - Run as a daemon with auto-restart
|
- 🔄 **Systemd Integration** - Run as a daemon with auto-restart
|
||||||
- 🎛️ **Full CLI & API** - Manage everything from terminal or HTTP API
|
- 🎛️ **Full CLI** - Manage everything from terminal or web interface
|
||||||
|
|
||||||
## Quick Start 🏁
|
## Quick Start 🏁
|
||||||
|
|
||||||
@@ -103,13 +103,13 @@ Onebox is built with modern technologies for performance and developer experienc
|
|||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────┐
|
||||||
│ Angular 19 Web UI │
|
│ Web UI │
|
||||||
│ (Real-time WebSocket Updates) │
|
│ (Real-time WebSocket Updates) │
|
||||||
└─────────────────┬───────────────────────────────┘
|
└─────────────────┬───────────────────────────────┘
|
||||||
│ HTTP/WS
|
│ HTTP/WS
|
||||||
┌─────────────────▼───────────────────────────────┐
|
┌─────────────────▼───────────────────────────────┐
|
||||||
│ Deno HTTP Server (Port 3000) │
|
│ OpsServer (Port 3000) │
|
||||||
│ REST API + WebSocket Broadcast │
|
│ TypedRequest + TypedSocket │
|
||||||
└─────────────────┬───────────────────────────────┘
|
└─────────────────┬───────────────────────────────┘
|
||||||
│
|
│
|
||||||
┌─────────────────▼───────────────────────────────┐
|
┌─────────────────▼───────────────────────────────┐
|
||||||
@@ -140,7 +140,7 @@ Onebox is built with modern technologies for performance and developer experienc
|
|||||||
| **Caddy Reverse Proxy** | Docker Swarm service with HTTP/2, HTTP/3, SNI, and WebSocket support |
|
| **Caddy Reverse Proxy** | Docker Swarm service with HTTP/2, HTTP/3, SNI, and WebSocket support |
|
||||||
| **Docker Swarm** | Container orchestration (all workloads run as services) |
|
| **Docker Swarm** | Container orchestration (all workloads run as services) |
|
||||||
| **SQLite Database** | Configuration, metrics, and user data |
|
| **SQLite Database** | Configuration, metrics, and user data |
|
||||||
| **WebSocket Server** | Real-time bidirectional communication |
|
| **OpsServer** | TypedRequest API and TypedSocket real-time updates |
|
||||||
| **Let's Encrypt** | Automatic SSL certificate management |
|
| **Let's Encrypt** | Automatic SSL certificate management |
|
||||||
| **Cloudflare API** | DNS record automation |
|
| **Cloudflare API** | DNS record automation |
|
||||||
|
|
||||||
@@ -235,9 +235,8 @@ onebox config show
|
|||||||
onebox config set <key> <value>
|
onebox config set <key> <value>
|
||||||
|
|
||||||
# Example: Configure Cloudflare
|
# Example: Configure Cloudflare
|
||||||
onebox config set cloudflareAPIKey your-api-key
|
onebox config set cloudflareToken your-api-token
|
||||||
onebox config set cloudflareEmail your@email.com
|
onebox config set cloudflareZoneId your-zone-id
|
||||||
onebox config set cloudflareZoneID your-zone-id
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### System Status
|
### System Status
|
||||||
@@ -324,7 +323,6 @@ onebox/
|
|||||||
│ │ ├── reverseproxy.ts # Reverse proxy orchestration
|
│ │ ├── reverseproxy.ts # Reverse proxy orchestration
|
||||||
│ │ ├── caddy.ts # Caddy Docker service management
|
│ │ ├── caddy.ts # Caddy Docker service management
|
||||||
│ │ ├── docker.ts # Docker Swarm API
|
│ │ ├── docker.ts # Docker Swarm API
|
||||||
│ │ ├── httpserver.ts # REST API + WebSocket
|
|
||||||
│ │ ├── services.ts # Service orchestration
|
│ │ ├── services.ts # Service orchestration
|
||||||
│ │ ├── certmanager.ts # SSL certificate management
|
│ │ ├── certmanager.ts # SSL certificate management
|
||||||
│ │ ├── cert-requirement-manager.ts # Certificate requirements
|
│ │ ├── cert-requirement-manager.ts # Certificate requirements
|
||||||
@@ -333,8 +331,10 @@ onebox/
|
|||||||
│ │ ├── registries.ts # External registry management
|
│ │ ├── registries.ts # External registry management
|
||||||
│ │ ├── dns.ts # DNS record management
|
│ │ ├── dns.ts # DNS record management
|
||||||
│ │ ├── cloudflare-sync.ts # Cloudflare zone sync
|
│ │ ├── cloudflare-sync.ts # Cloudflare zone sync
|
||||||
│ │ ├── daemon.ts # Systemd daemon management
|
│ │ └── daemon.ts # Systemd daemon management
|
||||||
│ │ └── apiclient.ts # API client utilities
|
│ ├── opsserver/ # Active server implementation
|
||||||
|
│ │ ├── classes.opsserver.ts # TypedRequest + TypedSocket server
|
||||||
|
│ │ └── handlers/ # Typed request handlers
|
||||||
│ ├── database/ # Database layer (repository pattern)
|
│ ├── database/ # Database layer (repository pattern)
|
||||||
│ │ ├── index.ts # Main OneboxDatabase class
|
│ │ ├── index.ts # Main OneboxDatabase class
|
||||||
│ │ ├── base.repository.ts # Base repository class
|
│ │ ├── base.repository.ts # Base repository class
|
||||||
@@ -348,105 +348,17 @@ onebox/
|
|||||||
│ ├── types.ts # TypeScript interfaces
|
│ ├── types.ts # TypeScript interfaces
|
||||||
│ ├── logging.ts # Logging utilities
|
│ ├── logging.ts # Logging utilities
|
||||||
│ └── plugins.ts # Dependency imports
|
│ └── plugins.ts # Dependency imports
|
||||||
├── ui/ # Angular 19 web interface
|
├── ts_web/ # Web interface source
|
||||||
├── test/ # Test files
|
├── test/ # Test files
|
||||||
├── mod.ts # Main entry point
|
├── mod.ts # Main entry point
|
||||||
└── deno.json # Deno configuration
|
└── deno.json # Deno configuration
|
||||||
```
|
```
|
||||||
|
|
||||||
### API Endpoints
|
### Active Server Surface
|
||||||
|
|
||||||
The HTTP server exposes a comprehensive REST API:
|
The active server surface is the `OpsServer`, which serves the bundled web UI and exposes typed operations via `TypedRequest` and real-time events via `TypedSocket`.
|
||||||
|
|
||||||
#### Authentication
|
The previously documented legacy `/api/*` REST interface has been removed.
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
|
||||||
| ------ | ----------------- | ----------------------------------- |
|
|
||||||
| `POST` | `/api/auth/login` | User authentication (returns token) |
|
|
||||||
|
|
||||||
#### Services
|
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
|
||||||
| -------- | --------------------------------- | ------------------------- |
|
|
||||||
| `GET` | `/api/services` | List all services |
|
|
||||||
| `POST` | `/api/services` | Create/deploy service |
|
|
||||||
| `GET` | `/api/services/:name` | Get service details |
|
|
||||||
| `PUT` | `/api/services/:name` | Update service |
|
|
||||||
| `DELETE` | `/api/services/:name` | Delete service |
|
|
||||||
| `POST` | `/api/services/:name/start` | Start service |
|
|
||||||
| `POST` | `/api/services/:name/stop` | Stop service |
|
|
||||||
| `POST` | `/api/services/:name/restart` | Restart service |
|
|
||||||
| `GET` | `/api/services/:name/logs` | Get service logs |
|
|
||||||
| `WS` | `/api/services/:name/logs/stream` | Stream logs via WebSocket |
|
|
||||||
|
|
||||||
#### SSL Certificates
|
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
|
||||||
| ------ | ------------------------ | ----------------------- |
|
|
||||||
| `GET` | `/api/ssl/list` | List all certificates |
|
|
||||||
| `GET` | `/api/ssl/:domain` | Get certificate details |
|
|
||||||
| `POST` | `/api/ssl/obtain` | Request new certificate |
|
|
||||||
| `POST` | `/api/ssl/:domain/renew` | Force renew certificate |
|
|
||||||
|
|
||||||
#### Domains
|
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
|
||||||
| ------ | ---------------------- | ---------------------------- |
|
|
||||||
| `GET` | `/api/domains` | List all domains |
|
|
||||||
| `GET` | `/api/domains/:domain` | Get domain details |
|
|
||||||
| `POST` | `/api/domains/sync` | Sync domains from Cloudflare |
|
|
||||||
|
|
||||||
#### DNS Records
|
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
|
||||||
| -------- | ------------------ | ------------------------ |
|
|
||||||
| `GET` | `/api/dns` | List DNS records |
|
|
||||||
| `POST` | `/api/dns` | Create DNS record |
|
|
||||||
| `DELETE` | `/api/dns/:domain` | Delete DNS record |
|
|
||||||
| `POST` | `/api/dns/sync` | Sync DNS from Cloudflare |
|
|
||||||
|
|
||||||
#### Registry
|
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
|
||||||
| -------- | ----------------------------- | ----------------------------- |
|
|
||||||
| `GET` | `/api/registry/tags/:service` | Get registry tags for service |
|
|
||||||
| `GET` | `/api/registry/tokens` | List registry tokens |
|
|
||||||
| `POST` | `/api/registry/tokens` | Create registry token |
|
|
||||||
| `DELETE` | `/api/registry/tokens/:id` | Delete registry token |
|
|
||||||
|
|
||||||
#### System
|
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
|
||||||
| ------ | --------------- | ------------------------------- |
|
|
||||||
| `GET` | `/api/status` | System status |
|
|
||||||
| `GET` | `/api/settings` | Get settings |
|
|
||||||
| `PUT` | `/api/settings` | Update settings |
|
|
||||||
| `WS` | `/api/ws` | WebSocket for real-time updates |
|
|
||||||
|
|
||||||
### WebSocket Messages
|
|
||||||
|
|
||||||
Real-time updates are broadcast via WebSocket:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Service lifecycle updates
|
|
||||||
{
|
|
||||||
type: 'service_update',
|
|
||||||
action: 'created' | 'updated' | 'deleted' | 'started' | 'stopped',
|
|
||||||
service: { id, name, status, ... }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Service status changes
|
|
||||||
{
|
|
||||||
type: 'service_status',
|
|
||||||
service: { id, name, status, ... }
|
|
||||||
}
|
|
||||||
|
|
||||||
// System status updates
|
|
||||||
{
|
|
||||||
type: 'system_status',
|
|
||||||
status: { docker, reverseProxy, services, ... }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Advanced Usage 🚀
|
## Advanced Usage 🚀
|
||||||
|
|
||||||
@@ -472,13 +384,7 @@ docker push localhost:4000/myapp:latest
|
|||||||
### Registry Token Management
|
### Registry Token Management
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create a CI/CD token via API
|
# Create a CI/CD token in the web UI, then use it for docker login
|
||||||
curl -X POST http://localhost:3000/api/registry/tokens \
|
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"name": "github-actions", "type": "ci", "scope": ["myapp"], "expiresIn": "90d"}'
|
|
||||||
|
|
||||||
# Use token for docker login
|
|
||||||
docker login localhost:4000 -u ci -p <token>
|
docker login localhost:4000 -u ci -p <token>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -486,9 +392,8 @@ docker login localhost:4000 -u ci -p <token>
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Configure Cloudflare (one-time setup)
|
# Configure Cloudflare (one-time setup)
|
||||||
onebox config set cloudflareAPIKey your-api-key
|
onebox config set cloudflareToken your-api-token
|
||||||
onebox config set cloudflareEmail your@email.com
|
onebox config set cloudflareZoneId your-zone-id
|
||||||
onebox config set cloudflareZoneID your-zone-id
|
|
||||||
|
|
||||||
# Deploy with automatic DNS
|
# Deploy with automatic DNS
|
||||||
onebox service add myapp \
|
onebox service add myapp \
|
||||||
@@ -562,7 +467,7 @@ onebox ssl force-renew yourdomain.com
|
|||||||
|
|
||||||
- ✅ Ensure firewall allows WebSocket connections
|
- ✅ Ensure firewall allows WebSocket connections
|
||||||
- ✅ Check browser console for connection errors
|
- ✅ Check browser console for connection errors
|
||||||
- ✅ Verify `/api/ws` endpoint is accessible
|
- ✅ Verify the dashboard socket connection is established
|
||||||
|
|
||||||
### Service Not Starting
|
### Service Not Starting
|
||||||
|
|
||||||
|
|||||||
@@ -1,210 +0,0 @@
|
|||||||
/**
|
|
||||||
* API Client for communicating with Onebox daemon
|
|
||||||
*
|
|
||||||
* Provides methods for CLI commands to interact with running daemon via HTTP API
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type {
|
|
||||||
IService,
|
|
||||||
IRegistry,
|
|
||||||
IDnsRecord,
|
|
||||||
ISslCertificate,
|
|
||||||
IServiceDeployOptions,
|
|
||||||
} from '../types.ts';
|
|
||||||
import { getErrorMessage } from '../utils/error.ts';
|
|
||||||
|
|
||||||
export class OneboxApiClient {
|
|
||||||
private baseUrl: string;
|
|
||||||
private token?: string;
|
|
||||||
|
|
||||||
constructor(port = 3000) {
|
|
||||||
this.baseUrl = `http://localhost:${port}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if daemon is reachable
|
|
||||||
*/
|
|
||||||
async isReachable(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${this.baseUrl}/api/status`, {
|
|
||||||
signal: AbortSignal.timeout(5000), // 5 second timeout
|
|
||||||
});
|
|
||||||
return response.ok;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ Service Operations ============
|
|
||||||
|
|
||||||
async deployService(config: IServiceDeployOptions): Promise<IService> {
|
|
||||||
return await this.request<IService>('POST', '/api/services', config);
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeService(name: string): Promise<void> {
|
|
||||||
await this.request('DELETE', `/api/services/${name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async startService(name: string): Promise<void> {
|
|
||||||
await this.request('POST', `/api/services/${name}/start`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async stopService(name: string): Promise<void> {
|
|
||||||
await this.request('POST', `/api/services/${name}/stop`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async restartService(name: string): Promise<void> {
|
|
||||||
await this.request('POST', `/api/services/${name}/restart`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async listServices(): Promise<IService[]> {
|
|
||||||
return await this.request<IService[]>('GET', '/api/services');
|
|
||||||
}
|
|
||||||
|
|
||||||
async getServiceLogs(name: string, limit = 1000): Promise<string[]> {
|
|
||||||
const result = await this.request<{ logs: string[] }>(
|
|
||||||
'GET',
|
|
||||||
`/api/services/${name}/logs?limit=${limit}`
|
|
||||||
);
|
|
||||||
return result.logs;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ Registry Operations ============
|
|
||||||
|
|
||||||
async addRegistry(url: string, username: string, password: string): Promise<void> {
|
|
||||||
await this.request('POST', '/api/registries', { url, username, password });
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeRegistry(url: string): Promise<void> {
|
|
||||||
await this.request('DELETE', `/api/registries/${encodeURIComponent(url)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async listRegistries(): Promise<IRegistry[]> {
|
|
||||||
return await this.request<IRegistry[]>('GET', '/api/registries');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ DNS Operations ============
|
|
||||||
|
|
||||||
async addDnsRecord(domain: string): Promise<void> {
|
|
||||||
await this.request('POST', '/api/dns', { domain });
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeDnsRecord(domain: string): Promise<void> {
|
|
||||||
await this.request('DELETE', `/api/dns/${domain}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async listDnsRecords(): Promise<IDnsRecord[]> {
|
|
||||||
return await this.request<IDnsRecord[]>('GET', '/api/dns');
|
|
||||||
}
|
|
||||||
|
|
||||||
async syncDns(): Promise<void> {
|
|
||||||
await this.request('POST', '/api/dns/sync');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ SSL Operations ============
|
|
||||||
|
|
||||||
async renewCertificate(domain?: string): Promise<void> {
|
|
||||||
const path = domain ? `/api/ssl/renew/${domain}` : '/api/ssl/renew';
|
|
||||||
await this.request('POST', path);
|
|
||||||
}
|
|
||||||
|
|
||||||
async listCertificates(): Promise<ISslCertificate[]> {
|
|
||||||
return await this.request<ISslCertificate[]>('GET', '/api/ssl');
|
|
||||||
}
|
|
||||||
|
|
||||||
async forceRenewCertificate(domain: string): Promise<void> {
|
|
||||||
await this.request('POST', `/api/ssl/renew/${domain}?force=true`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ Nginx Operations ============
|
|
||||||
|
|
||||||
async reloadNginx(): Promise<void> {
|
|
||||||
await this.request('POST', '/api/nginx/reload');
|
|
||||||
}
|
|
||||||
|
|
||||||
async testNginx(): Promise<{ success: boolean; output: string }> {
|
|
||||||
return await this.request('POST', '/api/nginx/test');
|
|
||||||
}
|
|
||||||
|
|
||||||
async getNginxStatus(): Promise<{ status: string }> {
|
|
||||||
return await this.request('GET', '/api/nginx/status');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ Config Operations ============
|
|
||||||
|
|
||||||
async getSettings(): Promise<Record<string, string>> {
|
|
||||||
return await this.request<Record<string, string>>('GET', '/api/config');
|
|
||||||
}
|
|
||||||
|
|
||||||
async setSetting(key: string, value: string): Promise<void> {
|
|
||||||
await this.request('POST', '/api/config', { key, value });
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ System Operations ============
|
|
||||||
|
|
||||||
async getStatus(): Promise<{
|
|
||||||
services: { total: number; running: number; stopped: number };
|
|
||||||
uptime: number;
|
|
||||||
}> {
|
|
||||||
return await this.request('GET', '/api/status');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ Helper Methods ============
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make HTTP request to daemon
|
|
||||||
*/
|
|
||||||
private async request<T = unknown>(
|
|
||||||
method: string,
|
|
||||||
path: string,
|
|
||||||
body?: unknown
|
|
||||||
): Promise<T> {
|
|
||||||
const url = `${this.baseUrl}${path}`;
|
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.token) {
|
|
||||||
headers['Authorization'] = `Bearer ${this.token}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: RequestInit = {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
signal: AbortSignal.timeout(30000), // 30 second timeout
|
|
||||||
};
|
|
||||||
|
|
||||||
if (body) {
|
|
||||||
options.body = JSON.stringify(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, options);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json().catch(() => ({ message: response.statusText }));
|
|
||||||
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For DELETE and some POST requests, there might be no content
|
|
||||||
if (response.status === 204 || response.headers.get('content-length') === '0') {
|
|
||||||
return undefined as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error && error.name === 'TimeoutError') {
|
|
||||||
throw new Error('Request timed out. Daemon might be unresponsive.');
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set authentication token
|
|
||||||
*/
|
|
||||||
setToken(token: string): void {
|
|
||||||
this.token = token;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,6 @@ import { OneboxDnsManager } from './dns.ts';
|
|||||||
import { OneboxSslManager } from './ssl.ts';
|
import { OneboxSslManager } from './ssl.ts';
|
||||||
import { OneboxDaemon } from './daemon.ts';
|
import { OneboxDaemon } from './daemon.ts';
|
||||||
import { OneboxSystemd } from './systemd.ts';
|
import { OneboxSystemd } from './systemd.ts';
|
||||||
import type { OneboxHttpServer } from './httpserver.ts';
|
|
||||||
import { CloudflareDomainSync } from './cloudflare-sync.ts';
|
import { CloudflareDomainSync } from './cloudflare-sync.ts';
|
||||||
import { CertRequirementManager } from './cert-requirement-manager.ts';
|
import { CertRequirementManager } from './cert-requirement-manager.ts';
|
||||||
import { RegistryManager } from './registry.ts';
|
import { RegistryManager } from './registry.ts';
|
||||||
@@ -37,7 +36,6 @@ export class Onebox {
|
|||||||
public ssl: OneboxSslManager;
|
public ssl: OneboxSslManager;
|
||||||
public daemon: OneboxDaemon;
|
public daemon: OneboxDaemon;
|
||||||
public systemd: OneboxSystemd;
|
public systemd: OneboxSystemd;
|
||||||
public httpServer: OneboxHttpServer | null;
|
|
||||||
public cloudflareDomainSync: CloudflareDomainSync;
|
public cloudflareDomainSync: CloudflareDomainSync;
|
||||||
public certRequirementManager: CertRequirementManager;
|
public certRequirementManager: CertRequirementManager;
|
||||||
public registry: RegistryManager;
|
public registry: RegistryManager;
|
||||||
@@ -63,7 +61,6 @@ export class Onebox {
|
|||||||
this.ssl = new OneboxSslManager(this);
|
this.ssl = new OneboxSslManager(this);
|
||||||
this.daemon = new OneboxDaemon(this);
|
this.daemon = new OneboxDaemon(this);
|
||||||
this.systemd = new OneboxSystemd();
|
this.systemd = new OneboxSystemd();
|
||||||
this.httpServer = null;
|
|
||||||
this.registry = new RegistryManager({
|
this.registry = new RegistryManager({
|
||||||
dataDir: './.nogit/registry-data',
|
dataDir: './.nogit/registry-data',
|
||||||
port: 4000,
|
port: 4000,
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ export { OneboxDnsManager } from './classes/dns.ts';
|
|||||||
export { OneboxSslManager } from './classes/ssl.ts';
|
export { OneboxSslManager } from './classes/ssl.ts';
|
||||||
export { OneboxDaemon } from './classes/daemon.ts';
|
export { OneboxDaemon } from './classes/daemon.ts';
|
||||||
export { OneboxSystemd } from './classes/systemd.ts';
|
export { OneboxSystemd } from './classes/systemd.ts';
|
||||||
export { OneboxHttpServer } from './classes/httpserver.ts';
|
|
||||||
export { OneboxApiClient } from './classes/apiclient.ts';
|
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
export * from './types.ts';
|
export * from './types.ts';
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export class OpsServer {
|
|||||||
|
|
||||||
// Set up all handlers
|
// Set up all handlers
|
||||||
await this.setupHandlers();
|
await this.setupHandlers();
|
||||||
|
this.registerCustomRoutes();
|
||||||
|
|
||||||
await this.server.start(port);
|
await this.server.start(port);
|
||||||
logger.success(`OpsServer started on http://localhost:${port}`);
|
logger.success(`OpsServer started on http://localhost:${port}`);
|
||||||
@@ -72,6 +73,78 @@ export class OpsServer {
|
|||||||
logger.success('OpsServer TypedRequest handlers initialized');
|
logger.success('OpsServer TypedRequest handlers initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private registerCustomRoutes(): void {
|
||||||
|
this.server.typedserver.addRoute(
|
||||||
|
'/backups/:backupId/download',
|
||||||
|
'GET',
|
||||||
|
async (ctx) => {
|
||||||
|
const jwt = ctx.query.jwt;
|
||||||
|
if (!jwt) {
|
||||||
|
return new Response('Missing JWT', { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.adminHandler.getVerifiedAdminIdentity({
|
||||||
|
jwt,
|
||||||
|
userId: '',
|
||||||
|
username: '',
|
||||||
|
expiresAt: 0,
|
||||||
|
role: 'user',
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return new Response('Unauthorized', { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const backupId = Number(ctx.params.backupId);
|
||||||
|
if (!Number.isInteger(backupId) || backupId < 1) {
|
||||||
|
return new Response('Invalid backup id', { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const backup = this.oneboxRef.database.getBackupById(backupId);
|
||||||
|
if (!backup) {
|
||||||
|
return new Response('Backup not found', { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const filename = this.sanitizeDownloadFilename(
|
||||||
|
backup.filename || `${backup.serviceName}-${backup.createdAt}.tar.enc`,
|
||||||
|
);
|
||||||
|
|
||||||
|
let filePath = this.oneboxRef.backupManager.getBackupFilePath(backupId);
|
||||||
|
let shouldCleanup = false;
|
||||||
|
|
||||||
|
if (!filePath) {
|
||||||
|
filePath = await this.oneboxRef.backupManager.getBackupExportPath(backupId);
|
||||||
|
shouldCleanup = !!filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filePath) {
|
||||||
|
return new Response('Backup export unavailable', { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileData = await Deno.readFile(filePath);
|
||||||
|
return new Response(fileData, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/octet-stream',
|
||||||
|
'content-disposition': `attachment; filename="${filename}"`,
|
||||||
|
'content-length': String(fileData.byteLength),
|
||||||
|
'cache-control': 'no-store',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
if (shouldCleanup) {
|
||||||
|
await Deno.remove(filePath).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sanitizeDownloadFilename(filename: string): string {
|
||||||
|
return filename.replace(/["\\\r\n]/g, '_');
|
||||||
|
}
|
||||||
|
|
||||||
public async stop() {
|
public async stop() {
|
||||||
if (this.server) {
|
if (this.server) {
|
||||||
await this.server.stop();
|
await this.server.stop();
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export class BackupsHandler {
|
|||||||
// Return a download URL that the client can fetch directly
|
// Return a download URL that the client can fetch directly
|
||||||
const filename = backup.filename || `${backup.serviceName}-${backup.createdAt}.tar.enc`;
|
const filename = backup.filename || `${backup.serviceName}-${backup.createdAt}.tar.enc`;
|
||||||
return {
|
return {
|
||||||
downloadUrl: `/api/backups/${dataArg.backupId}/download`,
|
downloadUrl: `/backups/${dataArg.backupId}/download?jwt=${encodeURIComponent(dataArg.identity.jwt)}`,
|
||||||
filename,
|
filename,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
+6
-4
@@ -257,14 +257,16 @@ export interface ISetting {
|
|||||||
// Application settings
|
// Application settings
|
||||||
export interface IAppSettings {
|
export interface IAppSettings {
|
||||||
serverIP?: string;
|
serverIP?: string;
|
||||||
cloudflareAPIKey?: string;
|
cloudflareToken?: string;
|
||||||
cloudflareEmail?: string;
|
cloudflareZoneId?: string;
|
||||||
cloudflareZoneID?: string;
|
|
||||||
acmeEmail?: string;
|
acmeEmail?: string;
|
||||||
nginxConfigDir?: string;
|
|
||||||
dataDir?: string;
|
dataDir?: string;
|
||||||
httpPort?: number;
|
httpPort?: number;
|
||||||
|
httpsPort?: number;
|
||||||
metricsInterval?: number;
|
metricsInterval?: number;
|
||||||
|
autoRenewCerts?: boolean;
|
||||||
|
renewalThreshold?: number;
|
||||||
|
forceHttps?: boolean;
|
||||||
logRetentionDays?: number;
|
logRetentionDays?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user