Refacto de l'API

This commit is contained in:
corenthin-lebreton 2025-12-02 20:40:46 +01:00
parent 1202357a32
commit 741da58194
7 changed files with 411 additions and 410 deletions

View File

@ -11,23 +11,23 @@ const envSchema = z.object({
APP_ADMIN_EMAIL: z.string().optional(),
APP_ADMIN_PASSWORD: z.string().optional(),
// Guacamole
GUACAMOLE_API_ENDPOINT: z.string().default('https://guacamole.firewax.fr/guacamole/api'),
GUACAMOLE_USERNAME: z.string().default('guacadmin'),
GUACAMOLE_PASSWORD: z.string().default('guacadmin'),
GUACAMOLE_API_ENDPOINT: z.string().default('https://guacamole.local/guacamole/api'),
GUACAMOLE_USERNAME: z.string().default('user'),
GUACAMOLE_PASSWORD: z.string().default('passsword'),
// Terraform
TERRAFORM_DROP_DIR: z.string().default('terraform_drop'),
TERRAFORM_TEMPLATE_PATH: z.string().default('/home/corenthin/terraform'),
TERRAFORM_REMOTE_TEMPLATE_PATH: z.string().default('/home/corenthin/terraform'),
TERRAFORM_DEPLOY_BASE_PATH: z.string().default('/home/corenthin/'),
TERRAFORM_TEMPLATE_PATH: z.string().default('/home/user/terraform'),
TERRAFORM_REMOTE_TEMPLATE_PATH: z.string().default('/home/user/terraform'),
TERRAFORM_DEPLOY_BASE_PATH: z.string().default('/home/user/'),
TERRAFORM_SSH_HOST: z.string().default('127.0.0.1'),
TERRAFORM_SSH_USER: z.string().default('corenthin'),
TERRAFORM_SSH_KEY_PATH: z.string().default('/home/corenthin/.ssh/testapi'),
TERRAFORM_SSH_USER: z.string().default('user'),
TERRAFORM_SSH_KEY_PATH: z.string().default('/home/user/.ssh/testapi'),
// Proxmox
PROXMOX_URL: z.string().default('https://node1.solyone.fr:8006/api2/json'),
PROXMOX_TOKEN_ID: z.string().default('terraform-prov@pve!mytoken'),
PROXMOX_TOKEN_SECRET: z.string().default('a4b8720c-6e69-4309-ab56-54b62126b6e6'),
PROXMOX_URL: z.string().default('https://localhost:8006/api2/json'),
PROXMOX_TOKEN_ID: z.string().default('tokenid'),
PROXMOX_TOKEN_SECRET: z.string().default('tokensecret'),
PROXMOX_INSECURE_TLS: z.string().default('true'),
PROXMOX_TARGET_NODE: z.string().default('node1'),
PROXMOX_TARGET_NODE: z.string().default('pve'),
PROXMOX_MODEL_WINDOWS: z.string().default('e1000'),
PROXMOX_MODEL_LINUX: z.string().default('virtio'),
// Cors

View File

@ -6,6 +6,7 @@ import * as terraformService from '../services/terraformService';
import * as proxmoxService from '../services/proxmoxService';
import path from 'path';
import logger from '../utils/logger';
import { ApplyRequest } from '../types/terraform';
// Zod schemas
const vmSchema = z.object({
@ -67,7 +68,7 @@ export const apply = async (req: Request, res: Response): Promise<void> => {
}
// Spawn background job
terraformService.handleApplyJob(formationId, body as any).catch(err => {
terraformService.handleApplyJob(formationId, body as unknown as ApplyRequest).catch(err => {
logger.error(`Apply job failed for ${formationId}: ${err}`);
});

View File

@ -1,313 +1,6 @@
import axios, { AxiosInstance } from 'axios';
import https from 'https';
import config from '../config';
import { GuacSession, GuacamoleUser, GuacamoleVm } from '../utils/guacamoleClient';
import logger from '../utils/logger';
interface GuacamoleUser {
username: string;
password?: string;
displayName?: string;
}
interface GuacamoleVm {
name: string;
ip: string;
rdpPort?: number;
rdpDomain?: string;
}
interface TokenResponse {
authToken: string;
dataSource?: string;
availableDataSources?: string[];
}
const DEFAULT_RDP_PORT = 3389;
class GuacSession {
private client: AxiosInstance;
private base: string;
private dataSource: string;
private token: string;
private constructor(client: AxiosInstance, base: string, dataSource: string, token: string) {
this.client = client;
this.base = base;
this.dataSource = dataSource;
this.token = token;
}
static async login(): Promise<GuacSession> {
const { apiEndpoint, username, password } = config.guacamole;
const base = apiEndpoint.replace(/\/$/, '');
const agent = new https.Agent({
rejectUnauthorized: false
});
const client = axios.create({
httpsAgent: agent,
timeout: 20000
});
const params = new URLSearchParams();
params.append('username', username);
params.append('password', password);
try {
const response = await client.post<TokenResponse>(`${base}/tokens`, params, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
const tokenResponse = response.data;
let dataSource = tokenResponse.dataSource;
if (!dataSource && tokenResponse.availableDataSources && tokenResponse.availableDataSources.length > 0) {
dataSource = tokenResponse.availableDataSources[tokenResponse.availableDataSources.length - 1];
}
if (!dataSource) {
throw new Error('Guacamole did not return a data source');
}
return new GuacSession(client, base, dataSource, tokenResponse.authToken);
} catch (error) {
console.error('Guacamole login failed:', error);
throw new Error('Guacamole login failed');
}
}
private get authParams() {
return {
token: this.token,
dataSource: this.dataSource
};
}
async checkExistence(group: string, users: GuacamoleUser[], vms: GuacamoleVm[]): Promise<void> {
// Check Group
try {
await this.client.get(`${this.base}/session/data/${this.dataSource}/userGroups/${encodeURIComponent(group)}`, { params: this.authParams });
// Group exists, which is fine, we'll update/use it
} catch (error: any) {
// If 404, it's good, we'll create it
}
// Check Users
for (const user of users) {
try {
await this.client.get(`${this.base}/session/data/${this.dataSource}/users/${encodeURIComponent(user.username)}`, { params: this.authParams });
// User exists, fine
} catch (error: any) {
// If 404, fine
}
}
// Check Connections
for (const vm of vms) {
const existing = await this.findConnectionIdentifier(vm.name);
if (existing) {
// Connection exists, fine
}
}
}
async ensureUserGroup(identifier: string, displayName: string): Promise<void> {
const url = `${this.base}/session/data/${this.dataSource}/userGroups`;
const body = {
identifier,
name: displayName,
attributes: {
"disabled": "",
"expired": "",
"valid-from": "",
"valid-until": "",
}
};
try {
await this.client.post(url, body, { params: this.authParams });
} catch (error: any) {
if (error.response && error.response.status === 409) return; // Conflict is OK
throw error;
}
}
async createOrUpdateUser(user: GuacamoleUser): Promise<void> {
const url = `${this.base}/session/data/${this.dataSource}/users`;
const body = {
username: user.username,
password: user.password,
attributes: {
"disabled": "",
"expired": "",
"valid-from": "",
"valid-until": "",
"guac-full-name": user.displayName || "",
"guac-organization": "",
"guac-organizational-role": ""
}
};
try {
await this.client.post(url, body, { params: this.authParams });
} catch (error: any) {
if (error.response && error.response.status === 409) {
// Update
const updateUrl = `${url}/${encodeURIComponent(user.username)}`;
await this.client.put(updateUrl, body, { params: this.authParams });
} else {
throw error;
}
}
}
async createOrUpdateConnection(groupIdentifier: string, vm: GuacamoleVm): Promise<string> {
const url = `${this.base}/session/data/${this.dataSource}/connections`;
const hostname = vm.ip.split('/')[0] || vm.ip;
const port = (vm.rdpPort || DEFAULT_RDP_PORT).toString();
const parameters: any = {
hostname,
port,
security: "any",
"ignore-cert": "true",
username: "dev",
password: "Formation123!"
};
if (vm.rdpDomain) {
parameters.domain = vm.rdpDomain;
}
const body = {
name: vm.name,
parentIdentifier: "ROOT",
protocol: "rdp",
parameters,
attributes: {
"max-connections": "",
"max-connections-per-user": "",
"weight": "",
"failover-only": ""
}
};
try {
const response = await this.client.post(url, body, { params: this.authParams });
return response.data.identifier; // Assuming response contains identifier in body or we extract it
} catch (error: any) {
if (error.response && error.response.status === 409) {
const existing = await this.findConnectionIdentifier(vm.name);
if (existing) return existing;
throw new Error(`Connection '${vm.name}' already exists but identifier could not be resolved`);
}
throw error;
}
}
async findConnectionIdentifier(name: string): Promise<string | null> {
const url = `${this.base}/session/data/${this.dataSource}/connections`;
const response = await this.client.get(url, { params: this.authParams });
const connections = response.data;
if (connections && typeof connections === 'object') {
for (const key in connections) {
if (connections[key].name === name) {
return connections[key].identifier;
}
}
}
return null;
}
async setGroupMembers(group: string, users: GuacamoleUser[]): Promise<void> {
const url = `${this.base}/session/data/${this.dataSource}/userGroups/${encodeURIComponent(group)}/memberUsers`;
const ops = users.map(user => ({
op: "add",
path: "/",
value: user.username
}));
await this.client.patch(url, ops, {
params: this.authParams,
headers: { 'Content-Type': 'application/json' }
});
}
async grantGroupConnection(group: string, connectionId: string): Promise<void> {
const url = `${this.base}/session/data/${this.dataSource}/userGroups/${encodeURIComponent(group)}/permissions`;
const ops = [{
op: "add",
path: `/connectionPermissions/${connectionId}`,
value: "READ"
}];
await this.client.patch(url, ops, {
params: this.authParams,
headers: { 'Content-Type': 'application/json' }
});
}
async grantUserConnection(username: string, connectionId: string): Promise<void> {
const url = `${this.base}/session/data/${this.dataSource}/users/${encodeURIComponent(username)}/permissions`;
const ops = [{
op: "add",
path: `/connectionPermissions/${connectionId}`,
value: "READ"
}];
await this.client.patch(url, ops, {
params: this.authParams,
headers: { 'Content-Type': 'application/json' }
});
}
async getGroupMembers(group: string): Promise<{ users: string[], connections: string[] }> {
const usersUrl = `${this.base}/session/data/${this.dataSource}/userGroups/${encodeURIComponent(group)}/memberUsers`;
const usersRes = await this.client.get(usersUrl, { params: this.authParams });
const users = usersRes.data || [];
const permsUrl = `${this.base}/session/data/${this.dataSource}/userGroups/${encodeURIComponent(group)}/permissions`;
const permsRes = await this.client.get(permsUrl, { params: this.authParams });
const connections: string[] = [];
if (permsRes.data && permsRes.data.connectionPermissions) {
connections.push(...Object.keys(permsRes.data.connectionPermissions));
}
// Also fetch user permissions
for (const user of users) {
const userPermsUrl = `${this.base}/session/data/${this.dataSource}/users/${encodeURIComponent(user)}/permissions`;
try {
const userPermsRes = await this.client.get(userPermsUrl, { params: this.authParams });
if (userPermsRes.data && userPermsRes.data.connectionPermissions) {
connections.push(...Object.keys(userPermsRes.data.connectionPermissions));
}
} catch (e) {
// ignore
}
}
return { users, connections: [...new Set(connections)] };
}
async deleteUser(username: string): Promise<void> {
const url = `${this.base}/session/data/${this.dataSource}/users/${encodeURIComponent(username)}`;
await this.client.delete(url, { params: this.authParams });
}
async deleteGroup(group: string): Promise<void> {
const url = `${this.base}/session/data/${this.dataSource}/userGroups/${encodeURIComponent(group)}`;
await this.client.delete(url, { params: this.authParams });
}
async deleteConnection(identifier: string): Promise<void> {
const url = `${this.base}/session/data/${this.dataSource}/connections/${encodeURIComponent(identifier)}`;
await this.client.delete(url, { params: this.authParams });
}
}
// Helper to extract numeric suffix for matching
const extractNumericSuffix = (s: string): number | null => {

View File

@ -6,36 +6,17 @@ import * as tfvarsService from './tfvarsService';
import * as shell from '../utils/shell';
import * as guacamoleService from './guacamoleService';
import logger from '../utils/logger';
import { ApplyRequest } from '../types/terraform';
interface VmDefinition {
name: string;
vmid: number;
cores: number;
memory: number;
disk_size: number;
template: string;
ip: string;
bridge?: string;
model: 'windows' | 'linux';
rdpDomain?: string;
rdpPort?: number;
}
interface GuacamoleConfig {
groupName?: string;
users: { username: string; password: string }[];
}
export interface ApplyRequest {
formationId: string;
vms: VmDefinition[];
guacamole?: GuacamoleConfig;
}
const prepareRemoteWorkspace = async (formationId: string): Promise<string> => {
const remoteDir = path.posix.join(config.terraform.deployBasePath, formationId);
const templateSrc = config.terraform.remoteTemplatePath;
await ensureRemoteDirectory(remoteDir);
await copyTemplateToRemote(remoteDir);
return remoteDir;
};
const ensureRemoteDirectory = async (remoteDir: string) => {
// Check if exists and cleanup
const checkCmd = `test -d ${shell.escapeShell(remoteDir)}`;
const checkRes = await shell.executeSsh(checkCmd);
@ -43,7 +24,10 @@ const prepareRemoteWorkspace = async (formationId: string): Promise<string> => {
logger.warn(`Remote directory ${remoteDir} exists. Cleaning up before copy.`);
await cleanupRemote(remoteDir);
}
};
const copyTemplateToRemote = async (remoteDir: string) => {
const templateSrc = config.terraform.remoteTemplatePath;
// Copy template using remote cp -r
// This assumes templateSrc exists on the remote machine
const copyCmd = `cp -r ${shell.escapeShell(templateSrc)} ${shell.escapeShell(remoteDir)}`;
@ -52,8 +36,6 @@ const prepareRemoteWorkspace = async (formationId: string): Promise<string> => {
if (copyRes.code !== 0) {
throw new Error(`Remote copy failed: ${copyRes.stderr}`);
}
return remoteDir;
};
const cleanupRemote = async (remoteDir: string) => {
@ -61,6 +43,15 @@ const cleanupRemote = async (remoteDir: string) => {
await shell.executeSsh(cmd);
};
const runTerraformCommand = async (remoteDir: string, command: string) => {
const fullCmd = `cd ${shell.escapeShell(remoteDir)} && terraform ${command}`;
const res = await shell.executeSsh(fullCmd);
if (res.code !== 0) {
throw new Error(`Terraform command '${command}' failed: ${res.stderr}`);
}
return res;
};
export const handleApplyJob = async (formationId: string, req: ApplyRequest) => {
const remoteDir = await prepareRemoteWorkspace(formationId);
@ -75,18 +66,10 @@ export const handleApplyJob = async (formationId: string, req: ApplyRequest) =>
}
// Terraform init
const initCmd = `cd ${shell.escapeShell(remoteDir)} && terraform init -input=false -upgrade`;
const initRes = await shell.executeSsh(initCmd);
if (initRes.code !== 0) {
throw new Error(`Terraform init failed: ${initRes.stderr}`);
}
await runTerraformCommand(remoteDir, 'init -input=false -upgrade');
// Terraform apply
const applyCmd = `cd ${shell.escapeShell(remoteDir)} && terraform apply -auto-approve -input=false`;
const applyRes = await shell.executeSsh(applyCmd);
if (applyRes.code !== 0) {
throw new Error(`Terraform apply failed: ${applyRes.stderr}`);
}
await runTerraformCommand(remoteDir, 'apply -auto-approve -input=false');
// Save to DB
await prisma.terraform_formations.create({
@ -141,15 +124,9 @@ export const destroyFormation = async (formationId: string) => {
// Terraform destroy
// We run init first to be safe, though state should be there
const initCmd = `cd ${shell.escapeShell(remoteDir)} && terraform init -input=false`;
await shell.executeSsh(initCmd);
await runTerraformCommand(remoteDir, 'init -input=false');
const destroyCmd = `cd ${shell.escapeShell(remoteDir)} && terraform destroy -auto-approve -input=false -lock=false`;
const destroyRes = await shell.executeSsh(destroyCmd);
if (destroyRes.code !== 0) {
throw new Error(`Terraform destroy failed: ${destroyRes.stderr}`);
}
await runTerraformCommand(remoteDir, 'destroy -auto-approve -input=false -lock=false');
// Remove remote dir
await cleanupRemote(remoteDir);

View File

@ -1,4 +1,5 @@
import config from '../config';
import { VmDefinition, ApplyRequest } from '../types/terraform';
interface DeploymentRequest {
name: string;
@ -10,21 +11,6 @@ interface DeploymentRequest {
ipStartHost?: number;
}
interface TerraformVm {
name: string;
vmid: number;
cores: number;
memory: number;
disk_size: number;
template: string;
ip: string;
bridge?: string;
model: 'windows' | 'linux';
}
interface TerraformApplyRequest {
vms: TerraformVm[];
}
const escape = (s: string) => s.replace(/"/g, '\\"');
@ -49,7 +35,7 @@ export const generate = (req: DeploymentRequest): string => {
return buf;
};
export const generateVmDefinitions = (req: TerraformApplyRequest): string => {
export const generateVmDefinitions = (req: ApplyRequest): string => {
const cfg = config.proxmox;
let buf = '';
@ -66,36 +52,47 @@ export const generateVmDefinitions = (req: TerraformApplyRequest): string => {
buf += `vms = {\n`;
for (const vm of req.vms) {
buf += ` "${escape(vm.name)}" = {\n`;
buf += ` vmid = ${vm.vmid}\n`;
buf += ` cores = ${vm.cores}\n`;
buf += ` memory = ${vm.memory}\n`;
buf += ` disk_size = "${vm.disk_size}G"\n`;
buf += ` template = "${escape(vm.template)}"\n`;
buf += ` full_clone = false\n`;
const gateway = "192.168.143.254";
const bridge = vm.bridge || "GUACALAN";
// Check if model is explicitly windows OR if template name suggests windows
const isWindows = (vm.model && vm.model.toLowerCase() === 'windows') ||
(vm.template && /w(?:in|10|11|2019|2022)/i.test(vm.template));
const modelValue = isWindows ? cfg.modelWindows : cfg.modelLinux;
buf += ` network_interfaces = [\n`;
buf += ` {\n`;
buf += ` ip = "${escape(vm.ip)}"\n`;
if (vm.ip.toLowerCase() !== 'dhcp') {
buf += ` gateway = "${escape(gateway)}"\n`;
}
buf += ` bridge = "${escape(bridge)}"\n`;
buf += ` model = "${escape(modelValue)}"\n`;
buf += ` }\n`;
buf += ` ]\n`;
buf += ` }\n`;
buf += generateVmBlock(vm, cfg);
}
buf += `}\n`;
return buf;
};
const generateVmBlock = (vm: VmDefinition, cfg: any): string => {
let buf = ` "${escape(vm.name)}" = {\n`;
buf += ` vmid = ${vm.vmid}\n`;
buf += ` cores = ${vm.cores}\n`;
buf += ` memory = ${vm.memory}\n`;
buf += ` disk_size = "${vm.disk_size}G"\n`;
buf += ` template = "${escape(vm.template)}"\n`;
buf += ` full_clone = false\n`;
buf += generateNetworkInterfaceBlock(vm, cfg);
buf += ` }\n`;
return buf;
};
const generateNetworkInterfaceBlock = (vm: VmDefinition, cfg: any): string => {
const gateway = "192.168.143.254";
const bridge = vm.bridge || "GUACALAN";
// Check if model is explicitly windows OR if template name suggests windows
const isWindows = (vm.model && vm.model.toLowerCase() === 'windows') ||
(vm.template && /w(?:in|10|11|2019|2022)/i.test(vm.template));
const modelValue = isWindows ? cfg.modelWindows : cfg.modelLinux;
let buf = ` network_interfaces = [\n`;
buf += ` {\n`;
buf += ` ip = "${escape(vm.ip)}"\n`;
if (vm.ip.toLowerCase() !== 'dhcp') {
buf += ` gateway = "${escape(gateway)}"\n`;
}
buf += ` bridge = "${escape(bridge)}"\n`;
buf += ` model = "${escape(modelValue)}"\n`;
buf += ` }\n`;
buf += ` ]\n`;
return buf;
};

24
src/types/terraform.ts Normal file
View File

@ -0,0 +1,24 @@
export interface VmDefinition {
name: string;
vmid: number;
cores: number;
memory: number;
disk_size: number;
template: string;
ip: string;
bridge?: string;
model: 'windows' | 'linux';
rdpDomain?: string;
rdpPort?: number;
}
export interface GuacamoleConfig {
groupName?: string;
users: { username: string; password: string }[];
}
export interface ApplyRequest {
formationId: string;
vms: VmDefinition[];
guacamole?: GuacamoleConfig;
}

View File

@ -0,0 +1,309 @@
import axios, { AxiosInstance } from 'axios';
import https from 'https';
import config from '../config';
export interface GuacamoleUser {
username: string;
password?: string;
displayName?: string;
}
export interface GuacamoleVm {
name: string;
ip: string;
rdpPort?: number;
rdpDomain?: string;
}
interface TokenResponse {
authToken: string;
dataSource?: string;
availableDataSources?: string[];
}
const DEFAULT_RDP_PORT = 3389;
export class GuacSession {
private client: AxiosInstance;
private base: string;
private dataSource: string;
private token: string;
private constructor(client: AxiosInstance, base: string, dataSource: string, token: string) {
this.client = client;
this.base = base;
this.dataSource = dataSource;
this.token = token;
}
static async login(): Promise<GuacSession> {
const { apiEndpoint, username, password } = config.guacamole;
const base = apiEndpoint.replace(/\/$/, '');
const agent = new https.Agent({
rejectUnauthorized: false
});
const client = axios.create({
httpsAgent: agent,
timeout: 20000
});
const params = new URLSearchParams();
params.append('username', username);
params.append('password', password);
try {
const response = await client.post<TokenResponse>(`${base}/tokens`, params, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
const tokenResponse = response.data;
let dataSource = tokenResponse.dataSource;
if (!dataSource && tokenResponse.availableDataSources && tokenResponse.availableDataSources.length > 0) {
dataSource = tokenResponse.availableDataSources[tokenResponse.availableDataSources.length - 1];
}
if (!dataSource) {
throw new Error('Guacamole did not return a data source');
}
return new GuacSession(client, base, dataSource, tokenResponse.authToken);
} catch (error) {
console.error('Guacamole login failed:', error);
throw new Error('Guacamole login failed');
}
}
private get authParams() {
return {
token: this.token,
dataSource: this.dataSource
};
}
async checkExistence(group: string, users: GuacamoleUser[], vms: GuacamoleVm[]): Promise<void> {
// Check Group
try {
await this.client.get(`${this.base}/session/data/${this.dataSource}/userGroups/${encodeURIComponent(group)}`, { params: this.authParams });
// Group exists, which is fine, we'll update/use it
} catch (error: any) {
// If 404, it's good, we'll create it
}
// Check Users
for (const user of users) {
try {
await this.client.get(`${this.base}/session/data/${this.dataSource}/users/${encodeURIComponent(user.username)}`, { params: this.authParams });
// User exists, fine
} catch (error: any) {
// If 404, fine
}
}
// Check Connections
for (const vm of vms) {
const existing = await this.findConnectionIdentifier(vm.name);
if (existing) {
// Connection exists, fine
}
}
}
async ensureUserGroup(identifier: string, displayName: string): Promise<void> {
const url = `${this.base}/session/data/${this.dataSource}/userGroups`;
const body = {
identifier,
name: displayName,
attributes: {
"disabled": "",
"expired": "",
"valid-from": "",
"valid-until": "",
}
};
try {
await this.client.post(url, body, { params: this.authParams });
} catch (error: any) {
if (error.response && error.response.status === 409) return; // Conflict is OK
throw error;
}
}
async createOrUpdateUser(user: GuacamoleUser): Promise<void> {
const url = `${this.base}/session/data/${this.dataSource}/users`;
const body = {
username: user.username,
password: user.password,
attributes: {
"disabled": "",
"expired": "",
"valid-from": "",
"valid-until": "",
"guac-full-name": user.displayName || "",
"guac-organization": "",
"guac-organizational-role": ""
}
};
try {
await this.client.post(url, body, { params: this.authParams });
} catch (error: any) {
if (error.response && error.response.status === 409) {
// Update
const updateUrl = `${url}/${encodeURIComponent(user.username)}`;
await this.client.put(updateUrl, body, { params: this.authParams });
} else {
throw error;
}
}
}
async createOrUpdateConnection(groupIdentifier: string, vm: GuacamoleVm): Promise<string> {
const url = `${this.base}/session/data/${this.dataSource}/connections`;
const hostname = vm.ip.split('/')[0] || vm.ip;
const port = (vm.rdpPort || DEFAULT_RDP_PORT).toString();
const parameters: any = {
hostname,
port,
security: "any",
"ignore-cert": "true",
username: "dev",
password: "Formation123!"
};
if (vm.rdpDomain) {
parameters.domain = vm.rdpDomain;
}
const body = {
name: vm.name,
parentIdentifier: "ROOT",
protocol: "rdp",
parameters,
attributes: {
"max-connections": "",
"max-connections-per-user": "",
"weight": "",
"failover-only": ""
}
};
try {
const response = await this.client.post(url, body, { params: this.authParams });
return response.data.identifier; // Assuming response contains identifier in body or we extract it
} catch (error: any) {
if (error.response && error.response.status === 409) {
const existing = await this.findConnectionIdentifier(vm.name);
if (existing) return existing;
throw new Error(`Connection '${vm.name}' already exists but identifier could not be resolved`);
}
throw error;
}
}
async findConnectionIdentifier(name: string): Promise<string | null> {
const url = `${this.base}/session/data/${this.dataSource}/connections`;
const response = await this.client.get(url, { params: this.authParams });
const connections = response.data;
if (connections && typeof connections === 'object') {
for (const key in connections) {
if (connections[key].name === name) {
return connections[key].identifier;
}
}
}
return null;
}
async setGroupMembers(group: string, users: GuacamoleUser[]): Promise<void> {
const url = `${this.base}/session/data/${this.dataSource}/userGroups/${encodeURIComponent(group)}/memberUsers`;
const ops = users.map(user => ({
op: "add",
path: "/",
value: user.username
}));
await this.client.patch(url, ops, {
params: this.authParams,
headers: { 'Content-Type': 'application/json' }
});
}
async grantGroupConnection(group: string, connectionId: string): Promise<void> {
const url = `${this.base}/session/data/${this.dataSource}/userGroups/${encodeURIComponent(group)}/permissions`;
const ops = [{
op: "add",
path: `/connectionPermissions/${connectionId}`,
value: "READ"
}];
await this.client.patch(url, ops, {
params: this.authParams,
headers: { 'Content-Type': 'application/json' }
});
}
async grantUserConnection(username: string, connectionId: string): Promise<void> {
const url = `${this.base}/session/data/${this.dataSource}/users/${encodeURIComponent(username)}/permissions`;
const ops = [{
op: "add",
path: `/connectionPermissions/${connectionId}`,
value: "READ"
}];
await this.client.patch(url, ops, {
params: this.authParams,
headers: { 'Content-Type': 'application/json' }
});
}
async getGroupMembers(group: string): Promise<{ users: string[], connections: string[] }> {
const usersUrl = `${this.base}/session/data/${this.dataSource}/userGroups/${encodeURIComponent(group)}/memberUsers`;
const usersRes = await this.client.get(usersUrl, { params: this.authParams });
const users = usersRes.data || [];
const permsUrl = `${this.base}/session/data/${this.dataSource}/userGroups/${encodeURIComponent(group)}/permissions`;
const permsRes = await this.client.get(permsUrl, { params: this.authParams });
const connections: string[] = [];
if (permsRes.data && permsRes.data.connectionPermissions) {
connections.push(...Object.keys(permsRes.data.connectionPermissions));
}
// Also fetch user permissions
for (const user of users) {
const userPermsUrl = `${this.base}/session/data/${this.dataSource}/users/${encodeURIComponent(user)}/permissions`;
try {
const userPermsRes = await this.client.get(userPermsUrl, { params: this.authParams });
if (userPermsRes.data && userPermsRes.data.connectionPermissions) {
connections.push(...Object.keys(userPermsRes.data.connectionPermissions));
}
} catch (e) {
// ignore
}
}
return { users, connections: [...new Set(connections)] };
}
async deleteUser(username: string): Promise<void> {
const url = `${this.base}/session/data/${this.dataSource}/users/${encodeURIComponent(username)}`;
await this.client.delete(url, { params: this.authParams });
}
async deleteGroup(group: string): Promise<void> {
const url = `${this.base}/session/data/${this.dataSource}/userGroups/${encodeURIComponent(group)}`;
await this.client.delete(url, { params: this.authParams });
}
async deleteConnection(identifier: string): Promise<void> {
const url = `${this.base}/session/data/${this.dataSource}/connections/${encodeURIComponent(identifier)}`;
await this.client.delete(url, { params: this.authParams });
}
}