179 lines
6.1 KiB
TypeScript
179 lines
6.1 KiB
TypeScript
import * as plugins from '../plugins.ts';
|
|
import { logger } from '../logging.ts';
|
|
import type { Onebox } from '../classes/onebox.ts';
|
|
import * as interfaces from '../../ts_interfaces/index.ts';
|
|
import * as handlers from './handlers/index.ts';
|
|
import { files as bundledFiles } from '../../ts_bundled/bundle.ts';
|
|
|
|
export class OpsServer {
|
|
public oneboxRef: Onebox;
|
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
public server!: plugins.typedserver.utilityservers.UtilityWebsiteServer;
|
|
|
|
// Handler instances
|
|
public adminHandler!: handlers.AdminHandler;
|
|
public statusHandler!: handlers.StatusHandler;
|
|
public servicesHandler!: handlers.ServicesHandler;
|
|
public platformHandler!: handlers.PlatformHandler;
|
|
public sslHandler!: handlers.SslHandler;
|
|
public domainsHandler!: handlers.DomainsHandler;
|
|
public dnsHandler!: handlers.DnsHandler;
|
|
public registryHandler!: handlers.RegistryHandler;
|
|
public networkHandler!: handlers.NetworkHandler;
|
|
public backupsHandler!: handlers.BackupsHandler;
|
|
public schedulesHandler!: handlers.SchedulesHandler;
|
|
public settingsHandler!: handlers.SettingsHandler;
|
|
public logsHandler!: handlers.LogsHandler;
|
|
public workspaceHandler!: handlers.WorkspaceHandler;
|
|
public appStoreHandler!: handlers.AppStoreHandler;
|
|
|
|
constructor(oneboxRef: Onebox) {
|
|
this.oneboxRef = oneboxRef;
|
|
}
|
|
|
|
public async start(port = 3000) {
|
|
this.server = new plugins.typedserver.utilityservers.UtilityWebsiteServer({
|
|
domain: 'localhost',
|
|
feedMetadata: undefined,
|
|
bundledContent: bundledFiles,
|
|
});
|
|
|
|
// Chain typedrouters: server -> opsServer -> individual handlers
|
|
this.server.typedrouter.addTypedRouter(this.typedrouter);
|
|
|
|
// Set up all handlers
|
|
await this.setupHandlers();
|
|
this.registerCustomRoutes();
|
|
|
|
await this.server.start(port);
|
|
logger.success(`OpsServer started on http://localhost:${port}`);
|
|
}
|
|
|
|
private async setupHandlers(): Promise<void> {
|
|
// AdminHandler requires async initialization for JWT key generation
|
|
this.adminHandler = new handlers.AdminHandler(this);
|
|
await this.adminHandler.initialize();
|
|
|
|
// All other handlers self-register in their constructors
|
|
this.statusHandler = new handlers.StatusHandler(this);
|
|
this.servicesHandler = new handlers.ServicesHandler(this);
|
|
this.platformHandler = new handlers.PlatformHandler(this);
|
|
this.sslHandler = new handlers.SslHandler(this);
|
|
this.domainsHandler = new handlers.DomainsHandler(this);
|
|
this.dnsHandler = new handlers.DnsHandler(this);
|
|
this.registryHandler = new handlers.RegistryHandler(this);
|
|
this.networkHandler = new handlers.NetworkHandler(this);
|
|
this.backupsHandler = new handlers.BackupsHandler(this);
|
|
this.schedulesHandler = new handlers.SchedulesHandler(this);
|
|
this.settingsHandler = new handlers.SettingsHandler(this);
|
|
this.logsHandler = new handlers.LogsHandler(this);
|
|
this.workspaceHandler = new handlers.WorkspaceHandler(this);
|
|
this.appStoreHandler = new handlers.AppStoreHandler(this);
|
|
|
|
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() {
|
|
if (this.server) {
|
|
await this.server.stop();
|
|
logger.success('OpsServer stopped');
|
|
}
|
|
}
|
|
|
|
public async pushDashboardEvent(method: string, payload: unknown): Promise<void> {
|
|
const typedsocket = (this.server as any)?.typedserver?.typedsocket;
|
|
if (!typedsocket) {
|
|
return;
|
|
}
|
|
|
|
const connections = await typedsocket.findAllTargetConnectionsByTag('role', 'ops_dashboard');
|
|
await Promise.allSettled(
|
|
connections.map((connection: any) => typedsocket.createTypedRequest(method, connection).fire(payload)),
|
|
);
|
|
}
|
|
|
|
public async broadcastServiceUpdate(
|
|
serviceName: string,
|
|
action: interfaces.requests.IReq_PushServiceUpdate['request']['action'],
|
|
service?: interfaces.data.IService | null,
|
|
): Promise<void> {
|
|
await this.pushDashboardEvent('pushServiceUpdate', {
|
|
action,
|
|
serviceName,
|
|
service: service || undefined,
|
|
});
|
|
}
|
|
}
|