Test
This commit is contained in:
parent
dc38b80db1
commit
2838b70a27
76
dist/config/index.js
vendored
Normal file
76
dist/config/index.js
vendored
Normal file
@ -0,0 +1,76 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const dotenv_1 = __importDefault(require("dotenv"));
|
||||
const zod_1 = require("zod");
|
||||
dotenv_1.default.config();
|
||||
const envSchema = zod_1.z.object({
|
||||
PORT: zod_1.z.string().default('3000'),
|
||||
DATABASE_URL: zod_1.z.string(),
|
||||
JWT_SECRET: zod_1.z.string(),
|
||||
JWT_TTL_SECONDS: zod_1.z.string().default('3600'),
|
||||
APP_ADMIN_EMAIL: zod_1.z.string().optional(),
|
||||
APP_ADMIN_PASSWORD: zod_1.z.string().optional(),
|
||||
// Guacamole
|
||||
GUACAMOLE_API_ENDPOINT: zod_1.z.string().default('https://guacamole.firewax.fr/guacamole/api'),
|
||||
GUACAMOLE_USERNAME: zod_1.z.string().default('guacadmin'),
|
||||
GUACAMOLE_PASSWORD: zod_1.z.string().default('guacadmin'),
|
||||
// Terraform
|
||||
TERRAFORM_DROP_DIR: zod_1.z.string().default('terraform_drop'),
|
||||
TERRAFORM_TEMPLATE_PATH: zod_1.z.string().default('/home/corenthin/terraform'),
|
||||
TERRAFORM_REMOTE_TEMPLATE_PATH: zod_1.z.string().default('/home/corenthin/terraform'),
|
||||
TERRAFORM_DEPLOY_BASE_PATH: zod_1.z.string().default('/home/corenthin/'),
|
||||
TERRAFORM_SSH_HOST: zod_1.z.string().default('127.0.0.1'),
|
||||
TERRAFORM_SSH_USER: zod_1.z.string().default('corenthin'),
|
||||
TERRAFORM_SSH_KEY_PATH: zod_1.z.string().default('/home/corenthin/.ssh/testapi'),
|
||||
// Proxmox
|
||||
PROXMOX_URL: zod_1.z.string().default('https://node1.solyone.fr:8006/api2/json'),
|
||||
PROXMOX_TOKEN_ID: zod_1.z.string().default('terraform-prov@pve!mytoken'),
|
||||
PROXMOX_TOKEN_SECRET: zod_1.z.string().default('a4b8720c-6e69-4309-ab56-54b62126b6e6'),
|
||||
PROXMOX_INSECURE_TLS: zod_1.z.string().default('true'),
|
||||
PROXMOX_TARGET_NODE: zod_1.z.string().default('node1'),
|
||||
PROXMOX_MODEL_WINDOWS: zod_1.z.string().default('e1000'),
|
||||
PROXMOX_MODEL_LINUX: zod_1.z.string().default('virtio'),
|
||||
// Cors
|
||||
CORS_ALLOWED_ORIGINS: zod_1.z.string().default('http://localhost:3000,http://localhost:5173,http://127.0.0.1:3000,http://127.0.0.1:5173,http://192.168.1.52:5173,https://dev.firewax.fr'),
|
||||
});
|
||||
const env = envSchema.parse(process.env);
|
||||
exports.default = {
|
||||
port: parseInt(env.PORT, 10),
|
||||
jwt: {
|
||||
secret: env.JWT_SECRET,
|
||||
ttlSeconds: parseInt(env.JWT_TTL_SECONDS, 10),
|
||||
},
|
||||
admin: {
|
||||
email: env.APP_ADMIN_EMAIL,
|
||||
password: env.APP_ADMIN_PASSWORD,
|
||||
},
|
||||
guacamole: {
|
||||
apiEndpoint: env.GUACAMOLE_API_ENDPOINT,
|
||||
username: env.GUACAMOLE_USERNAME,
|
||||
password: env.GUACAMOLE_PASSWORD,
|
||||
},
|
||||
terraform: {
|
||||
dropDir: env.TERRAFORM_DROP_DIR,
|
||||
templatePath: env.TERRAFORM_TEMPLATE_PATH,
|
||||
remoteTemplatePath: env.TERRAFORM_REMOTE_TEMPLATE_PATH,
|
||||
deployBasePath: env.TERRAFORM_DEPLOY_BASE_PATH,
|
||||
sshHost: env.TERRAFORM_SSH_HOST,
|
||||
sshUser: env.TERRAFORM_SSH_USER,
|
||||
sshKeyPath: env.TERRAFORM_SSH_KEY_PATH,
|
||||
},
|
||||
proxmox: {
|
||||
url: env.PROXMOX_URL,
|
||||
tokenId: env.PROXMOX_TOKEN_ID,
|
||||
tokenSecret: env.PROXMOX_TOKEN_SECRET,
|
||||
insecureTls: env.PROXMOX_INSECURE_TLS === 'true',
|
||||
targetNode: env.PROXMOX_TARGET_NODE,
|
||||
modelWindows: env.PROXMOX_MODEL_WINDOWS,
|
||||
modelLinux: env.PROXMOX_MODEL_LINUX,
|
||||
},
|
||||
cors: {
|
||||
allowedOrigins: env.CORS_ALLOWED_ORIGINS.split(','),
|
||||
}
|
||||
};
|
||||
218
dist/controllers/adminController.js
vendored
Normal file
218
dist/controllers/adminController.js
vendored
Normal file
@ -0,0 +1,218 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.deleteUser = exports.updateUser = exports.createUser = exports.listUsers = exports.getDashboard = void 0;
|
||||
const zod_1 = require("zod");
|
||||
const uuid_1 = require("uuid");
|
||||
const prisma_1 = __importDefault(require("../utils/prisma"));
|
||||
const authService = __importStar(require("../services/authService"));
|
||||
const config_1 = __importDefault(require("../config"));
|
||||
const logger_1 = __importDefault(require("../utils/logger"));
|
||||
const createUserSchema = zod_1.z.object({
|
||||
email: zod_1.z.string().email(),
|
||||
password: zod_1.z.string().min(8),
|
||||
displayName: zod_1.z.string().optional(),
|
||||
isAdmin: zod_1.z.boolean().default(false),
|
||||
});
|
||||
const updateUserSchema = zod_1.z.object({
|
||||
email: zod_1.z.string().email().optional(),
|
||||
password: zod_1.z.string().min(8).optional(),
|
||||
displayName: zod_1.z.string().optional().nullable(),
|
||||
isAdmin: zod_1.z.boolean().optional(),
|
||||
});
|
||||
const mapUser = (user) => ({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.display_name || user.email,
|
||||
displayName: user.display_name,
|
||||
isAdmin: user.is_admin,
|
||||
});
|
||||
const getDashboard = async (req, res) => {
|
||||
try {
|
||||
const users = await prisma_1.default.users.findMany();
|
||||
const summaries = users.map(mapUser);
|
||||
const currentUser = await prisma_1.default.users.findUnique({ where: { id: req.user.id } });
|
||||
res.json({
|
||||
currentUser: mapUser(currentUser),
|
||||
users: summaries,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
logger_1.default.error(`Dashboard error: ${error}`);
|
||||
res.status(500).json({ message: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
exports.getDashboard = getDashboard;
|
||||
const listUsers = async (req, res) => {
|
||||
try {
|
||||
const users = await prisma_1.default.users.findMany();
|
||||
const summaries = users.map(mapUser);
|
||||
res.json(summaries);
|
||||
}
|
||||
catch (error) {
|
||||
logger_1.default.error(`List users error: ${error}`);
|
||||
res.status(500).json({ message: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
exports.listUsers = listUsers;
|
||||
const createUser = async (req, res) => {
|
||||
try {
|
||||
const { email, password, displayName, isAdmin } = createUserSchema.parse(req.body);
|
||||
// Validate email domain
|
||||
if (!email.endsWith('@solyti.fr')) {
|
||||
res.status(400).json({ message: 'Email must belong to the @solyti.fr domain' });
|
||||
return;
|
||||
}
|
||||
const existingUser = await prisma_1.default.users.findUnique({ where: { email } });
|
||||
if (existingUser) {
|
||||
res.status(409).json({ message: 'Email already registered' });
|
||||
return;
|
||||
}
|
||||
const passwordHash = await authService.hashPassword(password);
|
||||
const newUser = await prisma_1.default.users.create({
|
||||
data: {
|
||||
id: (0, uuid_1.v4)(),
|
||||
email,
|
||||
password_hash: passwordHash,
|
||||
display_name: displayName,
|
||||
is_admin: isAdmin,
|
||||
},
|
||||
});
|
||||
res.status(201).json(mapUser(newUser));
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof zod_1.z.ZodError) {
|
||||
res.status(400).json({ message: 'Invalid input', errors: error.issues });
|
||||
return;
|
||||
}
|
||||
logger_1.default.error(`Create user error: ${error}`);
|
||||
res.status(500).json({ message: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
exports.createUser = createUser;
|
||||
const updateUser = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { email, password, displayName, isAdmin } = updateUserSchema.parse(req.body);
|
||||
const currentUser = req.user;
|
||||
const userToUpdate = await prisma_1.default.users.findUnique({ where: { id } });
|
||||
if (!userToUpdate) {
|
||||
res.status(404).json({ message: 'User not found' });
|
||||
return;
|
||||
}
|
||||
const defaultAdminEmail = config_1.default.admin.email;
|
||||
// Protect user if they match the configured admin email OR if their name is 'Corenthin'
|
||||
const isTargetDefaultAdmin = (defaultAdminEmail && userToUpdate.email.toLowerCase() === defaultAdminEmail.toLowerCase()) ||
|
||||
userToUpdate.display_name === 'Corenthin';
|
||||
// Prevent self-demotion for ANY user
|
||||
if (currentUser.id === userToUpdate.id && isAdmin === false) {
|
||||
res.status(400).json({ message: 'You cannot remove your own admin rights' });
|
||||
return;
|
||||
}
|
||||
if (isTargetDefaultAdmin && currentUser.id !== userToUpdate.id) {
|
||||
res.status(403).json({ message: 'Default administrator can only modify their own profile' });
|
||||
return;
|
||||
}
|
||||
if (isTargetDefaultAdmin && email && defaultAdminEmail && email.toLowerCase() !== defaultAdminEmail.toLowerCase()) {
|
||||
res.status(400).json({ message: 'Default administrator email cannot change' });
|
||||
return;
|
||||
}
|
||||
if (isTargetDefaultAdmin && isAdmin !== undefined && isAdmin !== userToUpdate.is_admin) {
|
||||
res.status(400).json({ message: 'Default administrator rights cannot change' });
|
||||
return;
|
||||
}
|
||||
const updateData = {};
|
||||
if (email)
|
||||
updateData.email = email;
|
||||
if (password)
|
||||
updateData.password_hash = await authService.hashPassword(password);
|
||||
if (displayName !== undefined)
|
||||
updateData.display_name = displayName; // Allow null to clear
|
||||
if (isAdmin !== undefined)
|
||||
updateData.is_admin = isAdmin;
|
||||
const updatedUser = await prisma_1.default.users.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
});
|
||||
res.json(mapUser(updatedUser));
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof zod_1.z.ZodError) {
|
||||
res.status(400).json({ message: 'Invalid input', errors: error.issues });
|
||||
return;
|
||||
}
|
||||
// Handle unique constraint violation for email
|
||||
// Prisma throws P2002
|
||||
if (error.code === 'P2002') {
|
||||
res.status(409).json({ message: 'Email already registered' });
|
||||
return;
|
||||
}
|
||||
logger_1.default.error(`Update user error: ${error}`);
|
||||
res.status(500).json({ message: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
exports.updateUser = updateUser;
|
||||
const deleteUser = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const currentUser = req.user;
|
||||
if (currentUser.id === id) {
|
||||
res.status(400).json({ message: 'Cannot delete yourself' });
|
||||
return;
|
||||
}
|
||||
const userToDelete = await prisma_1.default.users.findUnique({ where: { id } });
|
||||
if (!userToDelete) {
|
||||
res.status(404).json({ message: 'User not found' });
|
||||
return;
|
||||
}
|
||||
const defaultAdminEmail = config_1.default.admin.email;
|
||||
const isTargetDefaultAdmin = (defaultAdminEmail && userToDelete.email.toLowerCase() === defaultAdminEmail.toLowerCase()) ||
|
||||
userToDelete.display_name === 'Corenthin';
|
||||
if (isTargetDefaultAdmin) {
|
||||
res.status(403).json({ message: 'Default administrator cannot be deleted' });
|
||||
return;
|
||||
}
|
||||
await prisma_1.default.users.delete({ where: { id } });
|
||||
res.status(204).send();
|
||||
}
|
||||
catch (error) {
|
||||
logger_1.default.error(`Delete user error: ${error}`);
|
||||
res.status(500).json({ message: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
exports.deleteUser = deleteUser;
|
||||
127
dist/controllers/authController.js
vendored
Normal file
127
dist/controllers/authController.js
vendored
Normal file
@ -0,0 +1,127 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.logout = exports.me = exports.login = void 0;
|
||||
const zod_1 = require("zod");
|
||||
const prisma_1 = __importDefault(require("../utils/prisma"));
|
||||
const authService = __importStar(require("../services/authService"));
|
||||
const bruteforceService_1 = __importDefault(require("../services/bruteforceService"));
|
||||
const helpers_1 = require("../utils/helpers");
|
||||
const logger_1 = __importDefault(require("../utils/logger"));
|
||||
const loginSchema = zod_1.z.object({
|
||||
email: zod_1.z.string().email(),
|
||||
password: zod_1.z.string(),
|
||||
});
|
||||
const login = async (req, res) => {
|
||||
try {
|
||||
const { email, password } = loginSchema.parse(req.body);
|
||||
if (bruteforceService_1.default.isLocked(email)) {
|
||||
res.status(429).json({ message: 'Too many attempts, temporarily locked' });
|
||||
return;
|
||||
}
|
||||
const user = await prisma_1.default.users.findUnique({ where: { email } });
|
||||
if (!user) {
|
||||
bruteforceService_1.default.recordFailure(email);
|
||||
res.status(401).json({ message: 'Invalid credentials' });
|
||||
return;
|
||||
}
|
||||
const isValid = await authService.verifyPassword(password, user.password_hash);
|
||||
if (!isValid) {
|
||||
bruteforceService_1.default.recordFailure(email);
|
||||
res.status(401).json({ message: 'Invalid credentials' });
|
||||
return;
|
||||
}
|
||||
bruteforceService_1.default.recordSuccess(email);
|
||||
const token = authService.signToken(user.id, user.email);
|
||||
(0, helpers_1.setSessionCookie)(res, token);
|
||||
const meUser = {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.display_name || user.email,
|
||||
displayName: user.display_name,
|
||||
isAdmin: user.is_admin,
|
||||
};
|
||||
res.json({ token, user: meUser });
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof zod_1.z.ZodError) {
|
||||
res.status(400).json({ message: 'Invalid input', errors: error.issues });
|
||||
return;
|
||||
}
|
||||
logger_1.default.error(`Login error: ${error}`);
|
||||
res.status(500).json({ message: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
exports.login = login;
|
||||
const me = async (req, res) => {
|
||||
try {
|
||||
const token = req.cookies.token || req.headers.authorization?.split(' ')[1];
|
||||
if (!token) {
|
||||
res.status(401).json({ message: 'Unauthenticated' });
|
||||
return;
|
||||
}
|
||||
const claims = authService.verifyToken(token);
|
||||
if (!claims) {
|
||||
res.status(401).json({ message: 'Invalid session' });
|
||||
return;
|
||||
}
|
||||
const user = await prisma_1.default.users.findUnique({ where: { id: claims.userId } });
|
||||
if (!user || user.email !== claims.email) {
|
||||
res.status(401).json({ message: 'Invalid session' });
|
||||
return;
|
||||
}
|
||||
const meUser = {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.display_name || user.email,
|
||||
displayName: user.display_name,
|
||||
isAdmin: user.is_admin,
|
||||
};
|
||||
res.json({ user: meUser });
|
||||
}
|
||||
catch (error) {
|
||||
logger_1.default.error(`Me error: ${error}`);
|
||||
res.status(500).json({ message: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
exports.me = me;
|
||||
const logout = async (req, res) => {
|
||||
(0, helpers_1.clearSessionCookie)(res);
|
||||
res.status(204).send();
|
||||
};
|
||||
exports.logout = logout;
|
||||
93
dist/controllers/deploymentsController.js
vendored
Normal file
93
dist/controllers/deploymentsController.js
vendored
Normal file
@ -0,0 +1,93 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getSchema = exports.createDeployment = void 0;
|
||||
const zod_1 = require("zod");
|
||||
const uuid_1 = require("uuid");
|
||||
const promises_1 = __importDefault(require("fs/promises"));
|
||||
const path_1 = __importDefault(require("path"));
|
||||
const config_1 = __importDefault(require("../config"));
|
||||
const tfvarsService = __importStar(require("../services/tfvarsService"));
|
||||
const zod_to_json_schema_1 = require("zod-to-json-schema");
|
||||
const logger_1 = __importDefault(require("../utils/logger"));
|
||||
const deploymentSchema = zod_1.z.object({
|
||||
name: zod_1.z.string().min(1),
|
||||
userCount: zod_1.z.number().int().min(1),
|
||||
vmTemplate: zod_1.z.string(),
|
||||
cpu: zod_1.z.number().int().min(1),
|
||||
ramMb: zod_1.z.number().int().min(256),
|
||||
diskGb: zod_1.z.number().int().min(10),
|
||||
ipStartHost: zod_1.z.number().int().min(2).max(254).optional(),
|
||||
});
|
||||
const createDeployment = async (req, res) => {
|
||||
try {
|
||||
const payload = deploymentSchema.parse(req.body);
|
||||
if (payload.ipStartHost) {
|
||||
const last = payload.ipStartHost + payload.userCount - 1;
|
||||
if (last > 254) {
|
||||
res.status(400).json({ error: 'ip range exceeds 192.168.143.254' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
const id = (0, uuid_1.v4)();
|
||||
const tfvarsContent = tfvarsService.generate(payload);
|
||||
const dropDir = config_1.default.terraform.dropDir;
|
||||
const filename = `${payload.name.replace(/ /g, '_')}.tfvars`;
|
||||
const filePath = path_1.default.join(dropDir, filename);
|
||||
await promises_1.default.mkdir(dropDir, { recursive: true });
|
||||
await promises_1.default.writeFile(filePath, tfvarsContent);
|
||||
res.json({
|
||||
id,
|
||||
tfvarsPath: filePath,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof zod_1.z.ZodError) {
|
||||
res.status(400).json({ error: 'Invalid input', details: error.issues });
|
||||
return;
|
||||
}
|
||||
logger_1.default.error(`Create deployment error: ${error}`);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
exports.createDeployment = createDeployment;
|
||||
const getSchema = async (req, res) => {
|
||||
const schema = (0, zod_to_json_schema_1.zodToJsonSchema)(deploymentSchema, 'DeploymentRequest');
|
||||
res.json(schema);
|
||||
};
|
||||
exports.getSchema = getSchema;
|
||||
170
dist/controllers/terraformController.js
vendored
Normal file
170
dist/controllers/terraformController.js
vendored
Normal file
@ -0,0 +1,170 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.destroyFormation = exports.listFormations = exports.getTemplates = exports.apply = void 0;
|
||||
const zod_1 = require("zod");
|
||||
const prisma_1 = __importDefault(require("../utils/prisma"));
|
||||
const config_1 = __importDefault(require("../config"));
|
||||
const terraformService = __importStar(require("../services/terraformService"));
|
||||
const proxmoxService = __importStar(require("../services/proxmoxService"));
|
||||
const path_1 = __importDefault(require("path"));
|
||||
const logger_1 = __importDefault(require("../utils/logger"));
|
||||
// Zod schemas
|
||||
const vmSchema = zod_1.z.object({
|
||||
name: zod_1.z.string(),
|
||||
vmid: zod_1.z.number(),
|
||||
cores: zod_1.z.number(),
|
||||
memory: zod_1.z.number(),
|
||||
disk_size: zod_1.z.number(),
|
||||
template: zod_1.z.string(),
|
||||
ip: zod_1.z.string(),
|
||||
guacamoleIp: zod_1.z.string().optional(),
|
||||
bridge: zod_1.z.string().optional(),
|
||||
model: zod_1.z.enum(['windows', 'linux']).default('linux'),
|
||||
rdpDomain: zod_1.z.string().optional(),
|
||||
rdpPort: zod_1.z.number().optional(),
|
||||
});
|
||||
const guacamoleUserSchema = zod_1.z.object({
|
||||
username: zod_1.z.string(),
|
||||
password: zod_1.z.string(),
|
||||
});
|
||||
const guacamoleRequestSchema = zod_1.z.object({
|
||||
groupName: zod_1.z.string().optional(),
|
||||
users: zod_1.z.array(guacamoleUserSchema),
|
||||
});
|
||||
const applySchema = zod_1.z.object({
|
||||
formationId: zod_1.z.string().min(1),
|
||||
vms: zod_1.z.array(vmSchema).min(1),
|
||||
guacamole: guacamoleRequestSchema.optional(),
|
||||
});
|
||||
// Helper to clean formation ID
|
||||
const cleanFormationId = (id) => {
|
||||
return id;
|
||||
};
|
||||
const isValidFormationId = (id) => {
|
||||
return /^[a-zA-Z0-9-_]+$/.test(id);
|
||||
};
|
||||
const apply = async (req, res) => {
|
||||
try {
|
||||
const body = applySchema.parse(req.body);
|
||||
let formationId = cleanFormationId(body.formationId);
|
||||
if (!isValidFormationId(formationId)) {
|
||||
res.status(400).json({ message: 'Invalid formation id' });
|
||||
return;
|
||||
}
|
||||
const existingFormation = await prisma_1.default.terraform_formations.findUnique({
|
||||
where: { formation_id: formationId }
|
||||
});
|
||||
if (existingFormation) {
|
||||
res.status(409).json({ message: 'Formation already exists' });
|
||||
return;
|
||||
}
|
||||
// Spawn background job
|
||||
terraformService.handleApplyJob(formationId, body).catch(err => {
|
||||
logger_1.default.error(`Apply job failed for ${formationId}: ${err}`);
|
||||
});
|
||||
const remoteDirPreview = path_1.default.join(config_1.default.terraform.deployBasePath, formationId);
|
||||
res.status(202).json({
|
||||
status: 'queued',
|
||||
formationId,
|
||||
remotePath: remoteDirPreview,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof zod_1.z.ZodError) {
|
||||
logger_1.default.warn(`Validation failed: ${JSON.stringify(error.issues)}`);
|
||||
res.status(400).json({ message: 'Invalid input', errors: error.issues });
|
||||
return;
|
||||
}
|
||||
logger_1.default.error(`Apply error: ${error}`);
|
||||
res.status(500).json({ message: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
exports.apply = apply;
|
||||
const getTemplates = async (req, res) => {
|
||||
try {
|
||||
const templates = await proxmoxService.fetchTemplates();
|
||||
res.json({ templates });
|
||||
}
|
||||
catch (error) {
|
||||
logger_1.default.error(`Get templates error: ${error}`);
|
||||
res.status(502).json({ message: 'Failed to fetch templates from proxmox' });
|
||||
}
|
||||
};
|
||||
exports.getTemplates = getTemplates;
|
||||
const listFormations = async (req, res) => {
|
||||
try {
|
||||
const formationsRaw = await prisma_1.default.terraform_formations.findMany();
|
||||
const formations = formationsRaw.map(f => ({
|
||||
id: f.id,
|
||||
formationId: f.formation_id,
|
||||
remotePath: f.remote_path,
|
||||
guacamoleGroupName: f.guacamole_group_name,
|
||||
createdAt: f.created_at,
|
||||
status: 'active',
|
||||
}));
|
||||
res.json({ formations });
|
||||
}
|
||||
catch (error) {
|
||||
logger_1.default.error(`List formations error: ${error}`);
|
||||
res.status(500).json({ message: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
exports.listFormations = listFormations;
|
||||
const destroyFormation = async (req, res) => {
|
||||
try {
|
||||
const { formationId } = req.params;
|
||||
const formation = await prisma_1.default.terraform_formations.findUnique({
|
||||
where: { formation_id: formationId }
|
||||
});
|
||||
if (!formation) {
|
||||
res.status(404).json({ message: 'Formation not found' });
|
||||
return;
|
||||
}
|
||||
// Run in background
|
||||
terraformService.destroyFormation(formationId).catch(err => {
|
||||
logger_1.default.error(`Background destroy failed for ${formationId}: ${err}`);
|
||||
});
|
||||
res.status(202).json({ message: 'Formation destruction queued' });
|
||||
}
|
||||
catch (error) {
|
||||
logger_1.default.error(`Destroy formation error: ${error}`);
|
||||
res.status(500).json({ message: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
exports.destroyFormation = destroyFormation;
|
||||
70
dist/index.js
vendored
Normal file
70
dist/index.js
vendored
Normal file
@ -0,0 +1,70 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const express_1 = __importDefault(require("express"));
|
||||
const cors_1 = __importDefault(require("cors"));
|
||||
const helmet_1 = __importDefault(require("helmet"));
|
||||
const cookie_parser_1 = __importDefault(require("cookie-parser"));
|
||||
const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
|
||||
const morgan_1 = __importDefault(require("morgan"));
|
||||
const config_1 = __importDefault(require("./config"));
|
||||
const routes_1 = __importDefault(require("./routes"));
|
||||
const logger_1 = __importDefault(require("./utils/logger"));
|
||||
const errorHandler_1 = require("./middlewares/errorHandler");
|
||||
const prisma_1 = __importDefault(require("./utils/prisma"));
|
||||
const app = (0, express_1.default)();
|
||||
// Trust the first proxy (Nginx/Traefik)
|
||||
app.set('trust proxy', 1);
|
||||
// HTTP Request Logging
|
||||
app.use((0, morgan_1.default)('combined', { stream: { write: (message) => logger_1.default.http(message.trim()) } }));
|
||||
app.use((0, helmet_1.default)());
|
||||
app.use((0, cookie_parser_1.default)());
|
||||
app.use(express_1.default.json());
|
||||
// Rate Limiting
|
||||
const limiter = (0, express_rate_limit_1.default)({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
|
||||
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
|
||||
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
|
||||
message: 'Too many requests from this IP, please try again after 15 minutes',
|
||||
});
|
||||
app.use(limiter);
|
||||
app.use((0, cors_1.default)({
|
||||
origin: (origin, callback) => {
|
||||
if (!origin || config_1.default.cors.allowedOrigins.includes(origin)) {
|
||||
callback(null, true);
|
||||
}
|
||||
else {
|
||||
logger_1.default.warn(`CORS blocked origin: ${origin}`);
|
||||
callback(new Error('Not allowed by CORS'));
|
||||
}
|
||||
},
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
|
||||
}));
|
||||
app.use('/api', routes_1.default);
|
||||
// Error Handling Middleware (must be last)
|
||||
app.use(errorHandler_1.errorHandler);
|
||||
const server = app.listen(config_1.default.port, () => {
|
||||
logger_1.default.info(`Server running on port ${config_1.default.port}`);
|
||||
logger_1.default.info(`CORS Allowed Origins: ${config_1.default.cors.allowedOrigins.join(', ')}`);
|
||||
});
|
||||
// Graceful Shutdown
|
||||
const gracefulShutdown = async () => {
|
||||
logger_1.default.info('Received kill signal, shutting down gracefully');
|
||||
server.close(() => {
|
||||
logger_1.default.info('Closed out remaining connections');
|
||||
prisma_1.default.$disconnect().then(() => {
|
||||
logger_1.default.info('Database connection closed');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
setTimeout(() => {
|
||||
logger_1.default.error('Could not close connections in time, forcefully shutting down');
|
||||
process.exit(1);
|
||||
}, 10000);
|
||||
};
|
||||
process.on('SIGTERM', gracefulShutdown);
|
||||
process.on('SIGINT', gracefulShutdown);
|
||||
79
dist/middlewares/authMiddleware.js
vendored
Normal file
79
dist/middlewares/authMiddleware.js
vendored
Normal file
@ -0,0 +1,79 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.requireAdmin = exports.authenticate = void 0;
|
||||
const authService = __importStar(require("../services/authService"));
|
||||
const prisma_1 = __importDefault(require("../utils/prisma"));
|
||||
const authenticate = async (req, res, next) => {
|
||||
const token = req.cookies.token || req.headers.authorization?.split(' ')[1];
|
||||
if (!token) {
|
||||
res.status(401).json({ message: 'Unauthenticated' });
|
||||
return;
|
||||
}
|
||||
const claims = authService.verifyToken(token);
|
||||
if (!claims) {
|
||||
res.status(401).json({ message: 'Invalid session' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const user = await prisma_1.default.users.findUnique({ where: { id: claims.userId } });
|
||||
if (!user || user.email !== claims.email) {
|
||||
res.status(401).json({ message: 'Invalid session' });
|
||||
return;
|
||||
}
|
||||
req.user = {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
isAdmin: user.is_admin,
|
||||
};
|
||||
next();
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Auth middleware error:', error);
|
||||
res.status(500).json({ message: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
exports.authenticate = authenticate;
|
||||
const requireAdmin = (req, res, next) => {
|
||||
if (!req.user || !req.user.isAdmin) {
|
||||
res.status(403).json({ message: 'Admin privileges required' });
|
||||
return;
|
||||
}
|
||||
next();
|
||||
};
|
||||
exports.requireAdmin = requireAdmin;
|
||||
25
dist/middlewares/errorHandler.js
vendored
Normal file
25
dist/middlewares/errorHandler.js
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.errorHandler = void 0;
|
||||
const logger_1 = __importDefault(require("../utils/logger"));
|
||||
const zod_1 = require("zod");
|
||||
const errorHandler = (err, req, res, next) => {
|
||||
logger_1.default.error(`${err.status || 500} - ${err.message} - ${req.originalUrl} - ${req.method} - ${req.ip}`);
|
||||
if (err instanceof zod_1.z.ZodError) {
|
||||
res.status(400).json({
|
||||
message: 'Invalid input',
|
||||
errors: err.issues,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const statusCode = err.statusCode || 500;
|
||||
const message = err.message || 'Internal Server Error';
|
||||
res.status(statusCode).json({
|
||||
message,
|
||||
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
|
||||
});
|
||||
};
|
||||
exports.errorHandler = errorHandler;
|
||||
46
dist/routes/admin.js
vendored
Normal file
46
dist/routes/admin.js
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const express_1 = require("express");
|
||||
const adminController = __importStar(require("../controllers/adminController"));
|
||||
const authMiddleware_1 = require("../middlewares/authMiddleware");
|
||||
const router = (0, express_1.Router)();
|
||||
router.use(authMiddleware_1.authenticate, authMiddleware_1.requireAdmin);
|
||||
router.get('/', adminController.getDashboard);
|
||||
router.get('/users', adminController.listUsers);
|
||||
router.post('/users', adminController.createUser);
|
||||
router.put('/users/:id', adminController.updateUser);
|
||||
router.delete('/users/:id', adminController.deleteUser);
|
||||
exports.default = router;
|
||||
42
dist/routes/auth.js
vendored
Normal file
42
dist/routes/auth.js
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const express_1 = require("express");
|
||||
const authController = __importStar(require("../controllers/authController"));
|
||||
const router = (0, express_1.Router)();
|
||||
router.post('/login', authController.login);
|
||||
router.get('/me', authController.me);
|
||||
router.post('/logout', authController.logout);
|
||||
exports.default = router;
|
||||
41
dist/routes/deployments.js
vendored
Normal file
41
dist/routes/deployments.js
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const express_1 = require("express");
|
||||
const deploymentsController = __importStar(require("../controllers/deploymentsController"));
|
||||
const router = (0, express_1.Router)();
|
||||
router.post('/', deploymentsController.createDeployment);
|
||||
router.get('/schema', deploymentsController.getSchema);
|
||||
exports.default = router;
|
||||
28
dist/routes/index.js
vendored
Normal file
28
dist/routes/index.js
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const express_1 = require("express");
|
||||
const auth_1 = __importDefault(require("./auth"));
|
||||
const deployments_1 = __importDefault(require("./deployments"));
|
||||
const admin_1 = __importDefault(require("./admin"));
|
||||
const terraform_1 = __importDefault(require("./terraform"));
|
||||
const router = (0, express_1.Router)();
|
||||
const prisma_1 = __importDefault(require("../utils/prisma"));
|
||||
const logger_1 = __importDefault(require("../utils/logger"));
|
||||
router.get('/healthz', async (req, res) => {
|
||||
try {
|
||||
await prisma_1.default.$queryRaw `SELECT 1`;
|
||||
res.status(200).json({ status: 'ok', database: 'connected' });
|
||||
}
|
||||
catch (error) {
|
||||
logger_1.default.error(`Health check failed: ${error}`);
|
||||
res.status(503).json({ status: 'error', database: 'disconnected' });
|
||||
}
|
||||
});
|
||||
router.use('/auth', auth_1.default);
|
||||
router.use('/deployments', deployments_1.default);
|
||||
router.use('/admin', admin_1.default);
|
||||
router.use('/terraform', terraform_1.default);
|
||||
exports.default = router;
|
||||
43
dist/routes/terraform.js
vendored
Normal file
43
dist/routes/terraform.js
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const express_1 = require("express");
|
||||
const terraformController = __importStar(require("../controllers/terraformController"));
|
||||
const router = (0, express_1.Router)();
|
||||
router.post('/apply', terraformController.apply);
|
||||
router.get('/templates', terraformController.getTemplates);
|
||||
router.get('/formations', terraformController.listFormations);
|
||||
router.delete('/formations/:formationId', terraformController.destroyFormation);
|
||||
exports.default = router;
|
||||
39
dist/services/authService.js
vendored
Normal file
39
dist/services/authService.js
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.verifyToken = exports.signToken = exports.verifyPassword = exports.hashPassword = void 0;
|
||||
const argon2_1 = __importDefault(require("argon2"));
|
||||
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
||||
const config_1 = __importDefault(require("../config"));
|
||||
const hashPassword = async (password) => {
|
||||
return await argon2_1.default.hash(password);
|
||||
};
|
||||
exports.hashPassword = hashPassword;
|
||||
const verifyPassword = async (password, hash) => {
|
||||
try {
|
||||
return await argon2_1.default.verify(hash, password);
|
||||
}
|
||||
catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
exports.verifyPassword = verifyPassword;
|
||||
const signToken = (userId, email) => {
|
||||
return jsonwebtoken_1.default.sign({ sub: userId, email }, config_1.default.jwt.secret, { expiresIn: config_1.default.jwt.ttlSeconds });
|
||||
};
|
||||
exports.signToken = signToken;
|
||||
const verifyToken = (token) => {
|
||||
try {
|
||||
const decoded = jsonwebtoken_1.default.verify(token, config_1.default.jwt.secret);
|
||||
return {
|
||||
userId: decoded.sub,
|
||||
email: decoded.email,
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
exports.verifyToken = verifyToken;
|
||||
34
dist/services/bruteforceService.js
vendored
Normal file
34
dist/services/bruteforceService.js
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
class AuthGuard {
|
||||
constructor() {
|
||||
this.attempts = new Map();
|
||||
}
|
||||
isLocked(key) {
|
||||
const attempt = this.attempts.get(key);
|
||||
if (attempt && attempt.lockUntil) {
|
||||
return Date.now() < attempt.lockUntil;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
recordFailure(key, maxAttempts = 5, lockoutSecs = 300) {
|
||||
let attempt = this.attempts.get(key);
|
||||
const now = Date.now();
|
||||
if (!attempt) {
|
||||
attempt = { count: 0, lockUntil: null };
|
||||
this.attempts.set(key, attempt);
|
||||
}
|
||||
if (attempt.lockUntil && now < attempt.lockUntil) {
|
||||
return;
|
||||
}
|
||||
attempt.count += 1;
|
||||
if (attempt.count >= maxAttempts) {
|
||||
attempt.lockUntil = now + (lockoutSecs * 1000);
|
||||
attempt.count = 0; // Reset count after lock
|
||||
}
|
||||
}
|
||||
recordSuccess(key) {
|
||||
this.attempts.delete(key);
|
||||
}
|
||||
}
|
||||
exports.default = new AuthGuard();
|
||||
90
dist/services/guacamoleService.js
vendored
Normal file
90
dist/services/guacamoleService.js
vendored
Normal file
@ -0,0 +1,90 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.deleteProvisioning = exports.provision = void 0;
|
||||
const guacamoleClient_1 = require("../utils/guacamoleClient");
|
||||
const logger_1 = __importDefault(require("../utils/logger"));
|
||||
// Helper to extract numeric suffix for matching
|
||||
const extractNumericSuffix = (s) => {
|
||||
const match = s.match(/(\d+)$/);
|
||||
return match ? parseInt(match[1], 10) : null;
|
||||
};
|
||||
const matchUserForVm = (vm, users) => {
|
||||
const vmKey = extractNumericSuffix(vm.name);
|
||||
if (vmKey === null)
|
||||
return undefined;
|
||||
return users.find(user => {
|
||||
const userKey = extractNumericSuffix(user.username);
|
||||
return userKey === vmKey;
|
||||
});
|
||||
};
|
||||
const provision = async (formationId, groupName, users, vms) => {
|
||||
if (users.length === 0)
|
||||
return;
|
||||
const session = await guacamoleClient_1.GuacSession.login();
|
||||
let groupIdentifier = groupName || `formation-${formationId}`;
|
||||
// Logic to clean formation ID if groupName is missing
|
||||
if (!groupName) {
|
||||
groupIdentifier = formationId; // Simplified
|
||||
}
|
||||
await session.checkExistence(groupIdentifier, users, vms);
|
||||
await session.ensureUserGroup(groupIdentifier, groupIdentifier);
|
||||
// Create users in parallel
|
||||
await Promise.all(users.map(user => session.createOrUpdateUser(user)));
|
||||
// Create connections in parallel
|
||||
const connectionPromises = vms
|
||||
.filter(vm => vm.ip)
|
||||
.map(async (vm) => {
|
||||
const id = await session.createOrUpdateConnection(groupIdentifier, vm);
|
||||
return { id, vm };
|
||||
});
|
||||
const connections = await Promise.all(connectionPromises);
|
||||
await session.setGroupMembers(groupIdentifier, users);
|
||||
// Grant permissions in parallel
|
||||
const permissionPromises = connections.map(({ id, vm }) => {
|
||||
const user = matchUserForVm(vm, users);
|
||||
if (user) {
|
||||
return session.grantUserConnection(user.username, id);
|
||||
}
|
||||
else {
|
||||
return session.grantGroupConnection(groupIdentifier, id);
|
||||
}
|
||||
});
|
||||
await Promise.all(permissionPromises);
|
||||
};
|
||||
exports.provision = provision;
|
||||
const deleteProvisioning = async (formationId, groupName) => {
|
||||
const session = await guacamoleClient_1.GuacSession.login();
|
||||
const groupIdentifier = groupName || `formation-${formationId}`;
|
||||
try {
|
||||
logger_1.default.info(`Cleaning up Guacamole for group ${groupIdentifier}`);
|
||||
const { users, connections } = await session.getGroupMembers(groupIdentifier);
|
||||
logger_1.default.info(`Found ${users.length} users and ${connections.length} connections to delete for group ${groupIdentifier}`);
|
||||
// Delete users in parallel
|
||||
await Promise.all(users.map(async (user) => {
|
||||
try {
|
||||
await session.deleteUser(user);
|
||||
}
|
||||
catch (e) { }
|
||||
}));
|
||||
// Delete connections in parallel
|
||||
await Promise.all(connections.map(async (conn) => {
|
||||
try {
|
||||
await session.deleteConnection(conn);
|
||||
}
|
||||
catch (e) { }
|
||||
}));
|
||||
try {
|
||||
await session.deleteGroup(groupIdentifier);
|
||||
}
|
||||
catch (e) { }
|
||||
logger_1.default.info(`Guacamole cleanup completed for ${groupIdentifier}`);
|
||||
}
|
||||
catch (error) {
|
||||
logger_1.default.warn(`Error during Guacamole cleanup for ${groupIdentifier}: ${error.message}`);
|
||||
// If group doesn't exist, we might still want to try other cleanups if we implemented them
|
||||
}
|
||||
};
|
||||
exports.deleteProvisioning = deleteProvisioning;
|
||||
37
dist/services/proxmoxService.js
vendored
Normal file
37
dist/services/proxmoxService.js
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.fetchTemplates = void 0;
|
||||
const axios_1 = __importDefault(require("axios"));
|
||||
const https_1 = __importDefault(require("https"));
|
||||
const config_1 = __importDefault(require("../config"));
|
||||
const fetchTemplates = async () => {
|
||||
const { url, targetNode, tokenId, tokenSecret, insecureTls } = config_1.default.proxmox;
|
||||
const apiUrl = `${url.replace(/\/$/, '')}/nodes/${targetNode}/qemu?full=1`;
|
||||
const agent = new https_1.default.Agent({
|
||||
rejectUnauthorized: !insecureTls
|
||||
});
|
||||
try {
|
||||
const response = await axios_1.default.get(apiUrl, {
|
||||
headers: {
|
||||
'Authorization': `PVEAPIToken=${tokenId.trim()}=${tokenSecret.trim()}`
|
||||
},
|
||||
httpsAgent: agent
|
||||
});
|
||||
const templates = response.data.data
|
||||
.filter(vm => vm.template === 1 && vm.vmid)
|
||||
.map(vm => ({
|
||||
vmid: vm.vmid,
|
||||
name: vm.name || `vm-${vm.vmid}`,
|
||||
diskSizeGb: vm.maxdisk ? Math.floor(vm.maxdisk / 1073741824) : 0
|
||||
}));
|
||||
return templates;
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Proxmox fetch templates error:', error);
|
||||
throw new Error('Failed to fetch templates from Proxmox');
|
||||
}
|
||||
};
|
||||
exports.fetchTemplates = fetchTemplates;
|
||||
163
dist/services/terraformService.js
vendored
Normal file
163
dist/services/terraformService.js
vendored
Normal file
@ -0,0 +1,163 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.destroyFormation = exports.handleApplyJob = void 0;
|
||||
const uuid_1 = require("uuid");
|
||||
const path_1 = __importDefault(require("path"));
|
||||
const prisma_1 = __importDefault(require("../utils/prisma"));
|
||||
const config_1 = __importDefault(require("../config"));
|
||||
const tfvarsService = __importStar(require("./tfvarsService"));
|
||||
const shell = __importStar(require("../utils/shell"));
|
||||
const guacamoleService = __importStar(require("./guacamoleService"));
|
||||
const logger_1 = __importDefault(require("../utils/logger"));
|
||||
const prepareRemoteWorkspace = async (formationId) => {
|
||||
const remoteDir = path_1.default.posix.join(config_1.default.terraform.deployBasePath, formationId);
|
||||
await ensureRemoteDirectory(remoteDir);
|
||||
await copyTemplateToRemote(remoteDir);
|
||||
return remoteDir;
|
||||
};
|
||||
const ensureRemoteDirectory = async (remoteDir) => {
|
||||
// Check if exists and cleanup
|
||||
const checkCmd = `test -d ${shell.escapeShell(remoteDir)}`;
|
||||
const checkRes = await shell.executeSsh(checkCmd);
|
||||
if (checkRes.code === 0) {
|
||||
logger_1.default.warn(`Remote directory ${remoteDir} exists. Cleaning up before copy.`);
|
||||
await cleanupRemote(remoteDir);
|
||||
}
|
||||
};
|
||||
const copyTemplateToRemote = async (remoteDir) => {
|
||||
const templateSrc = config_1.default.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)}`;
|
||||
const copyRes = await shell.executeSsh(copyCmd);
|
||||
if (copyRes.code !== 0) {
|
||||
throw new Error(`Remote copy failed: ${copyRes.stderr}`);
|
||||
}
|
||||
};
|
||||
const cleanupRemote = async (remoteDir) => {
|
||||
const cmd = `rm -rf ${shell.escapeShell(remoteDir)}`;
|
||||
await shell.executeSsh(cmd);
|
||||
};
|
||||
const runTerraformCommand = async (remoteDir, command) => {
|
||||
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;
|
||||
};
|
||||
const handleApplyJob = async (formationId, req) => {
|
||||
const remoteDir = await prepareRemoteWorkspace(formationId);
|
||||
try {
|
||||
const tfvars = tfvarsService.generateVmDefinitions(req);
|
||||
// Upload tfvars
|
||||
// We use path.posix.join to ensure forward slashes for remote linux path
|
||||
const uploadRes = await shell.uploadFileSsh(path_1.default.posix.join(remoteDir, 'terraform.tfvars'), tfvars);
|
||||
if (uploadRes.code !== 0) {
|
||||
throw new Error(`Upload tfvars failed: ${uploadRes.stderr}`);
|
||||
}
|
||||
// Terraform init
|
||||
await runTerraformCommand(remoteDir, 'init -input=false -upgrade');
|
||||
// Prepare Terraform Apply Promise
|
||||
const terraformApplyPromise = runTerraformCommand(remoteDir, 'apply -auto-approve -input=false');
|
||||
// Prepare Guacamole Provisioning Promise
|
||||
let guacamolePromise = Promise.resolve();
|
||||
if (req.guacamole) {
|
||||
logger_1.default.info(`Starting Guacamole provisioning for formation ${formationId}`);
|
||||
guacamolePromise = guacamoleService.provision(formationId, req.guacamole.groupName, req.guacamole.users, req.vms.map(vm => ({
|
||||
name: vm.name,
|
||||
ip: vm.guacamoleIp || vm.ip,
|
||||
rdpDomain: vm.rdpDomain,
|
||||
rdpPort: vm.rdpPort,
|
||||
}))).then(() => {
|
||||
logger_1.default.info(`Guacamole provisioning succeeded for formation ${formationId}`);
|
||||
}).catch((error) => {
|
||||
logger_1.default.error(`Guacamole provisioning failed for formation ${formationId}: ${error.message}`);
|
||||
if (error.config)
|
||||
logger_1.default.error(`Failed request: ${error.config.method?.toUpperCase()} ${error.config.url}`);
|
||||
if (error.response)
|
||||
logger_1.default.error(`Response status: ${error.response.status}`);
|
||||
// We might want to re-throw or handle this differently depending on if we want the whole job to fail
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
// Run both in parallel
|
||||
await Promise.all([terraformApplyPromise, guacamolePromise]);
|
||||
// Save to DB
|
||||
await prisma_1.default.terraform_formations.create({
|
||||
data: {
|
||||
id: (0, uuid_1.v4)(),
|
||||
formation_id: formationId,
|
||||
remote_path: remoteDir,
|
||||
guacamole_group_name: req.guacamole?.groupName,
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
// Cleanup on failure if it was created during this job
|
||||
await cleanupRemote(remoteDir);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
exports.handleApplyJob = handleApplyJob;
|
||||
const destroyFormation = async (formationId) => {
|
||||
const formation = await prisma_1.default.terraform_formations.findUnique({
|
||||
where: { formation_id: formationId }
|
||||
});
|
||||
if (!formation) {
|
||||
throw new Error('Formation not found');
|
||||
}
|
||||
const remoteDir = formation.remote_path;
|
||||
// Terraform destroy
|
||||
// We run init first to be safe, though state should be there
|
||||
await runTerraformCommand(remoteDir, 'init -input=false');
|
||||
await runTerraformCommand(remoteDir, 'destroy -auto-approve -input=false -lock=false');
|
||||
// Remove remote dir
|
||||
await cleanupRemote(remoteDir);
|
||||
// Delete from DB
|
||||
await prisma_1.default.terraform_formations.delete({
|
||||
where: { id: formation.id }
|
||||
});
|
||||
// Cleanup Guacamole
|
||||
if (formation.guacamole_group_name || formation.formation_id) {
|
||||
guacamoleService.deleteProvisioning(formation.formation_id, formation.guacamole_group_name || undefined)
|
||||
.catch(err => logger_1.default.error(`Guacamole cleanup failed for ${formation.formation_id}: ${err}`));
|
||||
}
|
||||
};
|
||||
exports.destroyFormation = destroyFormation;
|
||||
76
dist/services/tfvarsService.js
vendored
Normal file
76
dist/services/tfvarsService.js
vendored
Normal file
@ -0,0 +1,76 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.generateVmDefinitions = exports.generate = void 0;
|
||||
const config_1 = __importDefault(require("../config"));
|
||||
const escape = (s) => s.replace(/"/g, '\\"');
|
||||
const generate = (req) => {
|
||||
const basePrefix = '192.168.143';
|
||||
const start = req.ipStartHost || 50;
|
||||
const ips = Array.from({ length: req.userCount }, (_, i) => `${basePrefix}.${start + i}`);
|
||||
let buf = '';
|
||||
buf += `deployment_name = "${escape(req.name)}"\n`;
|
||||
buf += `instance_count = ${req.userCount}\n`;
|
||||
buf += `vm_template = "${escape(req.vmTemplate)}"\n`;
|
||||
buf += `cpu = ${req.cpu}\n`;
|
||||
buf += `ram_mb = ${req.ramMb}\n`;
|
||||
buf += `disk_gb = ${req.diskGb}\n`;
|
||||
buf += `network_cidr = "192.168.143.0/24"\n`;
|
||||
const ipsStr = ips.map(ip => `"${escape(ip)}"`).join(', ');
|
||||
buf += `ips = [${ipsStr}]\n`;
|
||||
return buf;
|
||||
};
|
||||
exports.generate = generate;
|
||||
const generateVmDefinitions = (req) => {
|
||||
const cfg = config_1.default.proxmox;
|
||||
let buf = '';
|
||||
buf += `proxmox_url = "${escape(cfg.url)}"\n`;
|
||||
buf += `proxmox_token_id = "${escape(cfg.tokenId)}"\n`;
|
||||
buf += `proxmox_token_secret = "${escape(cfg.tokenSecret)}"\n`;
|
||||
buf += `proxmox_insecure_tls = ${cfg.insecureTls}\n`;
|
||||
buf += `target_node = "${escape(cfg.targetNode)}"\n`;
|
||||
buf += `model = {\n`;
|
||||
buf += ` "windows" = "${escape(cfg.modelWindows)}"\n`;
|
||||
buf += ` "linux" = "${escape(cfg.modelLinux)}"\n`;
|
||||
buf += `}\n\n`;
|
||||
buf += `vms = {\n`;
|
||||
for (const vm of req.vms) {
|
||||
buf += generateVmBlock(vm, cfg);
|
||||
}
|
||||
buf += `}\n`;
|
||||
return buf;
|
||||
};
|
||||
exports.generateVmDefinitions = generateVmDefinitions;
|
||||
const generateVmBlock = (vm, cfg) => {
|
||||
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, cfg) => {
|
||||
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;
|
||||
};
|
||||
2
dist/types/terraform.js
vendored
Normal file
2
dist/types/terraform.js
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
263
dist/utils/guacamoleClient.js
vendored
Normal file
263
dist/utils/guacamoleClient.js
vendored
Normal file
@ -0,0 +1,263 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.GuacSession = void 0;
|
||||
const axios_1 = __importDefault(require("axios"));
|
||||
const https_1 = __importDefault(require("https"));
|
||||
const config_1 = __importDefault(require("../config"));
|
||||
const DEFAULT_RDP_PORT = 3389;
|
||||
class GuacSession {
|
||||
constructor(client, base, dataSource, token) {
|
||||
this.client = client;
|
||||
this.base = base;
|
||||
this.dataSource = dataSource;
|
||||
this.token = token;
|
||||
}
|
||||
static async login() {
|
||||
const { apiEndpoint, username, password } = config_1.default.guacamole;
|
||||
const base = apiEndpoint.replace(/\/$/, '');
|
||||
const agent = new https_1.default.Agent({
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
const client = axios_1.default.create({
|
||||
httpsAgent: agent,
|
||||
timeout: 20000
|
||||
});
|
||||
const params = new URLSearchParams();
|
||||
params.append('username', username);
|
||||
params.append('password', password);
|
||||
try {
|
||||
const response = await client.post(`${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');
|
||||
}
|
||||
}
|
||||
get authParams() {
|
||||
return {
|
||||
token: this.token,
|
||||
dataSource: this.dataSource
|
||||
};
|
||||
}
|
||||
async checkExistence(group, users, vms) {
|
||||
// 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) {
|
||||
// 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) {
|
||||
// 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, displayName) {
|
||||
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) {
|
||||
if (error.response && error.response.status === 409)
|
||||
return; // Conflict is OK
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async createOrUpdateUser(user) {
|
||||
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) {
|
||||
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, vm) {
|
||||
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 = {
|
||||
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) {
|
||||
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) {
|
||||
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, users) {
|
||||
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, connectionId) {
|
||||
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, connectionId) {
|
||||
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) {
|
||||
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 = [];
|
||||
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) {
|
||||
const url = `${this.base}/session/data/${this.dataSource}/users/${encodeURIComponent(username)}`;
|
||||
await this.client.delete(url, { params: this.authParams });
|
||||
}
|
||||
async deleteGroup(group) {
|
||||
const url = `${this.base}/session/data/${this.dataSource}/userGroups/${encodeURIComponent(group)}`;
|
||||
await this.client.delete(url, { params: this.authParams });
|
||||
}
|
||||
async deleteConnection(identifier) {
|
||||
const url = `${this.base}/session/data/${this.dataSource}/connections/${encodeURIComponent(identifier)}`;
|
||||
await this.client.delete(url, { params: this.authParams });
|
||||
}
|
||||
}
|
||||
exports.GuacSession = GuacSession;
|
||||
22
dist/utils/helpers.js
vendored
Normal file
22
dist/utils/helpers.js
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.clearSessionCookie = exports.setSessionCookie = void 0;
|
||||
const setSessionCookie = (res, token) => {
|
||||
res.cookie('token', token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.COOKIE_SECURE === 'true', // Default to false if not set
|
||||
sameSite: 'lax',
|
||||
maxAge: 3600 * 1000, // 1 hour
|
||||
path: '/'
|
||||
});
|
||||
};
|
||||
exports.setSessionCookie = setSessionCookie;
|
||||
const clearSessionCookie = (res) => {
|
||||
res.clearCookie('token', {
|
||||
httpOnly: true,
|
||||
secure: process.env.COOKIE_SECURE === 'true',
|
||||
sameSite: 'lax',
|
||||
path: '/'
|
||||
});
|
||||
};
|
||||
exports.clearSessionCookie = clearSessionCookie;
|
||||
43
dist/utils/logger.js
vendored
Normal file
43
dist/utils/logger.js
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const winston_1 = __importDefault(require("winston"));
|
||||
const levels = {
|
||||
error: 0,
|
||||
warn: 1,
|
||||
info: 2,
|
||||
http: 3,
|
||||
debug: 4,
|
||||
};
|
||||
const level = () => {
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
const isDevelopment = env === 'development';
|
||||
return isDevelopment ? 'debug' : 'warn';
|
||||
};
|
||||
const colors = {
|
||||
error: 'red',
|
||||
warn: 'yellow',
|
||||
info: 'green',
|
||||
http: 'magenta',
|
||||
debug: 'white',
|
||||
};
|
||||
winston_1.default.addColors(colors);
|
||||
const format = winston_1.default.format.combine(winston_1.default.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }), winston_1.default.format.colorize({ all: true }), winston_1.default.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`));
|
||||
const transports = [
|
||||
new winston_1.default.transports.Console(),
|
||||
new winston_1.default.transports.File({
|
||||
filename: 'logs/error.log',
|
||||
level: 'error',
|
||||
format: winston_1.default.format.json(),
|
||||
}),
|
||||
new winston_1.default.transports.File({ filename: 'logs/all.log', format: winston_1.default.format.json() }),
|
||||
];
|
||||
const logger = winston_1.default.createLogger({
|
||||
level: level(),
|
||||
levels,
|
||||
format,
|
||||
transports,
|
||||
});
|
||||
exports.default = logger;
|
||||
5
dist/utils/prisma.js
vendored
Normal file
5
dist/utils/prisma.js
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const prisma_1 = require("../generated/prisma");
|
||||
const prisma = new prisma_1.PrismaClient();
|
||||
exports.default = prisma;
|
||||
111
dist/utils/shell.js
vendored
Normal file
111
dist/utils/shell.js
vendored
Normal file
@ -0,0 +1,111 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.copyDirectorySsh = exports.rsync = exports.uploadFileSsh = exports.executeSsh = exports.escapeShell = exports.executeCommand = void 0;
|
||||
const child_process_1 = require("child_process");
|
||||
const config_1 = __importDefault(require("../config"));
|
||||
const executeCommand = (command, args, input) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = (0, child_process_1.spawn)(command, args);
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
if (input && child.stdin) {
|
||||
child.stdin.write(input);
|
||||
child.stdin.end();
|
||||
}
|
||||
child.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
child.on('close', (code) => {
|
||||
resolve({ code, stdout, stderr });
|
||||
});
|
||||
child.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
exports.executeCommand = executeCommand;
|
||||
const escapeShell = (arg) => {
|
||||
return `'${arg.replace(/'/g, "'\\''")}'`;
|
||||
};
|
||||
exports.escapeShell = escapeShell;
|
||||
const ALLOWED_SSH_COMMANDS = ['terraform', 'mkdir', 'cp', 'cd', 'rm', 'test', 'cat'];
|
||||
const validateSshCommand = (cmd) => {
|
||||
// Split by common shell operators: &&, ||, ;, |
|
||||
// This is a basic parser to catch obvious unauthorized commands.
|
||||
const parts = cmd.split(/&&|\|\||;|\|/);
|
||||
for (const part of parts) {
|
||||
const trimmed = part.trim();
|
||||
if (!trimmed)
|
||||
continue;
|
||||
// Get the first word (command executable)
|
||||
// We handle cases like "VAR=val cmd" by ignoring leading vars?
|
||||
// For now, we assume standard "cmd args" format as used in the app.
|
||||
const command = trimmed.split(/\s+/)[0];
|
||||
if (!ALLOWED_SSH_COMMANDS.includes(command)) {
|
||||
throw new Error(`Security Error: Command '${command}' is not allowed via SSH. Allowed: ${ALLOWED_SSH_COMMANDS.join(', ')}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
const executeSsh = async (remoteCmd) => {
|
||||
validateSshCommand(remoteCmd);
|
||||
const args = [
|
||||
'-i', config_1.default.terraform.sshKeyPath,
|
||||
'-o', 'StrictHostKeyChecking=no',
|
||||
'-o', 'UserKnownHostsFile=/dev/null',
|
||||
'-o', 'LogLevel=ERROR',
|
||||
`${config_1.default.terraform.sshUser}@${config_1.default.terraform.sshHost}`,
|
||||
remoteCmd
|
||||
];
|
||||
return (0, exports.executeCommand)('ssh', args);
|
||||
};
|
||||
exports.executeSsh = executeSsh;
|
||||
const uploadFileSsh = async (remotePath, content) => {
|
||||
const escapedPath = (0, exports.escapeShell)(remotePath);
|
||||
const args = [
|
||||
'-i', config_1.default.terraform.sshKeyPath,
|
||||
'-o', 'StrictHostKeyChecking=no',
|
||||
'-o', 'UserKnownHostsFile=/dev/null',
|
||||
'-o', 'LogLevel=ERROR',
|
||||
`${config_1.default.terraform.sshUser}@${config_1.default.terraform.sshHost}`,
|
||||
`cat > ${escapedPath}`
|
||||
];
|
||||
return (0, exports.executeCommand)('ssh', args, content);
|
||||
};
|
||||
exports.uploadFileSsh = uploadFileSsh;
|
||||
const rsync = async (src, dest) => {
|
||||
const sshCmd = `ssh -i ${config_1.default.terraform.sshKeyPath} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null`;
|
||||
const remoteDest = `${config_1.default.terraform.sshUser}@${config_1.default.terraform.sshHost}:${dest}`;
|
||||
const args = [
|
||||
'-az',
|
||||
'--delete',
|
||||
'-e', sshCmd,
|
||||
src,
|
||||
remoteDest
|
||||
];
|
||||
return (0, exports.executeCommand)('rsync', args);
|
||||
};
|
||||
exports.rsync = rsync;
|
||||
const copyDirectorySsh = async (src, dest) => {
|
||||
// We use scp -r to copy the directory.
|
||||
// Note: If dest does not exist, scp copies src as dest.
|
||||
// If dest exists, scp copies src INTO dest (creating dest/src).
|
||||
// The caller should ensure dest does not exist if they want 'src' content to be 'dest'.
|
||||
const remoteDest = `${config_1.default.terraform.sshUser}@${config_1.default.terraform.sshHost}:${dest}`;
|
||||
const args = [
|
||||
'-i', config_1.default.terraform.sshKeyPath,
|
||||
'-o', 'StrictHostKeyChecking=no',
|
||||
'-o', 'UserKnownHostsFile=/dev/null',
|
||||
'-o', 'LogLevel=ERROR',
|
||||
'-r',
|
||||
src,
|
||||
remoteDest
|
||||
];
|
||||
return (0, exports.executeCommand)('scp', args);
|
||||
};
|
||||
exports.copyDirectorySsh = copyDirectorySsh;
|
||||
@ -17,6 +17,7 @@ const vmSchema = z.object({
|
||||
disk_size: z.number(),
|
||||
template: z.string(),
|
||||
ip: z.string(),
|
||||
guacamoleIp: z.string().optional(),
|
||||
bridge: z.string().optional(),
|
||||
model: z.enum(['windows', 'linux']).default('linux'),
|
||||
rdpDomain: z.string().optional(),
|
||||
@ -50,6 +51,7 @@ const isValidFormationId = (id: string): boolean => {
|
||||
|
||||
export const apply = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
logger.info(`Apply request body: ${JSON.stringify(req.body)}`);
|
||||
const body = applySchema.parse(req.body);
|
||||
let formationId = cleanFormationId(body.formationId);
|
||||
|
||||
|
||||
@ -75,16 +75,19 @@ export const handleApplyJob = async (formationId: string, req: ApplyRequest) =>
|
||||
let guacamolePromise: Promise<void> = Promise.resolve();
|
||||
if (req.guacamole) {
|
||||
logger.info(`Starting Guacamole provisioning for formation ${formationId}`);
|
||||
const guacamoleVms = req.vms.map(vm => ({
|
||||
name: vm.name,
|
||||
ip: vm.guacamoleIp || vm.ip,
|
||||
rdpDomain: vm.rdpDomain,
|
||||
rdpPort: vm.rdpPort,
|
||||
}));
|
||||
logger.info(`Provisioning Guacamole with VMs: ${JSON.stringify(guacamoleVms)}`);
|
||||
|
||||
guacamolePromise = guacamoleService.provision(
|
||||
formationId,
|
||||
req.guacamole.groupName,
|
||||
req.guacamole.users,
|
||||
req.vms.map(vm => ({
|
||||
name: vm.name,
|
||||
ip: vm.ip,
|
||||
rdpDomain: vm.rdpDomain,
|
||||
rdpPort: vm.rdpPort,
|
||||
}))
|
||||
guacamoleVms
|
||||
).then(() => {
|
||||
logger.info(`Guacamole provisioning succeeded for formation ${formationId}`);
|
||||
}).catch((error: any) => {
|
||||
|
||||
@ -6,6 +6,7 @@ export interface VmDefinition {
|
||||
disk_size: number;
|
||||
template: string;
|
||||
ip: string;
|
||||
guacamoleIp?: string;
|
||||
bridge?: string;
|
||||
model: 'windows' | 'linux';
|
||||
rdpDomain?: string;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user