feat: wire the panel command surface to the live Rust agent + wipe handler
All checks were successful
All checks were successful
The legacy Go agent was never deployed, so the entire backend command surface
published to a dead cmd.server/cmd.wipe/files.cmd void. Route it all to the
Rust agent's instance-scoped subjects.
Agent (corrosion-host-agent, alpha.10):
- New src/wipe.rs + 'wipe' func on {instance}.cmd: stop -> delete game files by
type (map/blueprint/full, with optional backup) -> restart. Jailed to the
instance root, symlink-safe (lstat, no cross-boundary follow — Lesson 26).
8 tests incl. jail-escape + symlink-skip proofs. Agent suite 64 tests green.
Backend (NestJS):
- InstancesService is now @Global with license-scoped convenience wrappers
(lifecycleForLicense/rconForLicense/writeFileForLicense/readFileForLicense/
deleteFileForLicense/wipeForLicense) + resolveDefaultInstance (license ->
primary instance).
- Routed to the agent: servers start/stop/restart/command; players kick/banid/
unban via RCON; schedules restart/announce/command/plugin-reload; wipes ->
wipeForLicense (real wipe now); plugins reload/unload/upload via rcon+file
ops; all 9 plugin-config module applies -> writeFileForLicense + oxide.reload
rcon, imports -> readFileForLicense (server:// prefix stripped).
- Honestly gated (need agent funcs not yet built): server deploy-from-panel,
Oxide install, one-click uMod install -> 503 coming-soon instead of dead
publishes.
Backend tsc green; agent cargo test green (64).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import { Injectable, Logger, NotFoundException, HttpException, HttpStatus } from
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { AutoDoorsConfig } from '../../entities/autodoors-config.entity';
|
||||
import { NatsService } from '../../services/nats.service';
|
||||
import { InstancesService } from '../instances/instances.service';
|
||||
import { CreateAutoDoorsConfigDto } from './dto/create-autodoors-config.dto';
|
||||
import { UpdateAutoDoorsConfigDto } from './dto/update-autodoors-config.dto';
|
||||
|
||||
@@ -13,7 +13,7 @@ export class AutoDoorsService {
|
||||
constructor(
|
||||
@InjectRepository(AutoDoorsConfig)
|
||||
private readonly autoDoorsRepo: Repository<AutoDoorsConfig>,
|
||||
private readonly natsService: NatsService,
|
||||
private readonly instancesService: InstancesService,
|
||||
) {}
|
||||
|
||||
/** List configs for a license (summaries — no JSONB) */
|
||||
@@ -81,26 +81,15 @@ export class AutoDoorsService {
|
||||
const jsonString = JSON.stringify(config.config_data, null, 2);
|
||||
|
||||
try {
|
||||
// Write AutoDoors.json via file manager NATS
|
||||
await this.natsService.request(
|
||||
`corrosion.${licenseId}.files.cmd`,
|
||||
{
|
||||
func: 'fm_save',
|
||||
path: 'server://oxide/config/AutoDoors.json',
|
||||
content: jsonString,
|
||||
},
|
||||
30000,
|
||||
// Write AutoDoors.json via Rust agent
|
||||
await this.instancesService.writeFileForLicense(
|
||||
licenseId,
|
||||
'oxide/config/AutoDoors.json',
|
||||
jsonString,
|
||||
);
|
||||
|
||||
// Reload AutoDoors plugin via RCON
|
||||
await this.natsService.publish(
|
||||
`corrosion.${licenseId}.cmd.server`,
|
||||
{
|
||||
action: 'command',
|
||||
command: 'oxide.reload AutoDoors',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
await this.instancesService.rconForLicense(licenseId, 'oxide.reload AutoDoors');
|
||||
|
||||
// Mark this config as active, deactivate others
|
||||
await this.autoDoorsRepo.update({ license_id: licenseId }, { is_active: false });
|
||||
@@ -126,17 +115,13 @@ export class AutoDoorsService {
|
||||
/** Import AutoDoors.json from game server via NATS */
|
||||
async importFromServer(licenseId: string, configName: string, description?: string) {
|
||||
try {
|
||||
// Read AutoDoors.json from server via file manager NATS
|
||||
const response = await this.natsService.request(
|
||||
`corrosion.${licenseId}.files.cmd`,
|
||||
{
|
||||
func: 'fm_preview',
|
||||
path: 'server://oxide/config/AutoDoors.json',
|
||||
},
|
||||
30000,
|
||||
// Read AutoDoors.json from server via Rust agent
|
||||
const result = await this.instancesService.readFileForLicense(
|
||||
licenseId,
|
||||
'oxide/config/AutoDoors.json',
|
||||
);
|
||||
|
||||
if (!response) {
|
||||
if (!result) {
|
||||
throw new HttpException(
|
||||
'No response from agent — it may be offline',
|
||||
HttpStatus.SERVICE_UNAVAILABLE,
|
||||
@@ -144,13 +129,13 @@ export class AutoDoorsService {
|
||||
}
|
||||
|
||||
// Parse the response content as JSON
|
||||
const responseData = response as Record<string, any>;
|
||||
const responseData = (result as any).content;
|
||||
let configData: Record<string, any>;
|
||||
|
||||
if (typeof responseData.content === 'string') {
|
||||
configData = JSON.parse(responseData.content);
|
||||
} else if (typeof responseData.content === 'object') {
|
||||
configData = responseData.content;
|
||||
if (typeof responseData === 'string') {
|
||||
configData = JSON.parse(responseData);
|
||||
} else if (typeof responseData === 'object') {
|
||||
configData = responseData;
|
||||
} else {
|
||||
throw new HttpException(
|
||||
'Unexpected response format from agent',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable, Logger, NotFoundException, HttpException, HttpStatus } from
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { BetterChatConfig } from '../../entities/betterchat-config.entity';
|
||||
import { NatsService } from '../../services/nats.service';
|
||||
import { InstancesService } from '../instances/instances.service';
|
||||
import { CreateBetterChatConfigDto } from './dto/create-betterchat-config.dto';
|
||||
import { UpdateBetterChatConfigDto } from './dto/update-betterchat-config.dto';
|
||||
|
||||
@@ -13,7 +13,7 @@ export class BetterChatService {
|
||||
constructor(
|
||||
@InjectRepository(BetterChatConfig)
|
||||
private readonly repo: Repository<BetterChatConfig>,
|
||||
private readonly natsService: NatsService,
|
||||
private readonly instancesService: InstancesService,
|
||||
) {}
|
||||
|
||||
/** List configs for a license (summaries — no JSONB) */
|
||||
@@ -81,26 +81,15 @@ export class BetterChatService {
|
||||
const jsonString = JSON.stringify(config.config_data, null, 2);
|
||||
|
||||
try {
|
||||
// Write BetterChat.json via file manager NATS
|
||||
await this.natsService.request(
|
||||
`corrosion.${licenseId}.files.cmd`,
|
||||
{
|
||||
func: 'fm_save',
|
||||
path: 'server://oxide/config/BetterChat.json',
|
||||
content: jsonString,
|
||||
},
|
||||
30000,
|
||||
// Write BetterChat.json via Rust agent
|
||||
await this.instancesService.writeFileForLicense(
|
||||
licenseId,
|
||||
'oxide/config/BetterChat.json',
|
||||
jsonString,
|
||||
);
|
||||
|
||||
// Reload BetterChat plugin via RCON
|
||||
await this.natsService.publish(
|
||||
`corrosion.${licenseId}.cmd.server`,
|
||||
{
|
||||
action: 'command',
|
||||
command: 'oxide.reload BetterChat',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
await this.instancesService.rconForLicense(licenseId, 'oxide.reload BetterChat');
|
||||
|
||||
// Mark this config as active, deactivate others
|
||||
await this.repo.update({ license_id: licenseId }, { is_active: false });
|
||||
@@ -126,17 +115,13 @@ export class BetterChatService {
|
||||
/** Import BetterChat.json from game server via NATS */
|
||||
async importFromServer(licenseId: string, configName: string, description?: string) {
|
||||
try {
|
||||
// Read BetterChat.json from server via file manager NATS
|
||||
const response = await this.natsService.request(
|
||||
`corrosion.${licenseId}.files.cmd`,
|
||||
{
|
||||
func: 'fm_preview',
|
||||
path: 'server://oxide/config/BetterChat.json',
|
||||
},
|
||||
30000,
|
||||
// Read BetterChat.json from server via Rust agent
|
||||
const result = await this.instancesService.readFileForLicense(
|
||||
licenseId,
|
||||
'oxide/config/BetterChat.json',
|
||||
);
|
||||
|
||||
if (!response) {
|
||||
if (!result) {
|
||||
throw new HttpException(
|
||||
'No response from agent — it may be offline',
|
||||
HttpStatus.SERVICE_UNAVAILABLE,
|
||||
@@ -144,13 +129,13 @@ export class BetterChatService {
|
||||
}
|
||||
|
||||
// Parse the response content as JSON
|
||||
const responseData = response as Record<string, any>;
|
||||
const responseData = (result as any).content;
|
||||
let configData: Record<string, any>;
|
||||
|
||||
if (typeof responseData.content === 'string') {
|
||||
configData = JSON.parse(responseData.content);
|
||||
} else if (typeof responseData.content === 'object') {
|
||||
configData = responseData.content;
|
||||
if (typeof responseData === 'string') {
|
||||
configData = JSON.parse(responseData);
|
||||
} else if (typeof responseData === 'object') {
|
||||
configData = responseData;
|
||||
} else {
|
||||
throw new HttpException(
|
||||
'Unexpected response format from agent',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable, Logger, NotFoundException, HttpException, HttpStatus } from
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { FurnaceSplitterConfig } from '../../entities/furnacesplitter-config.entity';
|
||||
import { NatsService } from '../../services/nats.service';
|
||||
import { InstancesService } from '../instances/instances.service';
|
||||
import { CreateFurnaceSplitterConfigDto } from './dto/create-furnacesplitter-config.dto';
|
||||
import { UpdateFurnaceSplitterConfigDto } from './dto/update-furnacesplitter-config.dto';
|
||||
|
||||
@@ -13,7 +13,7 @@ export class FurnaceSplitterService {
|
||||
constructor(
|
||||
@InjectRepository(FurnaceSplitterConfig)
|
||||
private readonly furnaceRepo: Repository<FurnaceSplitterConfig>,
|
||||
private readonly natsService: NatsService,
|
||||
private readonly instancesService: InstancesService,
|
||||
) {}
|
||||
|
||||
/** List configs for a license (summaries — no JSONB) */
|
||||
@@ -81,26 +81,15 @@ export class FurnaceSplitterService {
|
||||
const jsonString = JSON.stringify(config.config_data, null, 2);
|
||||
|
||||
try {
|
||||
// Write FurnaceSplitter.json via file manager NATS
|
||||
await this.natsService.request(
|
||||
`corrosion.${licenseId}.files.cmd`,
|
||||
{
|
||||
func: 'fm_save',
|
||||
path: 'server://oxide/config/FurnaceSplitter.json',
|
||||
content: jsonString,
|
||||
},
|
||||
30000,
|
||||
// Write FurnaceSplitter.json via Rust agent
|
||||
await this.instancesService.writeFileForLicense(
|
||||
licenseId,
|
||||
'oxide/config/FurnaceSplitter.json',
|
||||
jsonString,
|
||||
);
|
||||
|
||||
// Reload FurnaceSplitter plugin via RCON
|
||||
await this.natsService.publish(
|
||||
`corrosion.${licenseId}.cmd.server`,
|
||||
{
|
||||
action: 'command',
|
||||
command: 'oxide.reload FurnaceSplitter',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
await this.instancesService.rconForLicense(licenseId, 'oxide.reload FurnaceSplitter');
|
||||
|
||||
// Mark this config as active, deactivate others
|
||||
await this.furnaceRepo.update({ license_id: licenseId }, { is_active: false });
|
||||
@@ -126,17 +115,13 @@ export class FurnaceSplitterService {
|
||||
/** Import FurnaceSplitter.json from game server via NATS */
|
||||
async importFromServer(licenseId: string, configName: string, description?: string) {
|
||||
try {
|
||||
// Read FurnaceSplitter.json from server via file manager NATS
|
||||
const response = await this.natsService.request(
|
||||
`corrosion.${licenseId}.files.cmd`,
|
||||
{
|
||||
func: 'fm_preview',
|
||||
path: 'server://oxide/config/FurnaceSplitter.json',
|
||||
},
|
||||
30000,
|
||||
// Read FurnaceSplitter.json from server via Rust agent
|
||||
const result = await this.instancesService.readFileForLicense(
|
||||
licenseId,
|
||||
'oxide/config/FurnaceSplitter.json',
|
||||
);
|
||||
|
||||
if (!response) {
|
||||
if (!result) {
|
||||
throw new HttpException(
|
||||
'No response from agent — it may be offline',
|
||||
HttpStatus.SERVICE_UNAVAILABLE,
|
||||
@@ -144,13 +129,13 @@ export class FurnaceSplitterService {
|
||||
}
|
||||
|
||||
// Parse the response content as JSON
|
||||
const responseData = response as Record<string, any>;
|
||||
const responseData = (result as any).content;
|
||||
let configData: Record<string, any>;
|
||||
|
||||
if (typeof responseData.content === 'string') {
|
||||
configData = JSON.parse(responseData.content);
|
||||
} else if (typeof responseData.content === 'object') {
|
||||
configData = responseData.content;
|
||||
if (typeof responseData === 'string') {
|
||||
configData = JSON.parse(responseData);
|
||||
} else if (typeof responseData === 'object') {
|
||||
configData = responseData;
|
||||
} else {
|
||||
throw new HttpException(
|
||||
'Unexpected response format from agent',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable, Logger, NotFoundException, HttpException, HttpStatus } from
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { GatherConfig } from '../../entities/gather-config.entity';
|
||||
import { NatsService } from '../../services/nats.service';
|
||||
import { InstancesService } from '../instances/instances.service';
|
||||
import { CreateGatherConfigDto } from './dto/create-gather-config.dto';
|
||||
import { UpdateGatherConfigDto } from './dto/update-gather-config.dto';
|
||||
|
||||
@@ -13,7 +13,7 @@ export class GatherService {
|
||||
constructor(
|
||||
@InjectRepository(GatherConfig)
|
||||
private readonly gatherRepo: Repository<GatherConfig>,
|
||||
private readonly natsService: NatsService,
|
||||
private readonly instancesService: InstancesService,
|
||||
) {}
|
||||
|
||||
/** List configs for a license (summaries — no JSONB) */
|
||||
@@ -81,26 +81,15 @@ export class GatherService {
|
||||
const jsonString = JSON.stringify(config.config_data, null, 2);
|
||||
|
||||
try {
|
||||
// Write GatherManager.json via file manager NATS
|
||||
await this.natsService.request(
|
||||
`corrosion.${licenseId}.files.cmd`,
|
||||
{
|
||||
func: 'fm_save',
|
||||
path: 'server://oxide/config/GatherManager.json',
|
||||
content: jsonString,
|
||||
},
|
||||
30000,
|
||||
// Write GatherManager.json via Rust agent
|
||||
await this.instancesService.writeFileForLicense(
|
||||
licenseId,
|
||||
'oxide/config/GatherManager.json',
|
||||
jsonString,
|
||||
);
|
||||
|
||||
// Reload GatherManager plugin via RCON
|
||||
await this.natsService.publish(
|
||||
`corrosion.${licenseId}.cmd.server`,
|
||||
{
|
||||
action: 'command',
|
||||
command: 'oxide.reload GatherManager',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
await this.instancesService.rconForLicense(licenseId, 'oxide.reload GatherManager');
|
||||
|
||||
// Mark this config as active, deactivate others
|
||||
await this.gatherRepo.update({ license_id: licenseId }, { is_active: false });
|
||||
@@ -126,17 +115,13 @@ export class GatherService {
|
||||
/** Import GatherManager.json from game server via NATS */
|
||||
async importFromServer(licenseId: string, configName: string, description?: string) {
|
||||
try {
|
||||
// Read GatherManager.json from server via file manager NATS
|
||||
const response = await this.natsService.request(
|
||||
`corrosion.${licenseId}.files.cmd`,
|
||||
{
|
||||
func: 'fm_preview',
|
||||
path: 'server://oxide/config/GatherManager.json',
|
||||
},
|
||||
30000,
|
||||
// Read GatherManager.json from server via Rust agent
|
||||
const result = await this.instancesService.readFileForLicense(
|
||||
licenseId,
|
||||
'oxide/config/GatherManager.json',
|
||||
);
|
||||
|
||||
if (!response) {
|
||||
if (!result) {
|
||||
throw new HttpException(
|
||||
'No response from agent — it may be offline',
|
||||
HttpStatus.SERVICE_UNAVAILABLE,
|
||||
@@ -144,13 +129,13 @@ export class GatherService {
|
||||
}
|
||||
|
||||
// Parse the response content as JSON
|
||||
const responseData = response as Record<string, any>;
|
||||
const responseData = (result as any).content;
|
||||
let configData: Record<string, any>;
|
||||
|
||||
if (typeof responseData.content === 'string') {
|
||||
configData = JSON.parse(responseData.content);
|
||||
} else if (typeof responseData.content === 'object') {
|
||||
configData = responseData.content;
|
||||
if (typeof responseData === 'string') {
|
||||
configData = JSON.parse(responseData);
|
||||
} else if (typeof responseData === 'object') {
|
||||
configData = responseData;
|
||||
} else {
|
||||
throw new HttpException(
|
||||
'Unexpected response format from agent',
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { InstancesController } from './instances.controller';
|
||||
import { InstancesService } from './instances.service';
|
||||
import { GameInstance } from '../../entities/game-instance.entity';
|
||||
import { NatsService } from '../../services/nats.service';
|
||||
|
||||
// Global so the legacy single-server services (servers/players/schedules/wipes/
|
||||
// plugins + the 9 plugin-config modules) can inject InstancesService to route
|
||||
// commands at the now-only Rust agent without each importing this module.
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([GameInstance])],
|
||||
controllers: [InstancesController],
|
||||
providers: [InstancesService, NatsService],
|
||||
exports: [InstancesService],
|
||||
})
|
||||
export class InstancesModule {}
|
||||
|
||||
@@ -142,4 +142,82 @@ export class InstancesService {
|
||||
if (!path || !dest) throw new BadRequestException('path and dest are required');
|
||||
return (await this.fileOp(licenseId, instanceId, { op: 'copy', path, dest })).data ?? { ok: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Wipe an instance's game data via the agent's jailed wipe handler: stop →
|
||||
* delete files per wipe_type (map/blueprint/full) → restart. Long timeout
|
||||
* because the agent does all three steps before replying.
|
||||
*/
|
||||
async wipe(
|
||||
licenseId: string,
|
||||
instanceId: string,
|
||||
wipeType: 'map' | 'blueprint' | 'full',
|
||||
backup = true,
|
||||
): Promise<unknown> {
|
||||
const inst = await this.resolveInstance(licenseId, instanceId);
|
||||
const subject = `corrosion.${licenseId}.${inst.agent_instance_id}.cmd`;
|
||||
this.logger.log(`instance ${inst.agent_instance_id}: wipe (${wipeType})`);
|
||||
return this.nats.requestScoped(
|
||||
licenseId,
|
||||
subject,
|
||||
{ func: 'wipe', wipe_type: wipeType, backup },
|
||||
120_000,
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// License-scoped convenience wrappers. Legacy single-server services
|
||||
// (servers/players/schedules/wipes/plugins + the 9 plugin-config modules)
|
||||
// predate the instance model and carry only a licenseId. These resolve the
|
||||
// license's primary instance, then dispatch to the agent — replacing the old
|
||||
// publishes to the now-defunct `cmd.server` subject.
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/** The license's primary (oldest) instance. Throws if none is connected. */
|
||||
async resolveDefaultInstance(licenseId: string): Promise<GameInstance> {
|
||||
const inst = await this.instanceRepo.findOne({
|
||||
where: { license_id: licenseId },
|
||||
order: { created_at: 'ASC' },
|
||||
});
|
||||
if (!inst) {
|
||||
throw new NotFoundException(
|
||||
'No game instance is connected for this license yet — install and start the host agent first.',
|
||||
);
|
||||
}
|
||||
return inst;
|
||||
}
|
||||
|
||||
async lifecycleForLicense(licenseId: string, func: LifecycleFunc): Promise<unknown> {
|
||||
const inst = await this.resolveDefaultInstance(licenseId);
|
||||
return this.lifecycle(licenseId, inst.id, func);
|
||||
}
|
||||
|
||||
async rconForLicense(licenseId: string, command: string): Promise<unknown> {
|
||||
const inst = await this.resolveDefaultInstance(licenseId);
|
||||
return this.rcon(licenseId, inst.id, command);
|
||||
}
|
||||
|
||||
async writeFileForLicense(licenseId: string, path: string, content: string): Promise<unknown> {
|
||||
const inst = await this.resolveDefaultInstance(licenseId);
|
||||
return this.writeFile(licenseId, inst.id, path, content);
|
||||
}
|
||||
|
||||
async readFileForLicense(licenseId: string, path: string): Promise<unknown> {
|
||||
const inst = await this.resolveDefaultInstance(licenseId);
|
||||
return this.readFile(licenseId, inst.id, path);
|
||||
}
|
||||
|
||||
async deleteFileForLicense(licenseId: string, path: string): Promise<unknown> {
|
||||
const inst = await this.resolveDefaultInstance(licenseId);
|
||||
return this.deleteFile(licenseId, inst.id, path);
|
||||
}
|
||||
|
||||
async wipeForLicense(
|
||||
licenseId: string,
|
||||
wipeType: 'map' | 'blueprint' | 'full',
|
||||
backup = true,
|
||||
): Promise<unknown> {
|
||||
const inst = await this.resolveDefaultInstance(licenseId);
|
||||
return this.wipe(licenseId, inst.id, wipeType, backup);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable, Logger, NotFoundException, HttpException, HttpStatus } from
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { KitsConfig } from '../../entities/kits-config.entity';
|
||||
import { NatsService } from '../../services/nats.service';
|
||||
import { InstancesService } from '../instances/instances.service';
|
||||
import { CreateKitsConfigDto } from './dto/create-kits-config.dto';
|
||||
import { UpdateKitsConfigDto } from './dto/update-kits-config.dto';
|
||||
|
||||
@@ -13,7 +13,7 @@ export class KitsService {
|
||||
constructor(
|
||||
@InjectRepository(KitsConfig)
|
||||
private readonly kitsRepo: Repository<KitsConfig>,
|
||||
private readonly natsService: NatsService,
|
||||
private readonly instancesService: InstancesService,
|
||||
) {}
|
||||
|
||||
/** List configs for a license (summaries — no JSONB) */
|
||||
@@ -81,26 +81,15 @@ export class KitsService {
|
||||
const jsonString = JSON.stringify(config.config_data, null, 2);
|
||||
|
||||
try {
|
||||
// Write Kits.json via file manager NATS
|
||||
await this.natsService.request(
|
||||
`corrosion.${licenseId}.files.cmd`,
|
||||
{
|
||||
func: 'fm_save',
|
||||
path: 'server://oxide/config/Kits.json',
|
||||
content: jsonString,
|
||||
},
|
||||
30000,
|
||||
// Write Kits.json via Rust agent
|
||||
await this.instancesService.writeFileForLicense(
|
||||
licenseId,
|
||||
'oxide/config/Kits.json',
|
||||
jsonString,
|
||||
);
|
||||
|
||||
// Reload Kits plugin via RCON
|
||||
await this.natsService.publish(
|
||||
`corrosion.${licenseId}.cmd.server`,
|
||||
{
|
||||
action: 'command',
|
||||
command: 'oxide.reload Kits',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
await this.instancesService.rconForLicense(licenseId, 'oxide.reload Kits');
|
||||
|
||||
// Mark this config as active, deactivate others
|
||||
await this.kitsRepo.update({ license_id: licenseId }, { is_active: false });
|
||||
@@ -126,17 +115,13 @@ export class KitsService {
|
||||
/** Import Kits.json from game server via NATS */
|
||||
async importFromServer(licenseId: string, configName: string, description?: string) {
|
||||
try {
|
||||
// Read Kits.json from server via file manager NATS
|
||||
const response = await this.natsService.request(
|
||||
`corrosion.${licenseId}.files.cmd`,
|
||||
{
|
||||
func: 'fm_preview',
|
||||
path: 'server://oxide/config/Kits.json',
|
||||
},
|
||||
30000,
|
||||
// Read Kits.json from server via Rust agent
|
||||
const result = await this.instancesService.readFileForLicense(
|
||||
licenseId,
|
||||
'oxide/config/Kits.json',
|
||||
);
|
||||
|
||||
if (!response) {
|
||||
if (!result) {
|
||||
throw new HttpException(
|
||||
'No response from agent — it may be offline',
|
||||
HttpStatus.SERVICE_UNAVAILABLE,
|
||||
@@ -144,13 +129,13 @@ export class KitsService {
|
||||
}
|
||||
|
||||
// Parse the response content as JSON
|
||||
const responseData = response as Record<string, any>;
|
||||
const responseData = (result as any).content;
|
||||
let configData: Record<string, any>;
|
||||
|
||||
if (typeof responseData.content === 'string') {
|
||||
configData = JSON.parse(responseData.content);
|
||||
} else if (typeof responseData.content === 'object') {
|
||||
configData = responseData.content;
|
||||
if (typeof responseData === 'string') {
|
||||
configData = JSON.parse(responseData);
|
||||
} else if (typeof responseData === 'object') {
|
||||
configData = responseData;
|
||||
} else {
|
||||
throw new HttpException(
|
||||
'Unexpected response format from agent',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable, Logger, NotFoundException, HttpException, HttpStatus } from
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { LootProfile } from '../../entities/loot-profile.entity';
|
||||
import { NatsService } from '../../services/nats.service';
|
||||
import { InstancesService } from '../instances/instances.service';
|
||||
import { CreateLootProfileDto } from './dto/create-loot-profile.dto';
|
||||
import { UpdateLootProfileDto } from './dto/update-loot-profile.dto';
|
||||
import { ImportLootProfileDto } from './dto/import-loot-profile.dto';
|
||||
@@ -15,7 +15,7 @@ export class LootService {
|
||||
constructor(
|
||||
@InjectRepository(LootProfile)
|
||||
private readonly lootRepo: Repository<LootProfile>,
|
||||
private readonly natsService: NatsService,
|
||||
private readonly instancesService: InstancesService,
|
||||
) {}
|
||||
|
||||
/** List profiles for a license (summaries — no JSONB) */
|
||||
@@ -114,37 +114,22 @@ export class LootService {
|
||||
const lootGroupsJson = JSON.stringify(scaledGroups, null, 2);
|
||||
|
||||
try {
|
||||
// Write LootTables.json via file manager NATS
|
||||
await this.natsService.request(
|
||||
`corrosion.${licenseId}.files.cmd`,
|
||||
{
|
||||
func: 'fm_save',
|
||||
path: 'server://oxide/data/BetterLoot/LootTables.json',
|
||||
content: lootTablesJson,
|
||||
},
|
||||
30000,
|
||||
// Write LootTables.json via Rust agent
|
||||
await this.instancesService.writeFileForLicense(
|
||||
licenseId,
|
||||
'oxide/data/BetterLoot/LootTables.json',
|
||||
lootTablesJson,
|
||||
);
|
||||
|
||||
// Write LootGroups.json via file manager NATS
|
||||
await this.natsService.request(
|
||||
`corrosion.${licenseId}.files.cmd`,
|
||||
{
|
||||
func: 'fm_save',
|
||||
path: 'server://oxide/data/BetterLoot/LootGroups.json',
|
||||
content: lootGroupsJson,
|
||||
},
|
||||
30000,
|
||||
// Write LootGroups.json via Rust agent
|
||||
await this.instancesService.writeFileForLicense(
|
||||
licenseId,
|
||||
'oxide/data/BetterLoot/LootGroups.json',
|
||||
lootGroupsJson,
|
||||
);
|
||||
|
||||
// Reload BetterLoot plugin via RCON
|
||||
await this.natsService.publish(
|
||||
`corrosion.${licenseId}.cmd.server`,
|
||||
{
|
||||
action: 'command',
|
||||
command: 'oxide.reload BetterLoot',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
await this.instancesService.rconForLicense(licenseId, 'oxide.reload BetterLoot');
|
||||
|
||||
// Mark this profile as active, deactivate others
|
||||
await this.lootRepo.update({ license_id: licenseId }, { is_active: false });
|
||||
|
||||
@@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { PlayerAction } from '../../entities/player-action.entity';
|
||||
import { PlayerSession } from '../../entities/player-session.entity';
|
||||
import { NatsService } from '../../services/nats.service';
|
||||
import { InstancesService } from '../instances/instances.service';
|
||||
import { PlayerActionDto } from './dto/player-action.dto';
|
||||
|
||||
export interface Player {
|
||||
@@ -23,7 +23,7 @@ export class PlayersService {
|
||||
private readonly actionRepo: Repository<PlayerAction>,
|
||||
@InjectRepository(PlayerSession)
|
||||
private readonly sessionRepo: Repository<PlayerSession>,
|
||||
private readonly natsService: NatsService,
|
||||
private readonly instancesService: InstancesService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -132,15 +132,26 @@ export class PlayersService {
|
||||
|
||||
await this.actionRepo.save(action);
|
||||
|
||||
// Forward kick, ban, and unban to the game server via NATS
|
||||
// Forward kick, ban, and unban to the game server via RCON
|
||||
if (dto.action_type === 'kick' || dto.action_type === 'ban' || dto.action_type === 'unban') {
|
||||
await this.natsService.sendServerCommand(licenseId, dto.action_type, {
|
||||
steam_id: dto.steam_id,
|
||||
reason: dto.reason,
|
||||
duration_minutes: dto.duration_minutes,
|
||||
});
|
||||
const rconCmd = this.buildRconCommand(dto);
|
||||
await this.instancesService.rconForLicense(licenseId, rconCmd);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
private buildRconCommand(dto: PlayerActionDto): string {
|
||||
switch (dto.action_type) {
|
||||
case 'kick':
|
||||
return `kick ${dto.steam_id}${dto.reason ? ' ' + dto.reason : ''}`;
|
||||
case 'ban':
|
||||
// banid <steamId> <reason> <durationSeconds> — 0 = permanent
|
||||
return `banid ${dto.steam_id} ${dto.reason ?? 'banned'} ${dto.duration_minutes ? dto.duration_minutes * 60 : 0}`;
|
||||
case 'unban':
|
||||
return `unban ${dto.steam_id}`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Injectable, NotFoundException, ConflictException, BadRequestException, Logger } from '@nestjs/common';
|
||||
import { Injectable, NotFoundException, ConflictException, BadRequestException, ServiceUnavailableException, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { PluginRegistry } from '../../entities/plugin-registry.entity';
|
||||
import { InstallPluginDto } from './dto/install-plugin.dto';
|
||||
import { UpdatePluginConfigDto } from './dto/update-plugin-config.dto';
|
||||
import { NatsService } from '../../services/nats.service';
|
||||
import { InstancesService } from '../instances/instances.service';
|
||||
|
||||
interface UmodCacheEntry {
|
||||
data: unknown;
|
||||
@@ -20,7 +20,7 @@ export class PluginsService {
|
||||
constructor(
|
||||
@InjectRepository(PluginRegistry)
|
||||
private readonly pluginRegistryRepo: Repository<PluginRegistry>,
|
||||
private readonly natsService: NatsService,
|
||||
private readonly instancesService: InstancesService,
|
||||
) {}
|
||||
|
||||
async getPlugins(licenseId: string): Promise<PluginRegistry[]> {
|
||||
@@ -43,30 +43,11 @@ export class PluginsService {
|
||||
throw new ConflictException(`Plugin ${dto.plugin_name} is already installed`);
|
||||
}
|
||||
|
||||
const plugin = this.pluginRegistryRepo.create({
|
||||
license_id: licenseId,
|
||||
plugin_name: dto.plugin_name,
|
||||
umod_slug: dto.umod_slug,
|
||||
source: dto.source || 'manual',
|
||||
is_installed: true,
|
||||
is_loaded: false,
|
||||
});
|
||||
|
||||
const saved = await this.pluginRegistryRepo.save(plugin);
|
||||
|
||||
try {
|
||||
await this.natsService.publish(`corrosion.${licenseId}.cmd.server`, {
|
||||
action: 'plugin_install',
|
||||
plugin_name: dto.plugin_name,
|
||||
umod_slug: dto.umod_slug,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
this.logger.log(`Plugin install dispatched for ${dto.plugin_name} on license ${licenseId}`);
|
||||
} catch (err) {
|
||||
this.logger.error(`Failed to dispatch plugin install for ${dto.plugin_name} on license ${licenseId}: ${(err as Error).message}`);
|
||||
}
|
||||
|
||||
return saved;
|
||||
// One-click uMod install via agent is not yet implemented.
|
||||
// Fail fast — do not persist a DB record for a plugin that won't be deployed.
|
||||
throw new ServiceUnavailableException(
|
||||
'One-click uMod install is coming soon — download the .cs and use Upload for now.',
|
||||
);
|
||||
}
|
||||
|
||||
async uninstallPlugin(licenseId: string, pluginId: string): Promise<void> {
|
||||
@@ -80,11 +61,8 @@ export class PluginsService {
|
||||
|
||||
await this.pluginRegistryRepo.delete({ id: pluginId, license_id: licenseId });
|
||||
|
||||
await this.natsService.publish(`corrosion.${licenseId}.cmd.plugin`, {
|
||||
action: 'unload',
|
||||
plugin_name: plugin.plugin_name,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
await this.instancesService.rconForLicense(licenseId, `oxide.unload ${plugin.plugin_name}`);
|
||||
await this.instancesService.deleteFileForLicense(licenseId, `oxide/plugins/${plugin.plugin_name}.cs`);
|
||||
this.logger.log(`Plugin uninstall dispatched for ${plugin.plugin_name} on license ${licenseId}`);
|
||||
}
|
||||
|
||||
@@ -100,11 +78,7 @@ export class PluginsService {
|
||||
throw new NotFoundException(`Plugin ${pluginId} not found`);
|
||||
}
|
||||
|
||||
await this.natsService.publish(`corrosion.${licenseId}.cmd.plugin`, {
|
||||
action: 'reload',
|
||||
plugin_name: plugin.plugin_name,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
await this.instancesService.rconForLicense(licenseId, `oxide.reload ${plugin.plugin_name}`);
|
||||
this.logger.log(`Plugin reload dispatched for ${plugin.plugin_name} on license ${licenseId}`);
|
||||
|
||||
return { reloaded: true, plugin_name: plugin.plugin_name };
|
||||
@@ -215,19 +189,14 @@ export class PluginsService {
|
||||
|
||||
const saved = await this.pluginRegistryRepo.save(plugin);
|
||||
|
||||
// Dispatch to companion agent via NATS
|
||||
// Deploy .cs file to server via host agent
|
||||
try {
|
||||
const content = file.buffer.toString('base64');
|
||||
await this.natsService.publish(`corrosion.${licenseId}.cmd.server`, {
|
||||
action: 'plugin_upload',
|
||||
filename: originalName,
|
||||
content,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
this.logger.log(`Plugin upload dispatched: "${originalName}" (${file.size} bytes) for license ${licenseId}`);
|
||||
const content = file.buffer.toString('utf8');
|
||||
await this.instancesService.writeFileForLicense(licenseId, `oxide/plugins/${originalName}`, content);
|
||||
this.logger.log(`Plugin upload deployed: "${originalName}" (${file.size} bytes) for license ${licenseId}`);
|
||||
} catch (err) {
|
||||
this.logger.error(`NATS publish failed for plugin upload "${originalName}" on license ${licenseId}: ${(err as Error).message}`);
|
||||
// Don't fail the request — plugin record is saved, NATS delivery is best-effort
|
||||
this.logger.error(`File write failed for plugin upload "${originalName}" on license ${licenseId}: ${(err as Error).message}`);
|
||||
// Don't fail the request — plugin record is saved, file delivery is best-effort
|
||||
}
|
||||
|
||||
return saved;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable, Logger, NotFoundException, HttpException, HttpStatus } from
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { RaidableBasesConfig } from '../../entities/raidablebases-config.entity';
|
||||
import { NatsService } from '../../services/nats.service';
|
||||
import { InstancesService } from '../instances/instances.service';
|
||||
import { CreateRaidableBasesConfigDto } from './dto/create-raidablebases-config.dto';
|
||||
import { UpdateRaidableBasesConfigDto } from './dto/update-raidablebases-config.dto';
|
||||
|
||||
@@ -13,7 +13,7 @@ export class RaidableBasesService {
|
||||
constructor(
|
||||
@InjectRepository(RaidableBasesConfig)
|
||||
private readonly raidableBasesRepo: Repository<RaidableBasesConfig>,
|
||||
private readonly natsService: NatsService,
|
||||
private readonly instancesService: InstancesService,
|
||||
) {}
|
||||
|
||||
/** List configs for a license (summaries — no JSONB) */
|
||||
@@ -81,26 +81,15 @@ export class RaidableBasesService {
|
||||
const jsonString = JSON.stringify(config.config_data, null, 2);
|
||||
|
||||
try {
|
||||
// Write RaidableBases.json via file manager NATS
|
||||
await this.natsService.request(
|
||||
`corrosion.${licenseId}.files.cmd`,
|
||||
{
|
||||
func: 'fm_save',
|
||||
path: 'server://oxide/config/RaidableBases.json',
|
||||
content: jsonString,
|
||||
},
|
||||
30000,
|
||||
// Write RaidableBases.json via Rust agent
|
||||
await this.instancesService.writeFileForLicense(
|
||||
licenseId,
|
||||
'oxide/config/RaidableBases.json',
|
||||
jsonString,
|
||||
);
|
||||
|
||||
// Reload RaidableBases plugin via RCON
|
||||
await this.natsService.publish(
|
||||
`corrosion.${licenseId}.cmd.server`,
|
||||
{
|
||||
action: 'command',
|
||||
command: 'oxide.reload RaidableBases',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
await this.instancesService.rconForLicense(licenseId, 'oxide.reload RaidableBases');
|
||||
|
||||
// Mark this config as active, deactivate others
|
||||
await this.raidableBasesRepo.update({ license_id: licenseId }, { is_active: false });
|
||||
@@ -126,17 +115,13 @@ export class RaidableBasesService {
|
||||
/** Import RaidableBases.json from game server via NATS */
|
||||
async importFromServer(licenseId: string, configName: string, description?: string) {
|
||||
try {
|
||||
// Read RaidableBases.json from server via file manager NATS
|
||||
const response = await this.natsService.request(
|
||||
`corrosion.${licenseId}.files.cmd`,
|
||||
{
|
||||
func: 'fm_preview',
|
||||
path: 'server://oxide/config/RaidableBases.json',
|
||||
},
|
||||
30000,
|
||||
// Read RaidableBases.json from server via Rust agent
|
||||
const result = await this.instancesService.readFileForLicense(
|
||||
licenseId,
|
||||
'oxide/config/RaidableBases.json',
|
||||
);
|
||||
|
||||
if (!response) {
|
||||
if (!result) {
|
||||
throw new HttpException(
|
||||
'No response from agent — it may be offline',
|
||||
HttpStatus.SERVICE_UNAVAILABLE,
|
||||
@@ -144,13 +129,13 @@ export class RaidableBasesService {
|
||||
}
|
||||
|
||||
// Parse the response content as JSON
|
||||
const responseData = response as Record<string, any>;
|
||||
const responseData = (result as any).content;
|
||||
let configData: Record<string, any>;
|
||||
|
||||
if (typeof responseData.content === 'string') {
|
||||
configData = JSON.parse(responseData.content);
|
||||
} else if (typeof responseData.content === 'object') {
|
||||
configData = responseData.content;
|
||||
if (typeof responseData === 'string') {
|
||||
configData = JSON.parse(responseData);
|
||||
} else if (typeof responseData === 'object') {
|
||||
configData = responseData;
|
||||
} else {
|
||||
throw new HttpException(
|
||||
'Unexpected response format from agent',
|
||||
|
||||
@@ -10,7 +10,7 @@ import { LessThanOrEqual, Repository } from 'typeorm';
|
||||
import { ScheduledTask } from '../../entities/scheduled-task.entity';
|
||||
import { CreateTaskDto } from './dto/create-task.dto';
|
||||
import { UpdateTaskDto } from './dto/update-task.dto';
|
||||
import { NatsService } from '../../services/nats.service';
|
||||
import { InstancesService } from '../instances/instances.service';
|
||||
|
||||
/** Parse a 5-field cron expression and return the next Date after `after`. */
|
||||
function nextCronDate(expr: string, after: Date): Date | null {
|
||||
@@ -61,7 +61,7 @@ export class SchedulesService implements OnModuleInit, OnModuleDestroy {
|
||||
constructor(
|
||||
@InjectRepository(ScheduledTask)
|
||||
private taskRepository: Repository<ScheduledTask>,
|
||||
private readonly natsService: NatsService,
|
||||
private readonly instancesService: InstancesService,
|
||||
) {}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -160,21 +160,12 @@ export class SchedulesService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
switch (task_type) {
|
||||
case 'restart':
|
||||
await this.natsService.sendServerCommand(license_id, 'restart', {
|
||||
source: 'scheduler',
|
||||
task_id: task.id,
|
||||
});
|
||||
await this.instancesService.lifecycleForLicense(license_id, 'restart');
|
||||
break;
|
||||
|
||||
case 'announcement': {
|
||||
const message = (task_config?.message as string) ?? 'Scheduled announcement';
|
||||
await this.natsService.publish(`corrosion.${license_id}.cmd.server`, {
|
||||
action: 'command',
|
||||
command: `say ${message}`,
|
||||
source: 'scheduler',
|
||||
task_id: task.id,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
await this.instancesService.rconForLicense(license_id, `say ${message}`);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -184,25 +175,13 @@ export class SchedulesService implements OnModuleInit, OnModuleDestroy {
|
||||
this.logger.warn(`Task ${task.id} has no command configured — skipping`);
|
||||
return;
|
||||
}
|
||||
await this.natsService.publish(`corrosion.${license_id}.cmd.server`, {
|
||||
action: 'command',
|
||||
command,
|
||||
source: 'scheduler',
|
||||
task_id: task.id,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
await this.instancesService.rconForLicense(license_id, command);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'plugin_reload': {
|
||||
const plugin_name = (task_config?.plugin_name as string) ?? '';
|
||||
await this.natsService.publish(`corrosion.${license_id}.cmd.plugin`, {
|
||||
action: 'reload',
|
||||
plugin_name,
|
||||
source: 'scheduler',
|
||||
task_id: task.id,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
await this.instancesService.rconForLicense(license_id, `oxide.reload ${plugin_name}`);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Injectable, NotFoundException, InternalServerErrorException, Logger } from '@nestjs/common';
|
||||
import { Injectable, NotFoundException, InternalServerErrorException, ServiceUnavailableException, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ServerConnection } from '../../entities/server-connection.entity';
|
||||
import { ServerConfig } from '../../entities/server-config.entity';
|
||||
import { NatsService } from '../../services/nats.service';
|
||||
import { InstancesService } from '../instances/instances.service';
|
||||
import { UpdateServerConfigDto } from './dto/update-config.dto';
|
||||
import { DeployServerDto } from './dto/deploy-server.dto';
|
||||
|
||||
@@ -17,6 +18,7 @@ export class ServersService {
|
||||
@InjectRepository(ServerConfig)
|
||||
private readonly configRepo: Repository<ServerConfig>,
|
||||
private readonly natsService: NatsService,
|
||||
private readonly instancesService: InstancesService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -68,11 +70,11 @@ export class ServersService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a console command to the server via NATS
|
||||
* Send a console command to the server via the host agent (RCON)
|
||||
*/
|
||||
async sendCommand(licenseId: string, command: string) {
|
||||
try {
|
||||
await this.natsService.sendServerCommand(licenseId, 'command', { command });
|
||||
await this.instancesService.rconForLicense(licenseId, command);
|
||||
this.logger.log(`Console command dispatched for license ${licenseId}: ${command}`);
|
||||
} catch (err) {
|
||||
this.logger.error(`Failed to dispatch console command for license ${licenseId}: ${(err as Error).message}`);
|
||||
@@ -82,42 +84,45 @@ export class ServersService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the server via NATS
|
||||
* Start the server via the host agent
|
||||
*/
|
||||
async startServer(licenseId: string) {
|
||||
await this.natsService.sendServerCommand(licenseId, 'start');
|
||||
await this.instancesService.lifecycleForLicense(licenseId, 'start');
|
||||
return { message: 'Start command sent' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the server via NATS
|
||||
* Stop the server via the host agent
|
||||
*/
|
||||
async stopServer(licenseId: string) {
|
||||
await this.natsService.sendServerCommand(licenseId, 'stop');
|
||||
await this.instancesService.lifecycleForLicense(licenseId, 'stop');
|
||||
return { message: 'Stop command sent' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart the server via NATS
|
||||
* Restart the server via the host agent
|
||||
*/
|
||||
async restartServer(licenseId: string) {
|
||||
await this.natsService.sendServerCommand(licenseId, 'restart');
|
||||
await this.instancesService.lifecycleForLicense(licenseId, 'restart');
|
||||
return { message: 'Restart command sent' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy Rust server via companion agent
|
||||
* Deploy Rust server — not yet supported via host agent.
|
||||
* Install the server manually and point the host agent at it.
|
||||
*/
|
||||
async deployServer(licenseId: string, dto: DeployServerDto) {
|
||||
await this.natsService.sendDeployCommand(licenseId, { ...dto });
|
||||
return { message: 'Deployment started' };
|
||||
async deployServer(_licenseId: string, _dto: DeployServerDto) {
|
||||
throw new ServiceUnavailableException(
|
||||
'Server deployment from the panel is coming soon — install the server and point the host agent at it for now.',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Install Oxide/uMod via companion agent
|
||||
* Install Oxide/uMod — not yet supported via host agent.
|
||||
*/
|
||||
async installOxide(licenseId: string) {
|
||||
await this.natsService.sendOxideInstallCommand(licenseId);
|
||||
return { message: 'Oxide installation started' };
|
||||
async installOxide(_licenseId: string) {
|
||||
throw new ServiceUnavailableException(
|
||||
'Oxide install from the panel is coming soon — install Oxide/uMod on the server for now.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable, Logger, NotFoundException, HttpException, HttpStatus } from
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { TeleportConfig } from '../../entities/teleport-config.entity';
|
||||
import { NatsService } from '../../services/nats.service';
|
||||
import { InstancesService } from '../instances/instances.service';
|
||||
import { CreateTeleportConfigDto } from './dto/create-teleport-config.dto';
|
||||
import { UpdateTeleportConfigDto } from './dto/update-teleport-config.dto';
|
||||
|
||||
@@ -13,7 +13,7 @@ export class TeleportService {
|
||||
constructor(
|
||||
@InjectRepository(TeleportConfig)
|
||||
private readonly teleportRepo: Repository<TeleportConfig>,
|
||||
private readonly natsService: NatsService,
|
||||
private readonly instancesService: InstancesService,
|
||||
) {}
|
||||
|
||||
/** List configs for a license (summaries — no JSONB) */
|
||||
@@ -81,26 +81,15 @@ export class TeleportService {
|
||||
const jsonString = JSON.stringify(config.config_data, null, 2);
|
||||
|
||||
try {
|
||||
// Write NTeleportation.json via file manager NATS
|
||||
await this.natsService.request(
|
||||
`corrosion.${licenseId}.files.cmd`,
|
||||
{
|
||||
func: 'fm_save',
|
||||
path: 'server://oxide/config/NTeleportation.json',
|
||||
content: jsonString,
|
||||
},
|
||||
30000,
|
||||
// Write NTeleportation.json via Rust agent
|
||||
await this.instancesService.writeFileForLicense(
|
||||
licenseId,
|
||||
'oxide/config/NTeleportation.json',
|
||||
jsonString,
|
||||
);
|
||||
|
||||
// Reload NTeleportation plugin via RCON
|
||||
await this.natsService.publish(
|
||||
`corrosion.${licenseId}.cmd.server`,
|
||||
{
|
||||
action: 'command',
|
||||
command: 'oxide.reload NTeleportation',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
await this.instancesService.rconForLicense(licenseId, 'oxide.reload NTeleportation');
|
||||
|
||||
// Mark this config as active, deactivate others
|
||||
await this.teleportRepo.update({ license_id: licenseId }, { is_active: false });
|
||||
@@ -126,17 +115,13 @@ export class TeleportService {
|
||||
/** Import NTeleportation.json from game server via NATS */
|
||||
async importFromServer(licenseId: string, configName: string, description?: string) {
|
||||
try {
|
||||
// Read NTeleportation.json from server via file manager NATS
|
||||
const response = await this.natsService.request(
|
||||
`corrosion.${licenseId}.files.cmd`,
|
||||
{
|
||||
func: 'fm_preview',
|
||||
path: 'server://oxide/config/NTeleportation.json',
|
||||
},
|
||||
30000,
|
||||
// Read NTeleportation.json from server via Rust agent
|
||||
const result = await this.instancesService.readFileForLicense(
|
||||
licenseId,
|
||||
'oxide/config/NTeleportation.json',
|
||||
);
|
||||
|
||||
if (!response) {
|
||||
if (!result) {
|
||||
throw new HttpException(
|
||||
'No response from agent — it may be offline',
|
||||
HttpStatus.SERVICE_UNAVAILABLE,
|
||||
@@ -144,13 +129,13 @@ export class TeleportService {
|
||||
}
|
||||
|
||||
// Parse the response content as JSON
|
||||
const responseData = response as Record<string, any>;
|
||||
const responseData = (result as any).content;
|
||||
let configData: Record<string, any>;
|
||||
|
||||
if (typeof responseData.content === 'string') {
|
||||
configData = JSON.parse(responseData.content);
|
||||
} else if (typeof responseData.content === 'object') {
|
||||
configData = responseData.content;
|
||||
if (typeof responseData === 'string') {
|
||||
configData = JSON.parse(responseData);
|
||||
} else if (typeof responseData === 'object') {
|
||||
configData = responseData;
|
||||
} else {
|
||||
throw new HttpException(
|
||||
'Unexpected response format from agent',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable, Logger, NotFoundException, HttpException, HttpStatus } from
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { TimedExecuteConfig } from '../../entities/timedexecute-config.entity';
|
||||
import { NatsService } from '../../services/nats.service';
|
||||
import { InstancesService } from '../instances/instances.service';
|
||||
import { CreateTimedExecuteConfigDto } from './dto/create-timedexecute-config.dto';
|
||||
import { UpdateTimedExecuteConfigDto } from './dto/update-timedexecute-config.dto';
|
||||
|
||||
@@ -13,7 +13,7 @@ export class TimedExecuteService {
|
||||
constructor(
|
||||
@InjectRepository(TimedExecuteConfig)
|
||||
private readonly repo: Repository<TimedExecuteConfig>,
|
||||
private readonly natsService: NatsService,
|
||||
private readonly instancesService: InstancesService,
|
||||
) {}
|
||||
|
||||
/** List configs for a license (summaries — no JSONB) */
|
||||
@@ -81,26 +81,15 @@ export class TimedExecuteService {
|
||||
const jsonString = JSON.stringify(config.config_data, null, 2);
|
||||
|
||||
try {
|
||||
// Write TimedExecute.json via file manager NATS
|
||||
await this.natsService.request(
|
||||
`corrosion.${licenseId}.files.cmd`,
|
||||
{
|
||||
func: 'fm_save',
|
||||
path: 'server://oxide/config/TimedExecute.json',
|
||||
content: jsonString,
|
||||
},
|
||||
30000,
|
||||
// Write TimedExecute.json via Rust agent
|
||||
await this.instancesService.writeFileForLicense(
|
||||
licenseId,
|
||||
'oxide/config/TimedExecute.json',
|
||||
jsonString,
|
||||
);
|
||||
|
||||
// Reload TimedExecute plugin via RCON
|
||||
await this.natsService.publish(
|
||||
`corrosion.${licenseId}.cmd.server`,
|
||||
{
|
||||
action: 'command',
|
||||
command: 'oxide.reload TimedExecute',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
await this.instancesService.rconForLicense(licenseId, 'oxide.reload TimedExecute');
|
||||
|
||||
// Mark this config as active, deactivate others
|
||||
await this.repo.update({ license_id: licenseId }, { is_active: false });
|
||||
@@ -126,17 +115,13 @@ export class TimedExecuteService {
|
||||
/** Import TimedExecute.json from game server via NATS */
|
||||
async importFromServer(licenseId: string, configName: string, description?: string) {
|
||||
try {
|
||||
// Read TimedExecute.json from server via file manager NATS
|
||||
const response = await this.natsService.request(
|
||||
`corrosion.${licenseId}.files.cmd`,
|
||||
{
|
||||
func: 'fm_preview',
|
||||
path: 'server://oxide/config/TimedExecute.json',
|
||||
},
|
||||
30000,
|
||||
// Read TimedExecute.json from server via Rust agent
|
||||
const result = await this.instancesService.readFileForLicense(
|
||||
licenseId,
|
||||
'oxide/config/TimedExecute.json',
|
||||
);
|
||||
|
||||
if (!response) {
|
||||
if (!result) {
|
||||
throw new HttpException(
|
||||
'No response from agent — it may be offline',
|
||||
HttpStatus.SERVICE_UNAVAILABLE,
|
||||
@@ -144,13 +129,13 @@ export class TimedExecuteService {
|
||||
}
|
||||
|
||||
// Parse the response content as JSON
|
||||
const responseData = response as Record<string, any>;
|
||||
const responseData = (result as any).content;
|
||||
let configData: Record<string, any>;
|
||||
|
||||
if (typeof responseData.content === 'string') {
|
||||
configData = JSON.parse(responseData.content);
|
||||
} else if (typeof responseData.content === 'object') {
|
||||
configData = responseData.content;
|
||||
if (typeof responseData === 'string') {
|
||||
configData = JSON.parse(responseData);
|
||||
} else if (typeof responseData === 'object') {
|
||||
configData = responseData;
|
||||
} else {
|
||||
throw new HttpException(
|
||||
'Unexpected response format from agent',
|
||||
|
||||
@@ -8,7 +8,7 @@ import { CreateProfileDto } from './dto/create-profile.dto';
|
||||
import { UpdateProfileDto } from './dto/update-profile.dto';
|
||||
import { CreateScheduleDto } from './dto/create-schedule.dto';
|
||||
import { TriggerWipeDto } from './dto/trigger-wipe.dto';
|
||||
import { NatsService } from '../../services/nats.service';
|
||||
import { InstancesService } from '../instances/instances.service';
|
||||
|
||||
@Injectable()
|
||||
export class WipesService {
|
||||
@@ -21,7 +21,7 @@ export class WipesService {
|
||||
private readonly wipeScheduleRepo: Repository<WipeSchedule>,
|
||||
@InjectRepository(WipeHistory)
|
||||
private readonly wipeHistoryRepo: Repository<WipeHistory>,
|
||||
private readonly natsService: NatsService,
|
||||
private readonly instancesService: InstancesService,
|
||||
) {}
|
||||
|
||||
async getProfiles(licenseId: string): Promise<WipeProfile[]> {
|
||||
@@ -107,13 +107,7 @@ export class WipesService {
|
||||
|
||||
const saved = await this.wipeHistoryRepo.save(history);
|
||||
|
||||
await this.natsService.publish(`corrosion.${licenseId}.cmd.wipe`, {
|
||||
wipe_history_id: saved.id,
|
||||
wipe_type: dto.wipe_type,
|
||||
wipe_profile_id: dto.wipe_profile_id ?? null,
|
||||
trigger_type: 'manual',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
await this.instancesService.wipeForLicense(licenseId, dto.wipe_type, true);
|
||||
this.logger.log(`Wipe triggered for license ${licenseId} — history id ${saved.id}`);
|
||||
|
||||
return { wipe_history_id: saved.id };
|
||||
|
||||
Reference in New Issue
Block a user