Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f08de80144 | |||
| 2838b70a27 | |||
| dc38b80db1 | |||
| 5ce6abe268 | |||
| b33d2a51d2 | |||
| cacba65fa2 | |||
| 0a65d1b5ed | |||
| f071e9e609 | |||
| 80a965228b | |||
|
|
c99c1809c4 | ||
|
|
be23cf5891 | ||
|
|
74b0639ac6 | ||
|
|
741da58194 | ||
|
|
1202357a32 | ||
|
|
e5268b1c43 | ||
|
|
89c2029cbc | ||
|
|
a647109cc2 | ||
|
|
e400dd8431 | ||
|
|
4f7cb4fb2c | ||
|
|
ce8d9ec9c4 | ||
|
|
7cf90e4633 | ||
|
|
d07963fc5d | ||
|
|
7bc47bb156 | ||
|
|
ca556278db | ||
| 8d501a22d7 | |||
| 62930463b0 | |||
| 0c6d6d8c35 | |||
| 753d5b54fe | |||
| 1d0646472c | |||
| fab2c15059 | |||
| 0cc99324e9 | |||
| d74fa6ddf7 | |||
| 9612ba3f3a | |||
| 3478db2062 | |||
| a2b23e8fa1 | |||
| 89d3d876ed | |||
| 41fd95aaf5 | |||
| 9880f89dff | |||
| bdaf95090c | |||
| 6b43e097c4 | |||
| f5e1c2ccb9 | |||
| 23ed25a5d1 | |||
| 3e539d8f96 | |||
| e7a8ca5eaf | |||
| d5d9b2ce92 | |||
| f913a39713 | |||
| 53f5f9849d | |||
| 624a86a9c1 | |||
| c7a5e76c4c | |||
| a9620edaa1 | |||
| 7534e8c825 | |||
| 97849f6079 | |||
| 08268e6665 | |||
| 5e89b9a4d1 | |||
| 69648caa08 | |||
| 408093c7b6 | |||
| 5e633a6e56 | |||
| 1ddef6114c | |||
| 7c1d4b048c | |||
| d8c4982d49 | |||
| 0c0bb8deba | |||
| c909f37fad | |||
| 8dd2ac0265 | |||
| bba5834204 | |||
| 08612e7ce5 | |||
| 2c660b2d89 | |||
| 8e5b78ce77 | |||
| d08818a86c |
18
.gitignore
vendored
18
.gitignore
vendored
@ -1,17 +1,5 @@
|
||||
# Rust target
|
||||
target/
|
||||
**/*.rs.bk
|
||||
|
||||
# Env files
|
||||
node_modules
|
||||
# Keep environment variables out of version control
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Terraform drop/output directories
|
||||
terraform_drop/
|
||||
|
||||
# OS/editor junk
|
||||
.DS_Store
|
||||
*.swp
|
||||
.idea/
|
||||
.vscode/
|
||||
/src/generated/prisma
|
||||
|
||||
3099
Cargo.lock
generated
3099
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
27
Cargo.toml
27
Cargo.toml
@ -1,27 +0,0 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/api",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
authors = ["Corenthin"]
|
||||
license = "MIT"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
axum = "0.7"
|
||||
config = "0.14"
|
||||
dotenvy = "0.15"
|
||||
rand = { version = "0.8", features = ["std"] }
|
||||
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
schemars = "0.8"
|
||||
sqlx = { version = "0.7", features = ["runtime-tokio", "mysql", "chrono", "uuid"] }
|
||||
thiserror = "1"
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread", "process"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
30
Dockerfile
Normal file
30
Dockerfile
Normal file
@ -0,0 +1,30 @@
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
COPY prisma ./prisma/
|
||||
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npx prisma generate
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/package*.json ./
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/prisma ./prisma
|
||||
COPY --from=builder /app/src/generated ./dist/generated
|
||||
|
||||
RUN npm ci --only=production
|
||||
RUN apk add --no-cache openssh-client rsync gcompat openssl
|
||||
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["sh", "-c", "echo \"$SSH_PRIVATE_KEY_BASE64\" | base64 -d > /tmp/id_rsa && chmod 600 /tmp/id_rsa && npx prisma db push && npm start"]
|
||||
115
Jenkinsfile
vendored
Normal file
115
Jenkinsfile
vendored
Normal file
@ -0,0 +1,115 @@
|
||||
pipeline {
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
}
|
||||
agent any
|
||||
environment {
|
||||
SERVICE_NAME = 'ApiAutoDeploy'
|
||||
DOCKER_REGISTRY_URL = 'http://172.75.13.27:3000/corenthin/apiautodeploy:latest'
|
||||
DOCKER_TAG = '172.75.13.27:3000/corenthin/apiautodeploy:latest'
|
||||
DEPLOY_HOST = '172.75.13.5'
|
||||
DEPLOY_CREDENTIALS_ID = '1000'
|
||||
DATABASE_URL_SECRET = '1001'
|
||||
TERRAFORM_SSH_KEY_CREDENTIAL_ID = 'terraform-ssh-key'
|
||||
PROXMOX_CREDENTIALS_ID = 'proxmox-api-credentials'
|
||||
GUACAMOLE_CREDENTIALS_ID = 'guacamole-api-credentials'
|
||||
JWT_SECRET_ID = 'api-jwt-secret'
|
||||
}
|
||||
stages {
|
||||
stage('Checkout Code') {
|
||||
steps {
|
||||
checkout scm
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build Docker Image') {
|
||||
steps {
|
||||
script {
|
||||
env.IMAGE_TAG = "${env.DOCKER_TAG}"
|
||||
|
||||
echo "Construction de l'image Docker pour l'API TS avec le tag : ${env.IMAGE_TAG}"
|
||||
|
||||
sh "docker build -t ${env.IMAGE_TAG} ."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Push Docker Image') {
|
||||
steps {
|
||||
withCredentials([usernamePassword(credentialsId: 'docker-registry-credentials', passwordVariable: 'DOCKER_PASSWORD', usernameVariable: 'DOCKER_USERNAME')]) {
|
||||
script {
|
||||
def registryHostPort = env.DOCKER_REGISTRY_URL.split('//')[1].split('/')[0]
|
||||
|
||||
sh "echo \$DOCKER_PASSWORD | docker login -u \$DOCKER_USERNAME --password-stdin 172.75.13.27:3000"
|
||||
}
|
||||
|
||||
sh "docker push ${env.IMAGE_TAG}"
|
||||
sh "docker rmi ${env.IMAGE_TAG}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Deploy on VM') {
|
||||
steps {
|
||||
withCredentials([
|
||||
string(credentialsId: env.DATABASE_URL_SECRET, variable: 'DATABASE_URL_SECRET'),
|
||||
sshUserPrivateKey(credentialsId: env.TERRAFORM_SSH_KEY_CREDENTIAL_ID, keyFileVariable: 'SSH_KEY_FILE'),
|
||||
usernamePassword(credentialsId: env.PROXMOX_CREDENTIALS_ID, usernameVariable: 'PROXMOX_TOKEN_ID', passwordVariable: 'PROXMOX_TOKEN_SECRET'),
|
||||
usernamePassword(credentialsId: env.GUACAMOLE_CREDENTIALS_ID, usernameVariable: 'GUACAMOLE_USERNAME', passwordVariable: 'GUACAMOLE_PASSWORD'),
|
||||
string(credentialsId: env.JWT_SECRET_ID, variable: 'JWT_SECRET'),
|
||||
string(credentialsId: 'proxmox-url', variable: 'PROXMOX_URL'),
|
||||
string(credentialsId: 'guacamole-url', variable: 'GUACAMOLE_API_ENDPOINT')
|
||||
]) {
|
||||
script {
|
||||
def sshKeyBase64 = sh(script: "base64 -w 0 ${SSH_KEY_FILE}", returnStdout: true).trim()
|
||||
|
||||
def deployScript = """
|
||||
docker pull ${env.IMAGE_TAG}
|
||||
docker rm -f ${env.SERVICE_NAME} || true
|
||||
|
||||
echo "Démarrage du nouveau container..."
|
||||
docker run -d --name ${env.SERVICE_NAME} \\
|
||||
-p 4447:3000 \\
|
||||
-v /home/corenthin/terraform:/app/templates:ro \\
|
||||
-e DATABASE_URL="${DATABASE_URL_SECRET}" \\
|
||||
-e JWT_SECRET="${JWT_SECRET}" \\
|
||||
-e PROXMOX_URL="${PROXMOX_URL}" \\
|
||||
-e PROXMOX_TOKEN_ID="${PROXMOX_TOKEN_ID}" \\
|
||||
-e PROXMOX_TOKEN_SECRET="${PROXMOX_TOKEN_SECRET}" \\
|
||||
-e PROXMOX_INSECURE_TLS="true" \\
|
||||
-e GUACAMOLE_API_ENDPOINT="${GUACAMOLE_API_ENDPOINT}" \\
|
||||
-e GUACAMOLE_USERNAME="${GUACAMOLE_USERNAME}" \\
|
||||
-e GUACAMOLE_PASSWORD="${GUACAMOLE_PASSWORD}" \\
|
||||
-e TERRAFORM_SSH_KEY_PATH="/tmp/id_rsa" \\
|
||||
-e SSH_PRIVATE_KEY_BASE64="${sshKeyBase64}" \\
|
||||
-e TERRAFORM_TEMPLATE_PATH="/app/templates" \\
|
||||
-e TERRAFORM_SSH_HOST="${env.DEPLOY_HOST}" \\
|
||||
${env.IMAGE_TAG}
|
||||
|
||||
docker image prune -f --filter "until=24h"
|
||||
"""
|
||||
|
||||
sshagent(credentials: [env.DEPLOY_CREDENTIALS_ID]) {
|
||||
sh "ssh -o StrictHostKeyChecking=no corenthin@${env.DEPLOY_HOST} \"${deployScript}\""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Cleanup') {
|
||||
steps {
|
||||
cleanWs()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
always {
|
||||
echo "Pipeline Backend TS terminée. Image déployée: ${env.IMAGE_TAG}"
|
||||
}
|
||||
failure {
|
||||
echo '!!! ÉCHEC de la pipeline Backend TS !!!'
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
[service]
|
||||
host = "127.0.0.1"
|
||||
port = 3000
|
||||
|
||||
[database]
|
||||
url = "mysql://user:password@localhost:3306/apiprojetsolyti"
|
||||
max_connections = 10
|
||||
|
||||
|
||||
[guacamole]
|
||||
api_endpoint = "https://guacamole.local/api"
|
||||
username = ""
|
||||
password = ""
|
||||
|
||||
[pfsense]
|
||||
api_endpoint = "https://pfsense.local/api/v1"
|
||||
api_key = ""
|
||||
api_secret = ""
|
||||
|
||||
[terraform]
|
||||
# Local drop directory where API writes generated tfvars files
|
||||
drop_dir = "terraform_drop"
|
||||
1
crates/api/.gitignore
vendored
1
crates/api/.gitignore
vendored
@ -1 +0,0 @@
|
||||
/target
|
||||
@ -1,22 +0,0 @@
|
||||
[package]
|
||||
name = "api"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Automation API for Guacamole and pfSense orchestration with Terraform tfvars generation"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
axum = { workspace = true }
|
||||
config = { workspace = true }
|
||||
dotenvy = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
sqlx = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
@ -1,30 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use std::net::SocketAddr;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
use crate::{config::AppConfig, routes};
|
||||
|
||||
pub async fn run() -> Result<()> {
|
||||
init_tracing();
|
||||
|
||||
let cfg = AppConfig::load()?;
|
||||
let router = routes::build_router(cfg.clone()).await?;
|
||||
|
||||
let addr: SocketAddr = format!("{}:{}", cfg.service.host, cfg.service.port).parse()?;
|
||||
tracing::info!(%addr, "Starting HTTP server");
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
axum::serve(listener, router).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_tracing() {
|
||||
let filter_layer = tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
|
||||
let fmt_layer = tracing_subscriber::fmt::layer();
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(filter_layer)
|
||||
.with(fmt_layer)
|
||||
.init();
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct DatabaseConfig {
|
||||
pub url: String,
|
||||
pub max_connections: u32,
|
||||
}
|
||||
|
||||
impl Default for DatabaseConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
url: "mysql://user:password@localhost:3306/apiprojetsolyti".to_string(),
|
||||
max_connections: 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,79 +0,0 @@
|
||||
pub mod database;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct AppConfig {
|
||||
pub service: ServiceConfig,
|
||||
pub database: database::DatabaseConfig,
|
||||
pub guacamole: GuacamoleConfig,
|
||||
pub pfsense: PfSenseConfig,
|
||||
pub terraform: TerraformConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ServiceConfig {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct GuacamoleConfig {
|
||||
pub api_endpoint: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct PfSenseConfig {
|
||||
pub api_endpoint: String,
|
||||
pub api_key: String,
|
||||
pub api_secret: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct TerraformConfig {
|
||||
pub drop_dir: String,
|
||||
}
|
||||
|
||||
impl Default for AppConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
service: ServiceConfig {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 3000,
|
||||
},
|
||||
database: database::DatabaseConfig::default(),
|
||||
guacamole: GuacamoleConfig {
|
||||
api_endpoint: String::new(),
|
||||
username: String::new(),
|
||||
password: String::new(),
|
||||
},
|
||||
pfsense: PfSenseConfig {
|
||||
api_endpoint: String::new(),
|
||||
api_key: String::new(),
|
||||
api_secret: String::new(),
|
||||
},
|
||||
terraform: TerraformConfig {
|
||||
drop_dir: "terraform_drop".to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn load() -> anyhow::Result<Self> {
|
||||
let _ = dotenvy::dotenv();
|
||||
|
||||
let builder = config::Config::builder()
|
||||
.add_source(config::File::with_name("config/default.toml").required(false))
|
||||
.add_source(
|
||||
config::Environment::with_prefix("APP")
|
||||
.try_parsing(true)
|
||||
.separator("__"),
|
||||
);
|
||||
|
||||
let cfg = builder.build()?;
|
||||
cfg.try_deserialize().map_err(Into::into)
|
||||
}
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Deployment {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub user_count: u32,
|
||||
}
|
||||
|
||||
impl Deployment {
|
||||
pub fn new(name: impl Into<String>, user_count: u32) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
name: name.into(),
|
||||
user_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
pub mod deployments;
|
||||
pub mod users;
|
||||
|
||||
pub use deployments::Deployment;
|
||||
pub use users::{GuacamoleUser, VmAssignment};
|
||||
@ -1,26 +0,0 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GuacamoleUser {
|
||||
pub id: Uuid,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VmAssignment {
|
||||
pub user_id: Uuid,
|
||||
pub vm_id: Uuid,
|
||||
pub ip_address: String,
|
||||
pub mac_address: String,
|
||||
}
|
||||
|
||||
impl GuacamoleUser {
|
||||
pub fn new(username: impl Into<String>, password: impl Into<String>) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
username: username.into(),
|
||||
password: password.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use sqlx::mysql::MySqlPoolOptions;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use crate::config::database::DatabaseConfig;
|
||||
|
||||
pub type DatabasePool = MySqlPool;
|
||||
|
||||
pub async fn create_pool(config: &DatabaseConfig) -> Result<DatabasePool> {
|
||||
let pool = MySqlPoolOptions::new()
|
||||
.max_connections(config.max_connections)
|
||||
.connect(&config.url)
|
||||
.await?;
|
||||
|
||||
Ok(pool)
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::config::GuacamoleConfig;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GuacamoleClient {
|
||||
config: GuacamoleConfig,
|
||||
}
|
||||
|
||||
impl GuacamoleClient {
|
||||
pub fn new(config: GuacamoleConfig) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
|
||||
pub async fn health_check(&self) -> Result<()> {
|
||||
tracing::debug!(endpoint = %self.config.api_endpoint, "Guacamole client health check placeholder");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
pub mod database;
|
||||
pub mod guacamole_client;
|
||||
pub mod pf_sense_client;
|
||||
pub mod terraform_executor;
|
||||
|
||||
pub use database::DatabasePool;
|
||||
@ -1,19 +0,0 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::config::PfSenseConfig;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PfSenseClient {
|
||||
config: PfSenseConfig,
|
||||
}
|
||||
|
||||
impl PfSenseClient {
|
||||
pub fn new(config: PfSenseConfig) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
|
||||
pub async fn push_mapping(&self) -> Result<()> {
|
||||
tracing::debug!(endpoint = %self.config.api_endpoint, "pfSense client mapping placeholder");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use tokio::process::Command;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct TerraformExecutor;
|
||||
|
||||
impl TerraformExecutor {
|
||||
pub async fn apply(&self, working_dir: &str) -> Result<()> {
|
||||
run_command("apply", working_dir).await
|
||||
}
|
||||
|
||||
pub async fn destroy(&self, working_dir: &str) -> Result<()> {
|
||||
run_command("destroy", working_dir).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_command(subcommand: &str, working_dir: &str) -> Result<()> {
|
||||
tracing::debug!(subcommand, working_dir, "Terraform command placeholder");
|
||||
|
||||
let status = Command::new("terraform")
|
||||
.arg(subcommand)
|
||||
.arg("-auto-approve")
|
||||
.current_dir(working_dir)
|
||||
.status()
|
||||
.await?;
|
||||
|
||||
if !status.success() {
|
||||
anyhow::bail!("Terraform command `{}` failed", subcommand);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
mod bootstrap;
|
||||
pub mod config;
|
||||
pub mod domain;
|
||||
pub mod infrastructure;
|
||||
pub mod routes;
|
||||
pub mod services;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
bootstrap::run().await
|
||||
}
|
||||
@ -1,118 +0,0 @@
|
||||
use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{config::AppConfig, services::tfvars::TfvarsGenerator};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub config: AppConfig,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(config: AppConfig) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeploymentRequest {
|
||||
pub name: String,
|
||||
pub user_count: u32,
|
||||
// VM characteristics
|
||||
pub vm_template: String,
|
||||
pub cpu: u32,
|
||||
pub ram_mb: u32,
|
||||
pub disk_gb: u32,
|
||||
// Optional starting host offset within 192.168.143.0/24
|
||||
pub ip_start_host: Option<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeploymentResponse {
|
||||
pub id: Uuid,
|
||||
pub tfvars_path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ErrorResponse {
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
pub async fn create_deployment(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<DeploymentRequest>,
|
||||
) -> impl IntoResponse {
|
||||
if let Err(msg) = validate_request(&payload) {
|
||||
return (StatusCode::BAD_REQUEST, Json(ErrorResponse { error: msg })).into_response();
|
||||
}
|
||||
|
||||
let id = Uuid::new_v4();
|
||||
|
||||
let tfvars = TfvarsGenerator::generate(&payload);
|
||||
let drop_dir = &state.config.terraform.drop_dir;
|
||||
let filename = format!("{}-{}.tfvars", payload.name.replace(' ', "_"), id);
|
||||
let path = format!("{}/{}", drop_dir, filename);
|
||||
|
||||
if let Err(e) = write_tfvars(&path, &tfvars).await {
|
||||
tracing::error!(error = %e, %path, "Failed to write tfvars file");
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse {
|
||||
error: "Failed to write tfvars file".into(),
|
||||
}),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
Json(DeploymentResponse {
|
||||
id,
|
||||
tfvars_path: path,
|
||||
})
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn write_tfvars(path: &str, content: &str) -> anyhow::Result<()> {
|
||||
if let Some(parent) = std::path::Path::new(path).parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
tokio::fs::write(path, content).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_request(req: &DeploymentRequest) -> Result<(), String> {
|
||||
if req.name.trim().is_empty() {
|
||||
return Err("name must not be empty".into());
|
||||
}
|
||||
if req.user_count == 0 {
|
||||
return Err("user_count must be >= 1".into());
|
||||
}
|
||||
if req.cpu == 0 {
|
||||
return Err("cpu must be >= 1".into());
|
||||
}
|
||||
if req.ram_mb < 256 {
|
||||
return Err("ram_mb must be >= 256".into());
|
||||
}
|
||||
if req.disk_gb < 10 {
|
||||
return Err("disk_gb must be >= 10".into());
|
||||
}
|
||||
|
||||
let start = req.ip_start_host.unwrap_or(50) as u32;
|
||||
if start < 2 || start > 254 {
|
||||
return Err("ip_start_host must be in [2, 254]".into());
|
||||
}
|
||||
let last = start + req.user_count.saturating_sub(1) as u32;
|
||||
if last > 254 {
|
||||
return Err("ip range exceeds 192.168.143.254".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn schema() -> impl IntoResponse {
|
||||
let schema = schemars::schema_for!(DeploymentRequest);
|
||||
Json(schema)
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
use axum::{response::IntoResponse, Json};
|
||||
use serde_json::json;
|
||||
|
||||
pub async fn healthz() -> impl IntoResponse {
|
||||
Json(json!({"status": "ok"}))
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use axum::{
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
|
||||
use crate::config::AppConfig;
|
||||
|
||||
pub mod deployments;
|
||||
pub mod health;
|
||||
|
||||
pub async fn build_router(cfg: AppConfig) -> Result<Router> {
|
||||
let app_state = deployments::AppState::new(cfg);
|
||||
|
||||
let router = Router::new()
|
||||
.route("/healthz", get(health::healthz))
|
||||
.route("/deployments", post(deployments::create_deployment))
|
||||
.route("/deployments/schema", get(deployments::schema))
|
||||
.with_state(app_state);
|
||||
|
||||
Ok(router)
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
|
||||
pub struct CredentialGenerator;
|
||||
|
||||
impl CredentialGenerator {
|
||||
pub fn random_password(length: usize) -> Result<String> {
|
||||
if length < 8 {
|
||||
anyhow::bail!("password length too short");
|
||||
}
|
||||
|
||||
let password: String = rand::thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(length)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
|
||||
Ok(password)
|
||||
}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::config::GuacamoleConfig;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GuacamoleService {
|
||||
config: GuacamoleConfig,
|
||||
}
|
||||
|
||||
impl GuacamoleService {
|
||||
pub fn new(config: GuacamoleConfig) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
|
||||
pub async fn create_users(&self) -> Result<()> {
|
||||
tracing::debug!(endpoint = %self.config.api_endpoint, "Guacamole user creation placeholder");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
pub mod credentials;
|
||||
pub mod guacamole;
|
||||
pub mod pf_sense;
|
||||
pub mod terraform;
|
||||
pub mod tfvars;
|
||||
|
||||
pub use credentials::CredentialGenerator;
|
||||
@ -1,19 +0,0 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::config::PfSenseConfig;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PfSenseService {
|
||||
config: PfSenseConfig,
|
||||
}
|
||||
|
||||
impl PfSenseService {
|
||||
pub fn new(config: PfSenseConfig) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
|
||||
pub async fn push_static_mapping(&self) -> Result<()> {
|
||||
tracing::debug!(endpoint = %self.config.api_endpoint, "pfSense mapping placeholder");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
use anyhow::Result;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct TerraformService;
|
||||
|
||||
impl TerraformService {
|
||||
pub async fn apply(&self) -> Result<()> {
|
||||
tracing::debug!("Terraform apply placeholder");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn destroy(&self) -> Result<()> {
|
||||
tracing::debug!("Terraform destroy placeholder");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
use crate::routes::deployments::DeploymentRequest;
|
||||
|
||||
pub struct TfvarsGenerator;
|
||||
|
||||
impl TfvarsGenerator {
|
||||
pub fn generate(req: &DeploymentRequest) -> String {
|
||||
// network is always 192.168.143.0/24 per requirements
|
||||
let base_prefix = "192.168.143";
|
||||
let start = req.ip_start_host.unwrap_or(50); // default to .50
|
||||
|
||||
let ips: Vec<String> = (0..req.user_count)
|
||||
.map(|i| format!("{}.{}", base_prefix, start as u32 + i as u32))
|
||||
.collect();
|
||||
|
||||
let mut buf = String::new();
|
||||
|
||||
push_kv(&mut buf, "deployment_name", &req.name);
|
||||
push_kv_num(&mut buf, "instance_count", req.user_count);
|
||||
push_kv(&mut buf, "vm_template", &req.vm_template);
|
||||
push_kv_num(&mut buf, "cpu", req.cpu);
|
||||
push_kv_num(&mut buf, "ram_mb", req.ram_mb);
|
||||
push_kv_num(&mut buf, "disk_gb", req.disk_gb);
|
||||
push_kv(&mut buf, "network_cidr", "192.168.143.0/24");
|
||||
push_list(&mut buf, "ips", &ips);
|
||||
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
fn push_kv(buf: &mut String, key: &str, val: &str) {
|
||||
use std::fmt::Write as _;
|
||||
let _ = writeln!(buf, "{} = \"{}\"", key, escape(val));
|
||||
}
|
||||
|
||||
fn push_kv_num<T: std::fmt::Display>(buf: &mut String, key: &str, val: T) {
|
||||
use std::fmt::Write as _;
|
||||
let _ = writeln!(buf, "{} = {}", key, val);
|
||||
}
|
||||
|
||||
fn push_list(buf: &mut String, key: &str, values: &[String]) {
|
||||
use std::fmt::Write as _;
|
||||
let inner = values
|
||||
.iter()
|
||||
.map(|v| format!("\"{}\"", escape(v)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let _ = writeln!(buf, "{} = [{}]", key, inner);
|
||||
}
|
||||
|
||||
fn escape(s: &str) -> String {
|
||||
s.replace('"', "\\\"")
|
||||
}
|
||||
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;
|
||||
177
dist/controllers/terraformController.js
vendored
Normal file
177
dist/controllers/terraformController.js
vendored
Normal file
@ -0,0 +1,177 @@
|
||||
"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(),
|
||||
guacamole_ip: 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(),
|
||||
}).transform(data => ({
|
||||
...data,
|
||||
guacamoleIp: data.guacamoleIp || data.guacamole_ip
|
||||
}));
|
||||
const guacamoleUserSchema = zod_1.z.object({
|
||||
username: zod_1.z.string(),
|
||||
password: zod_1.z.string(),
|
||||
ip: zod_1.z.string().optional(),
|
||||
displayName: zod_1.z.string().optional(),
|
||||
});
|
||||
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 {
|
||||
logger_1.default.info(`Apply request body: ${JSON.stringify(req.body)}`);
|
||||
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;
|
||||
184
dist/services/terraformService.js
vendored
Normal file
184
dist/services/terraformService.js
vendored
Normal file
@ -0,0 +1,184 @@
|
||||
"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"));
|
||||
// Helper to extract numeric suffix for matching (e.g. "USER-1" -> 1)
|
||||
const extractNumericSuffix = (s) => {
|
||||
const match = s.match(/(\d+)$/);
|
||||
return match ? parseInt(match[1], 10) : null;
|
||||
};
|
||||
const matchUserForVm = (vmName, users) => {
|
||||
const vmKey = extractNumericSuffix(vmName);
|
||||
if (vmKey === null)
|
||||
return undefined;
|
||||
return users.find(user => {
|
||||
const userKey = extractNumericSuffix(user.username);
|
||||
return userKey === vmKey;
|
||||
});
|
||||
};
|
||||
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}`);
|
||||
const guacamoleVms = req.vms.map(vm => {
|
||||
// Try to find matching user to get specific IP (e.g. for static IP assignment via UI)
|
||||
const user = matchUserForVm(vm.name, req.guacamole.users);
|
||||
const targetIp = user?.ip || vm.guacamoleIp || vm.ip;
|
||||
return {
|
||||
name: vm.name,
|
||||
ip: targetIp,
|
||||
rdpDomain: vm.rdpDomain,
|
||||
rdpPort: vm.rdpPort,
|
||||
};
|
||||
});
|
||||
logger_1.default.info(`Provisioning Guacamole with VMs: ${JSON.stringify(guacamoleVms)}`);
|
||||
guacamolePromise = guacamoleService.provision(formationId, req.guacamole.groupName, req.guacamole.users, guacamoleVms).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;
|
||||
314
logs/all.log
Normal file
314
logs/all.log
Normal file
@ -0,0 +1,314 @@
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mServer running on port 3000\u001b[39m","timestamp":"2025-11-28 18:54:45:5445"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mServer running on port 3000\u001b[39m","timestamp":"2025-11-28 19:13:18:1318"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mServer running on port 3000\u001b[39m","timestamp":"2025-11-28 19:20:01:201"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31m500 - Not allowed by CORS - /auth/me - OPTIONS - ::ffff:127.0.0.1\u001b[39m","timestamp":"2025-11-28 19:27:09:279"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:27:09 +0000] \"OPTIONS /auth/me HTTP/1.1\" 500 33 \"http://192.168.1.52:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:27:09:279"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31m500 - Not allowed by CORS - /auth/me - OPTIONS - ::ffff:127.0.0.1\u001b[39m","timestamp":"2025-11-28 19:27:09:279"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:27:09 +0000] \"OPTIONS /auth/me HTTP/1.1\" 500 33 \"http://192.168.1.52:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:27:09:279"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31m500 - Not allowed by CORS - /auth/me - OPTIONS - ::ffff:127.0.0.1\u001b[39m","timestamp":"2025-11-28 19:27:09:279"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:27:09 +0000] \"OPTIONS /auth/me HTTP/1.1\" 500 33 \"http://192.168.1.52:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:27:09:279"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mServer running on port 3000\u001b[39m","timestamp":"2025-11-28 19:30:02:302"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31m500 - Not allowed by CORS - /auth/me - OPTIONS - ::ffff:127.0.0.1\u001b[39m","timestamp":"2025-11-28 19:34:37:3437"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:34:37 +0000] \"OPTIONS /auth/me HTTP/1.1\" 500 33 \"http://192.168.1.52:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:34:37:3437"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31m500 - Not allowed by CORS - /auth/me - OPTIONS - ::ffff:127.0.0.1\u001b[39m","timestamp":"2025-11-28 19:34:37:3437"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:34:37 +0000] \"OPTIONS /auth/me HTTP/1.1\" 500 33 \"http://192.168.1.52:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:34:37:3437"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31m500 - Not allowed by CORS - /auth/me - OPTIONS - ::ffff:127.0.0.1\u001b[39m","timestamp":"2025-11-28 19:34:37:3437"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:34:37 +0000] \"OPTIONS /auth/me HTTP/1.1\" 500 33 \"http://192.168.1.52:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:34:37:3437"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31m500 - Not allowed by CORS - /auth/login - OPTIONS - ::ffff:127.0.0.1\u001b[39m","timestamp":"2025-11-28 19:34:54:3454"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:34:54 +0000] \"OPTIONS /auth/login HTTP/1.1\" 500 33 \"http://192.168.1.52:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:34:54:3454"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mServer running on port 3000\u001b[39m","timestamp":"2025-11-28 19:35:49:3549"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mServer running on port 3000\u001b[39m","timestamp":"2025-11-28 19:35:51:3551"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mCORS Allowed Origins: http://localhost:3000, http://localhost:5173, http://127.0.0.1:3000, http://127.0.0.1:5173\u001b[39m","timestamp":"2025-11-28 19:35:51:3551"}
|
||||
{"level":"\u001b[33mwarn\u001b[39m","message":"\u001b[33mCORS blocked origin: http://192.168.1.52:5173\u001b[39m","timestamp":"2025-11-28 19:44:05:445"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31m500 - Not allowed by CORS - /auth/me - OPTIONS - ::ffff:127.0.0.1\u001b[39m","timestamp":"2025-11-28 19:44:05:445"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:44:05 +0000] \"OPTIONS /auth/me HTTP/1.1\" 500 33 \"http://192.168.1.52:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:44:05:445"}
|
||||
{"level":"\u001b[33mwarn\u001b[39m","message":"\u001b[33mCORS blocked origin: http://192.168.1.52:5173\u001b[39m","timestamp":"2025-11-28 19:44:05:445"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31m500 - Not allowed by CORS - /auth/me - OPTIONS - ::ffff:127.0.0.1\u001b[39m","timestamp":"2025-11-28 19:44:05:445"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:44:05 +0000] \"OPTIONS /auth/me HTTP/1.1\" 500 33 \"http://192.168.1.52:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:44:05:445"}
|
||||
{"level":"\u001b[33mwarn\u001b[39m","message":"\u001b[33mCORS blocked origin: http://192.168.1.52:5173\u001b[39m","timestamp":"2025-11-28 19:44:05:445"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31m500 - Not allowed by CORS - /auth/me - OPTIONS - ::ffff:127.0.0.1\u001b[39m","timestamp":"2025-11-28 19:44:05:445"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:44:05 +0000] \"OPTIONS /auth/me HTTP/1.1\" 500 33 \"http://192.168.1.52:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:44:05:445"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:44:20 +0000] \"OPTIONS /auth/me HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:44:20:4420"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:44:20 +0000] \"GET /auth/me HTTP/1.1\" 401 29 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:44:20:4420"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:44:20 +0000] \"GET /auth/me HTTP/1.1\" 401 29 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:44:20:4420"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:44:20 +0000] \"GET /auth/me HTTP/1.1\" 401 29 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:44:20:4420"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:44:51 +0000] \"OPTIONS /auth/login HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:44:51:4451"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:44:51 +0000] \"POST /auth/login HTTP/1.1\" 200 404 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:44:51:4451"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:44:51 +0000] \"OPTIONS /auth/me HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:44:51:4451"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:44:51 +0000] \"GET /auth/me HTTP/1.1\" 200 153 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:44:51:4451"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:44:51 +0000] \"OPTIONS /terraform/templates HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:44:51:4451"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:44:51 +0000] \"OPTIONS /admin/users HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:44:51:4451"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:44:51 +0000] \"GET /admin/users HTTP/1.1\" 200 146 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:44:51:4451"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:44:51 +0000] \"GET /admin/users HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:44:51:4451"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:44:51 +0000] \"GET /terraform/templates HTTP/1.1\" 200 64 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:44:51:4451"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:44:51 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:44:51:4451"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:45:02 +0000] \"OPTIONS /terraform/formations HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:45:02:452"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:45:02 +0000] \"GET /terraform/formations HTTP/1.1\" 200 17 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:45:02:452"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:45:02 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:45:02:452"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:45:03 +0000] \"OPTIONS /terraform/templates HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:45:03:453"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:45:03 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:45:03:453"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:45:03 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:45:03:453"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:45:22 +0000] \"OPTIONS /terraform/apply HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:45:22:4522"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:45:22 +0000] \"POST /terraform/apply HTTP/1.1\" 400 164 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:45:22:4522"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:45:42 +0000] \"OPTIONS /auth/me HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:45:42:4542"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:45:42 +0000] \"GET /auth/me HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:45:42:4542"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:45:42 +0000] \"GET /auth/me HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:45:42:4542"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:45:42 +0000] \"OPTIONS /terraform/templates HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:45:42:4542"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:45:42 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:45:42:4542"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:45:42 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:45:42:4542"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:45:54 +0000] \"OPTIONS /terraform/apply HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:45:54:4554"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:45:54 +0000] \"POST /terraform/apply HTTP/1.1\" 400 164 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:45:54:4554"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mServer running on port 3000\u001b[39m","timestamp":"2025-11-28 19:46:36:4636"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mCORS Allowed Origins: http://localhost:3000, http://localhost:5173, http://127.0.0.1:3000, http://127.0.0.1:5173, http://192.168.1.52:5173\u001b[39m","timestamp":"2025-11-28 19:46:36:4636"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mServer running on port 3000\u001b[39m","timestamp":"2025-11-28 19:47:14:4714"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mCORS Allowed Origins: http://localhost:3000, http://localhost:5173, http://127.0.0.1:3000, http://127.0.0.1:5173, http://192.168.1.52:5173\u001b[39m","timestamp":"2025-11-28 19:47:14:4714"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:53:47 +0000] \"OPTIONS /auth/me HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:53:47:5347"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:53:47 +0000] \"GET /auth/me HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:53:47:5347"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:53:47 +0000] \"GET /auth/me HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:53:47:5347"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:53:47 +0000] \"OPTIONS /terraform/templates HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:53:47:5347"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:53:47 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:53:47:5347"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:53:47 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:53:47:5347"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:53:59 +0000] \"OPTIONS /terraform/apply HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:53:59:5359"}
|
||||
{"level":"\u001b[33mwarn\u001b[39m","message":"\u001b[33mValidation failed: [{\"expected\":\"'windows' | 'linux'\",\"received\":\"undefined\",\"code\":\"invalid_type\",\"path\":[\"vms\",0,\"model\"],\"message\":\"Required\"}]\u001b[39m","timestamp":"2025-11-28 19:53:59:5359"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:53:59 +0000] \"POST /terraform/apply HTTP/1.1\" 400 164 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:53:59:5359"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:54:07 +0000] \"OPTIONS /terraform/apply HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:54:07:547"}
|
||||
{"level":"\u001b[33mwarn\u001b[39m","message":"\u001b[33mValidation failed: [{\"expected\":\"'windows' | 'linux'\",\"received\":\"undefined\",\"code\":\"invalid_type\",\"path\":[\"vms\",0,\"model\"],\"message\":\"Required\"}]\u001b[39m","timestamp":"2025-11-28 19:54:07:547"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:18:54:07 +0000] \"POST /terraform/apply HTTP/1.1\" 400 164 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 19:54:07:547"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mServer running on port 3000\u001b[39m","timestamp":"2025-11-28 19:55:01:551"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mCORS Allowed Origins: http://localhost:3000, http://localhost:5173, http://127.0.0.1:3000, http://127.0.0.1:5173, http://192.168.1.52:5173\u001b[39m","timestamp":"2025-11-28 19:55:01:551"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:01:14 +0000] \"OPTIONS /terraform/apply HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:01:14:114"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:01:14 +0000] \"POST /terraform/apply HTTP/1.1\" 202 76 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:01:14:114"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:01:14 +0000] \"OPTIONS /terraform/formations HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:01:14:114"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:01:14 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:01:14:114"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:01:14 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:01:14:114"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mApply job failed for test: Error: Formation already exists on terraform host\u001b[39m","timestamp":"2025-11-28 20:01:14:114"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:01:26 +0000] \"OPTIONS /terraform/formations HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:01:26:126"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:01:26 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:01:26:126"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:01:27 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:01:27:127"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:01:27 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:01:27:127"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:01:28 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:01:28:128"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:01:31 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:01:31:131"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:01:31 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:01:31:131"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:01:31 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:01:31:131"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mReceived kill signal, shutting down gracefully\u001b[39m","timestamp":"2025-11-28 20:01:37:137"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mClosed out remaining connections\u001b[39m","timestamp":"2025-11-28 20:01:37:137"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mDatabase connection closed\u001b[39m","timestamp":"2025-11-28 20:01:37:137"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mServer running on port 3000\u001b[39m","timestamp":"2025-11-28 20:01:41:141"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mCORS Allowed Origins: http://localhost:3000, http://localhost:5173, http://127.0.0.1:3000, http://127.0.0.1:5173, http://192.168.1.52:5173\u001b[39m","timestamp":"2025-11-28 20:01:41:141"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mServer running on port 3000\u001b[39m","timestamp":"2025-11-28 20:02:19:219"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mCORS Allowed Origins: http://localhost:3000, http://localhost:5173, http://127.0.0.1:3000, http://127.0.0.1:5173, http://192.168.1.52:5173\u001b[39m","timestamp":"2025-11-28 20:02:19:219"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:03:31 +0000] \"OPTIONS /terraform/formations HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:03:31:331"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:03:31 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:03:31:331"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:03:32 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:03:32:332"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:03:46 +0000] \"OPTIONS /terraform/templates HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:03:46:346"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:03:46 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:03:46:346"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:03:46 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:03:46:346"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:04:05 +0000] \"OPTIONS /terraform/apply HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:04:05:45"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:04:05 +0000] \"POST /terraform/apply HTTP/1.1\" 202 80 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:04:05:45"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:04:05 +0000] \"OPTIONS /terraform/formations HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:04:05:45"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:04:05 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:04:05:45"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:04:05 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:04:05:45"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:04:14 +0000] \"OPTIONS /terraform/formations HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:04:14:414"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:04:14 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:04:14:414"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mStarting Guacamole provisioning for formation test50\u001b[39m","timestamp":"2025-11-28 20:05:46:546"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mGuacamole provisioning failed for formation test50: AxiosError: Request failed with status code 415\u001b[39m","timestamp":"2025-11-28 20:05:46:546"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:06:00 +0000] \"OPTIONS /terraform/formations HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:06:00:60"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:06:00 +0000] \"GET /terraform/formations HTTP/1.1\" 200 197 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:06:00:60"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:06:05 +0000] \"OPTIONS /terraform/formations/undefined HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:06:05:65"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:06:05 +0000] \"DELETE /terraform/formations/undefined HTTP/1.1\" 404 33 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:06:05:65"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mServer running on port 3000\u001b[39m","timestamp":"2025-11-28 20:06:35:635"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mCORS Allowed Origins: http://localhost:3000, http://localhost:5173, http://127.0.0.1:3000, http://127.0.0.1:5173, http://192.168.1.52:5173\u001b[39m","timestamp":"2025-11-28 20:06:35:635"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mServer running on port 3000\u001b[39m","timestamp":"2025-11-28 20:09:10:910"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mCORS Allowed Origins: http://localhost:3000, http://localhost:5173, http://127.0.0.1:3000, http://127.0.0.1:5173, http://192.168.1.52:5173\u001b[39m","timestamp":"2025-11-28 20:09:10:910"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mServer running on port 3000\u001b[39m","timestamp":"2025-11-28 20:09:12:912"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mCORS Allowed Origins: http://localhost:3000, http://localhost:5173, http://127.0.0.1:3000, http://127.0.0.1:5173, http://192.168.1.52:5173\u001b[39m","timestamp":"2025-11-28 20:09:12:912"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:11:26 +0000] \"OPTIONS /terraform/formations HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:11:26:1126"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:11:26 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:11:26:1126"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:11:28 +0000] \"OPTIONS /terraform/formations/undefined HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:11:28:1128"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:11:28 +0000] \"DELETE /terraform/formations/undefined HTTP/1.1\" 404 33 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:11:28:1128"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:11:31 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:11:31:1131"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mServer running on port 3000\u001b[39m","timestamp":"2025-11-28 20:12:31:1231"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mCORS Allowed Origins: http://localhost:3000, http://localhost:5173, http://127.0.0.1:3000, http://127.0.0.1:5173, http://192.168.1.52:5173\u001b[39m","timestamp":"2025-11-28 20:12:31:1231"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:14:41 +0000] \"OPTIONS /terraform/formations HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:14:41:1441"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:14:41 +0000] \"GET /terraform/formations HTTP/1.1\" 200 192 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:14:41:1441"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:14:45 +0000] \"OPTIONS /terraform/formations/test50 HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:14:45:1445"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:14:57 +0000] \"DELETE /terraform/formations/test50 HTTP/1.1\" 204 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:14:57:1457"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mGuacamole cleanup failed for test50: AxiosError: Request failed with status code 404\u001b[39m","timestamp":"2025-11-28 20:14:57:1457"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:15:27 +0000] \"OPTIONS /terraform/templates HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:15:27:1527"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:15:28 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:15:28:1528"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:15:28 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:15:28:1528"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:16:03 +0000] \"OPTIONS /terraform/apply HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:16:03:163"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:16:03 +0000] \"POST /terraform/apply HTTP/1.1\" 202 76 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:16:03:163"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:16:03 +0000] \"OPTIONS /terraform/formations HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:16:03:163"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:16:03 +0000] \"GET /terraform/formations HTTP/1.1\" 200 17 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:16:03:163"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:16:03 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:16:03:163"}
|
||||
{"level":"\u001b[33mwarn\u001b[39m","message":"\u001b[33mRemote directory /home/corenthin/test exists but formation is not in DB. Cleaning up stale directory.\u001b[39m","timestamp":"2025-11-28 20:16:04:164"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mStarting Guacamole provisioning for formation test\u001b[39m","timestamp":"2025-11-28 20:17:42:1742"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mGuacamole provisioning failed for formation test: AxiosError: Request failed with status code 415\u001b[39m","timestamp":"2025-11-28 20:17:42:1742"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:23:09 +0000] \"OPTIONS /terraform/formations HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:23:09:239"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:23:09 +0000] \"GET /terraform/formations HTTP/1.1\" 200 189 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:23:09:239"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:23:18 +0000] \"OPTIONS /terraform/formations/test HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:23:18:2318"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:23:32 +0000] \"DELETE /terraform/formations/test HTTP/1.1\" 204 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:23:32:2332"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mGuacamole cleanup failed for test: AxiosError: Request failed with status code 404\u001b[39m","timestamp":"2025-11-28 20:23:32:2332"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:23:48 +0000] \"OPTIONS /terraform/templates HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:23:48:2348"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:23:48 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:23:48:2348"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:23:48 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:23:48:2348"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:24:07 +0000] \"OPTIONS /terraform/apply HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:24:07:247"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:24:07 +0000] \"POST /terraform/apply HTTP/1.1\" 202 86 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:24:07:247"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:24:07 +0000] \"OPTIONS /terraform/formations HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:24:07:247"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:24:07 +0000] \"GET /terraform/formations HTTP/1.1\" 200 17 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:24:07:247"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:24:07 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:24:07:247"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mStarting Guacamole provisioning for formation corenthin\u001b[39m","timestamp":"2025-11-28 20:25:46:2546"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mGuacamole provisioning failed for formation corenthin: AxiosError: Request failed with status code 415\u001b[39m","timestamp":"2025-11-28 20:25:46:2546"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mServer running on port 3000\u001b[39m","timestamp":"2025-11-28 20:26:46:2646"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mCORS Allowed Origins: http://localhost:3000, http://localhost:5173, http://127.0.0.1:3000, http://127.0.0.1:5173, http://192.168.1.52:5173\u001b[39m","timestamp":"2025-11-28 20:26:46:2646"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:30:18 +0000] \"OPTIONS /terraform/formations HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:30:18:3018"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:30:18 +0000] \"GET /terraform/formations HTTP/1.1\" 200 201 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:30:18:3018"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:30:21 +0000] \"OPTIONS /terraform/formations/corenthin HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:30:21:3021"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:30:33 +0000] \"DELETE /terraform/formations/corenthin HTTP/1.1\" 204 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:30:33:3033"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mGuacamole cleanup failed for corenthin: AxiosError: Request failed with status code 404\u001b[39m","timestamp":"2025-11-28 20:30:33:3033"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:30:51 +0000] \"OPTIONS /terraform/templates HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:30:51:3051"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:30:51 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:30:51:3051"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:30:51 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:30:51:3051"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:31:04 +0000] \"OPTIONS /terraform/apply HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:31:04:314"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:31:04 +0000] \"POST /terraform/apply HTTP/1.1\" 202 74 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:31:04:314"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:31:04 +0000] \"OPTIONS /terraform/formations HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:31:04:314"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:31:04 +0000] \"GET /terraform/formations HTTP/1.1\" 200 17 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:31:04:314"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:31:04 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:31:04:314"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:31:08 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:31:08:318"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mStarting Guacamole provisioning for formation oui\u001b[39m","timestamp":"2025-11-28 20:32:43:3243"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mGuacamole provisioning failed for formation oui: AxiosError: Request failed with status code 415\u001b[39m","timestamp":"2025-11-28 20:32:43:3243"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mServer running on port 3000\u001b[39m","timestamp":"2025-11-28 20:34:07:347"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mCORS Allowed Origins: http://localhost:3000, http://localhost:5173, http://127.0.0.1:3000, http://127.0.0.1:5173, http://192.168.1.52:5173\u001b[39m","timestamp":"2025-11-28 20:34:07:347"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:37:29 +0000] \"OPTIONS /terraform/templates HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:37:29:3729"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:37:29 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:37:29:3729"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:37:29 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:37:29:3729"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:37:48 +0000] \"OPTIONS /terraform/apply HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:37:48:3748"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:37:48 +0000] \"POST /terraform/apply HTTP/1.1\" 202 80 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:37:48:3748"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:37:48 +0000] \"OPTIONS /terraform/formations HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:37:48:3748"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:37:48 +0000] \"GET /terraform/formations HTTP/1.1\" 200 184 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:37:48:3748"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:37:48 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:37:48:3748"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mStarting Guacamole provisioning for formation blabla\u001b[39m","timestamp":"2025-11-28 20:39:26:3926"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mGuacamole provisioning failed for formation blabla: Request failed with status code 415\u001b[39m","timestamp":"2025-11-28 20:39:27:3927"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mFailed request: PATCH https://guacamole.firewax.fr/guacamole/api/session/data/mysql/userGroups/blabla/memberUsers\u001b[39m","timestamp":"2025-11-28 20:39:27:3927"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mResponse status: 415\u001b[39m","timestamp":"2025-11-28 20:39:27:3927"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mResponse data: \"<!doctype html><html lang=\\\"fr\\\"><head><title>État HTTP 415 – Type de média non supporté</title><style type=\\\"text/css\\\">body {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>État HTTP 415 – Type de média non supporté</h1><hr class=\\\"line\\\" /><p><b>Type</b> Rapport d'état</p><p><b>message</b> Unsupported Media Type</p><p><b>description</b> Le serveur a refusé cette requête car l'entité de requête est dans un format non supporté par la ressource demandée avec la méthode spécifiée.</p><hr class=\\\"line\\\" /><h3>Apache Tomcat/9.0.70 (Debian)</h3></body></html>\"\u001b[39m","timestamp":"2025-11-28 20:39:27:3927"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mServer running on port 3000\u001b[39m","timestamp":"2025-11-28 20:41:10:4110"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mCORS Allowed Origins: http://localhost:3000, http://localhost:5173, http://127.0.0.1:3000, http://127.0.0.1:5173, http://192.168.1.52:5173\u001b[39m","timestamp":"2025-11-28 20:41:10:4110"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mServer running on port 3000\u001b[39m","timestamp":"2025-11-28 20:41:15:4115"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mCORS Allowed Origins: http://localhost:3000, http://localhost:5173, http://127.0.0.1:3000, http://127.0.0.1:5173, http://192.168.1.52:5173\u001b[39m","timestamp":"2025-11-28 20:41:15:4115"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mServer running on port 3000\u001b[39m","timestamp":"2025-11-28 20:41:19:4119"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mCORS Allowed Origins: http://localhost:3000, http://localhost:5173, http://127.0.0.1:3000, http://127.0.0.1:5173, http://192.168.1.52:5173\u001b[39m","timestamp":"2025-11-28 20:41:19:4119"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:43:32 +0000] \"OPTIONS /auth/me HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:43:32:4332"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:43:32 +0000] \"GET /auth/me HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:43:32:4332"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:43:32 +0000] \"GET /auth/me HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:43:32:4332"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:43:32 +0000] \"OPTIONS /terraform/formations HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:43:32:4332"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:43:32 +0000] \"GET /terraform/formations HTTP/1.1\" 200 360 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:43:32:4332"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:43:32 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:43:32:4332"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:43:39 +0000] \"OPTIONS /terraform/formations/oui HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:43:39:4339"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:43:44 +0000] \"OPTIONS /terraform/formations/blabla HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:43:44:4344"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:43:52 +0000] \"DELETE /terraform/formations/oui HTTP/1.1\" 204 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:43:52:4352"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:43:57 +0000] \"DELETE /terraform/formations/blabla HTTP/1.1\" 204 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:43:57:4357"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:44:31 +0000] \"OPTIONS /terraform/templates HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:44:31:4431"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:44:31 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:44:31:4431"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:44:31 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:44:31:4431"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:44:46 +0000] \"OPTIONS /terraform/apply HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:44:46:4446"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:44:46 +0000] \"POST /terraform/apply HTTP/1.1\" 202 74 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:44:46:4446"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:44:46 +0000] \"OPTIONS /terraform/formations HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:44:46:4446"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:44:46 +0000] \"GET /terraform/formations HTTP/1.1\" 200 17 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:44:46:4446"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:19:44:46 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 20:44:46:4446"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mStarting Guacamole provisioning for formation mat\u001b[39m","timestamp":"2025-11-28 20:46:24:4624"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mGuacamole provisioning failed for formation mat: Connection for VM \"VM-1\" already exists.\u001b[39m","timestamp":"2025-11-28 20:46:25:4625"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:20:48:23 +0000] \"OPTIONS /terraform/formations HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 21:48:23:4823"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:20:48:23 +0000] \"GET /terraform/formations HTTP/1.1\" 200 183 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 21:48:23:4823"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:20:50:02 +0000] \"OPTIONS /terraform/formations/mat HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 21:50:02:502"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:20:50:14 +0000] \"DELETE /terraform/formations/mat HTTP/1.1\" 204 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 21:50:14:5014"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mGuacamole cleanup failed for mat: AxiosError: Request failed with status code 404\u001b[39m","timestamp":"2025-11-28 21:50:14:5014"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mServer running on port 3000\u001b[39m","timestamp":"2025-11-28 21:51:57:5157"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mCORS Allowed Origins: http://localhost:3000, http://localhost:5173, http://127.0.0.1:3000, http://127.0.0.1:5173, http://192.168.1.52:5173\u001b[39m","timestamp":"2025-11-28 21:51:57:5157"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mServer running on port 3000\u001b[39m","timestamp":"2025-11-28 21:54:18:5418"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mCORS Allowed Origins: http://localhost:3000, http://localhost:5173, http://127.0.0.1:3000, http://127.0.0.1:5173, http://192.168.1.52:5173\u001b[39m","timestamp":"2025-11-28 21:54:18:5418"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:20:55:26 +0000] \"OPTIONS /terraform/templates HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 21:55:26:5526"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:20:55:26 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 21:55:26:5526"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:20:55:26 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 21:55:26:5526"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:20:55:44 +0000] \"OPTIONS /terraform/apply HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 21:55:44:5544"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:20:55:44 +0000] \"POST /terraform/apply HTTP/1.1\" 202 74 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 21:55:44:5544"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:20:55:44 +0000] \"OPTIONS /terraform/formations HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 21:55:44:5544"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:20:55:44 +0000] \"GET /terraform/formations HTTP/1.1\" 200 17 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 21:55:44:5544"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:20:55:44 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 21:55:44:5544"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mStarting Guacamole provisioning for formation leo\u001b[39m","timestamp":"2025-11-28 21:57:23:5723"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mGuacamole provisioning failed for formation leo: Request failed with status code 415\u001b[39m","timestamp":"2025-11-28 21:57:23:5723"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mFailed request: PATCH https://guacamole.firewax.fr/guacamole/api/session/data/mysql/userGroups/LEO/memberUsers\u001b[39m","timestamp":"2025-11-28 21:57:23:5723"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mRequest headers: {\"Accept\":\"application/json, text/plain, */*\",\"Content-Type\":\"application/json-patch+json\",\"User-Agent\":\"axios/1.13.2\",\"Content-Length\":\"41\",\"Accept-Encoding\":\"gzip, compress, deflate, br\"}\u001b[39m","timestamp":"2025-11-28 21:57:23:5723"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mResponse status: 415\u001b[39m","timestamp":"2025-11-28 21:57:23:5723"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mResponse data: \"<!doctype html><html lang=\\\"fr\\\"><head><title>État HTTP 415 – Type de média non supporté</title><style type=\\\"text/css\\\">body {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>État HTTP 415 – Type de média non supporté</h1><hr class=\\\"line\\\" /><p><b>Type</b> Rapport d'état</p><p><b>message</b> Unsupported Media Type</p><p><b>description</b> Le serveur a refusé cette requête car l'entité de requête est dans un format non supporté par la ressource demandée avec la méthode spécifiée.</p><hr class=\\\"line\\\" /><h3>Apache Tomcat/9.0.70 (Debian)</h3></body></html>\"\u001b[39m","timestamp":"2025-11-28 21:57:23:5723"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:20:57:42 +0000] \"OPTIONS /terraform/formations HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 21:57:42:5742"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:20:57:42 +0000] \"GET /terraform/formations HTTP/1.1\" 200 183 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 21:57:42:5742"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mServer running on port 3000\u001b[39m","timestamp":"2025-11-28 21:58:25:5825"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mCORS Allowed Origins: http://localhost:3000, http://localhost:5173, http://127.0.0.1:3000, http://127.0.0.1:5173, http://192.168.1.52:5173\u001b[39m","timestamp":"2025-11-28 21:58:25:5825"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mServer running on port 3000\u001b[39m","timestamp":"2025-11-28 21:58:29:5829"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mCORS Allowed Origins: http://localhost:3000, http://localhost:5173, http://127.0.0.1:3000, http://127.0.0.1:5173, http://192.168.1.52:5173\u001b[39m","timestamp":"2025-11-28 21:58:29:5829"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:20:59:16 +0000] \"OPTIONS /terraform/formations HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 21:59:16:5916"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:20:59:16 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 21:59:16:5916"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:20:59:18 +0000] \"OPTIONS /terraform/formations/leo HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 21:59:18:5918"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:20:59:30 +0000] \"DELETE /terraform/formations/leo HTTP/1.1\" 204 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 21:59:30:5930"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mCleaning up Guacamole for group LEO\u001b[39m","timestamp":"2025-11-28 21:59:30:5930"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mFound 0 users and 0 connections to delete for group LEO\u001b[39m","timestamp":"2025-11-28 21:59:30:5930"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mGuacamole cleanup completed for LEO\u001b[39m","timestamp":"2025-11-28 21:59:30:5930"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:00:01 +0000] \"OPTIONS /terraform/templates HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:00:01:01"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:00:01 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:00:01:01"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:00:01 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:00:01:01"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:00:16 +0000] \"OPTIONS /terraform/apply HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:00:16:016"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:00:16 +0000] \"POST /terraform/apply HTTP/1.1\" 202 76 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:00:16:016"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:00:16 +0000] \"OPTIONS /terraform/formations HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:00:16:016"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:00:16 +0000] \"GET /terraform/formations HTTP/1.1\" 200 17 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:00:16:016"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:00:16 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:00:16:016"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mStarting Guacamole provisioning for formation quac\u001b[39m","timestamp":"2025-11-28 22:01:56:156"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mGuacamole provisioning succeeded for formation quac\u001b[39m","timestamp":"2025-11-28 22:01:57:157"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:03:43 +0000] \"OPTIONS /terraform/formations HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:03:43:343"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:03:43 +0000] \"GET /terraform/formations HTTP/1.1\" 200 186 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:03:43:343"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:04:10 +0000] \"OPTIONS /terraform/formations/quac HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:04:10:410"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:04:22 +0000] \"DELETE /terraform/formations/quac HTTP/1.1\" 204 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:04:22:422"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mCleaning up Guacamole for group quac\u001b[39m","timestamp":"2025-11-28 22:04:22:422"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mFound 1 users and 1 connections to delete for group quac\u001b[39m","timestamp":"2025-11-28 22:04:22:422"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mGuacamole cleanup completed for quac\u001b[39m","timestamp":"2025-11-28 22:04:22:422"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:04:26 +0000] \"OPTIONS /terraform/templates HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:04:26:426"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:04:26 +0000] \"OPTIONS /admin/users HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:04:26:426"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:04:26 +0000] \"GET /admin/users HTTP/1.1\" 401 29 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:04:26:426"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:04:26 +0000] \"GET /admin/users HTTP/1.1\" 401 29 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:04:26:426"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:04:26 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:04:26:426"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:04:26 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:04:26:426"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:04:29 +0000] \"OPTIONS /auth/me HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:04:29:429"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:04:29 +0000] \"GET /auth/me HTTP/1.1\" 401 29 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:04:29:429"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:04:29 +0000] \"GET /auth/me HTTP/1.1\" 401 29 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:04:29:429"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:04:29 +0000] \"GET /auth/me HTTP/1.1\" 401 29 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:04:29:429"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:04:29 +0000] \"GET /auth/me HTTP/1.1\" 401 29 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:04:29:429"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:04:45 +0000] \"OPTIONS /auth/login HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:04:45:445"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:04:45 +0000] \"POST /auth/login HTTP/1.1\" 200 404 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:04:45:445"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:04:45 +0000] \"OPTIONS /auth/me HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:04:45:445"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:04:45 +0000] \"GET /auth/me HTTP/1.1\" 200 153 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:04:45:445"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:04:45 +0000] \"OPTIONS /terraform/templates HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:04:45:445"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:04:45 +0000] \"OPTIONS /admin/users HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:04:45:445"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:04:45 +0000] \"OPTIONS /admin/users HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:04:45:445"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:04:45 +0000] \"GET /admin/users HTTP/1.1\" 200 146 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:04:45:445"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:04:45 +0000] \"GET /admin/users HTTP/1.1\" 200 146 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:04:45:445"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:04:45 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:04:45:445"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:04:45 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:04:45:445"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:05:03 +0000] \"OPTIONS /admin/users HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:05:03:53"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:05:03 +0000] \"POST /admin/users HTTP/1.1\" 201 123 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:05:03:53"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:05:11 +0000] \"OPTIONS /admin/users/7bfd551e-d15f-466b-80e0-dc8e13ab4c7d HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:05:11:511"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:05:11 +0000] \"PUT /admin/users/7bfd551e-d15f-466b-80e0-dc8e13ab4c7d HTTP/1.1\" 200 124 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:05:11:511"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:05:12 +0000] \"OPTIONS /admin/users/8a583ec1-cc42-11f0-8ab1-a63e2675a89e HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:05:12:512"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:05:12 +0000] \"PUT /admin/users/8a583ec1-cc42-11f0-8ab1-a63e2675a89e HTTP/1.1\" 400 56 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:05:12:512"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:05:17 +0000] \"DELETE /admin/users/8a583ec1-cc42-11f0-8ab1-a63e2675a89e HTTP/1.1\" 400 36 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:05:17:517"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:05:19 +0000] \"OPTIONS /admin/users/7bfd551e-d15f-466b-80e0-dc8e13ab4c7d HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:05:19:519"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:05:19 +0000] \"DELETE /admin/users/7bfd551e-d15f-466b-80e0-dc8e13ab4c7d HTTP/1.1\" 204 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:05:19:519"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:05:29 +0000] \"OPTIONS /terraform/templates HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:05:29:529"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:05:29 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:05:29:529"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:05:29 +0000] \"GET /terraform/templates HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:05:29:529"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:05:32 +0000] \"OPTIONS /terraform/formations HTTP/1.1\" 204 0 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:05:32:532"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:05:32 +0000] \"GET /terraform/formations HTTP/1.1\" 200 17 \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:05:32:532"}
|
||||
{"level":"\u001b[35mhttp\u001b[39m","message":"\u001b[35m::ffff:127.0.0.1 - - [28/Nov/2025:21:05:32 +0000] \"GET /terraform/formations HTTP/1.1\" 304 - \"http://localhost:5173/\" \"Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0\"\u001b[39m","timestamp":"2025-11-28 22:05:32:532"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mReceived kill signal, shutting down gracefully\u001b[39m","timestamp":"2025-11-28 22:05:47:547"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mClosed out remaining connections\u001b[39m","timestamp":"2025-11-28 22:05:47:547"}
|
||||
{"level":"\u001b[32minfo\u001b[39m","message":"\u001b[32mDatabase connection closed\u001b[39m","timestamp":"2025-11-28 22:05:47:547"}
|
||||
29
logs/error.log
Normal file
29
logs/error.log
Normal file
@ -0,0 +1,29 @@
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31m500 - Not allowed by CORS - /auth/me - OPTIONS - ::ffff:127.0.0.1\u001b[39m","timestamp":"2025-11-28 19:27:09:279"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31m500 - Not allowed by CORS - /auth/me - OPTIONS - ::ffff:127.0.0.1\u001b[39m","timestamp":"2025-11-28 19:27:09:279"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31m500 - Not allowed by CORS - /auth/me - OPTIONS - ::ffff:127.0.0.1\u001b[39m","timestamp":"2025-11-28 19:27:09:279"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31m500 - Not allowed by CORS - /auth/me - OPTIONS - ::ffff:127.0.0.1\u001b[39m","timestamp":"2025-11-28 19:34:37:3437"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31m500 - Not allowed by CORS - /auth/me - OPTIONS - ::ffff:127.0.0.1\u001b[39m","timestamp":"2025-11-28 19:34:37:3437"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31m500 - Not allowed by CORS - /auth/me - OPTIONS - ::ffff:127.0.0.1\u001b[39m","timestamp":"2025-11-28 19:34:37:3437"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31m500 - Not allowed by CORS - /auth/login - OPTIONS - ::ffff:127.0.0.1\u001b[39m","timestamp":"2025-11-28 19:34:54:3454"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31m500 - Not allowed by CORS - /auth/me - OPTIONS - ::ffff:127.0.0.1\u001b[39m","timestamp":"2025-11-28 19:44:05:445"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31m500 - Not allowed by CORS - /auth/me - OPTIONS - ::ffff:127.0.0.1\u001b[39m","timestamp":"2025-11-28 19:44:05:445"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31m500 - Not allowed by CORS - /auth/me - OPTIONS - ::ffff:127.0.0.1\u001b[39m","timestamp":"2025-11-28 19:44:05:445"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mApply job failed for test: Error: Formation already exists on terraform host\u001b[39m","timestamp":"2025-11-28 20:01:14:114"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mGuacamole provisioning failed for formation test50: AxiosError: Request failed with status code 415\u001b[39m","timestamp":"2025-11-28 20:05:46:546"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mGuacamole cleanup failed for test50: AxiosError: Request failed with status code 404\u001b[39m","timestamp":"2025-11-28 20:14:57:1457"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mGuacamole provisioning failed for formation test: AxiosError: Request failed with status code 415\u001b[39m","timestamp":"2025-11-28 20:17:42:1742"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mGuacamole cleanup failed for test: AxiosError: Request failed with status code 404\u001b[39m","timestamp":"2025-11-28 20:23:32:2332"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mGuacamole provisioning failed for formation corenthin: AxiosError: Request failed with status code 415\u001b[39m","timestamp":"2025-11-28 20:25:46:2546"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mGuacamole cleanup failed for corenthin: AxiosError: Request failed with status code 404\u001b[39m","timestamp":"2025-11-28 20:30:33:3033"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mGuacamole provisioning failed for formation oui: AxiosError: Request failed with status code 415\u001b[39m","timestamp":"2025-11-28 20:32:43:3243"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mGuacamole provisioning failed for formation blabla: Request failed with status code 415\u001b[39m","timestamp":"2025-11-28 20:39:27:3927"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mFailed request: PATCH https://guacamole.firewax.fr/guacamole/api/session/data/mysql/userGroups/blabla/memberUsers\u001b[39m","timestamp":"2025-11-28 20:39:27:3927"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mResponse status: 415\u001b[39m","timestamp":"2025-11-28 20:39:27:3927"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mResponse data: \"<!doctype html><html lang=\\\"fr\\\"><head><title>État HTTP 415 – Type de média non supporté</title><style type=\\\"text/css\\\">body {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>État HTTP 415 – Type de média non supporté</h1><hr class=\\\"line\\\" /><p><b>Type</b> Rapport d'état</p><p><b>message</b> Unsupported Media Type</p><p><b>description</b> Le serveur a refusé cette requête car l'entité de requête est dans un format non supporté par la ressource demandée avec la méthode spécifiée.</p><hr class=\\\"line\\\" /><h3>Apache Tomcat/9.0.70 (Debian)</h3></body></html>\"\u001b[39m","timestamp":"2025-11-28 20:39:27:3927"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mGuacamole provisioning failed for formation mat: Connection for VM \"VM-1\" already exists.\u001b[39m","timestamp":"2025-11-28 20:46:25:4625"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mGuacamole cleanup failed for mat: AxiosError: Request failed with status code 404\u001b[39m","timestamp":"2025-11-28 21:50:14:5014"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mGuacamole provisioning failed for formation leo: Request failed with status code 415\u001b[39m","timestamp":"2025-11-28 21:57:23:5723"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mFailed request: PATCH https://guacamole.firewax.fr/guacamole/api/session/data/mysql/userGroups/LEO/memberUsers\u001b[39m","timestamp":"2025-11-28 21:57:23:5723"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mRequest headers: {\"Accept\":\"application/json, text/plain, */*\",\"Content-Type\":\"application/json-patch+json\",\"User-Agent\":\"axios/1.13.2\",\"Content-Length\":\"41\",\"Accept-Encoding\":\"gzip, compress, deflate, br\"}\u001b[39m","timestamp":"2025-11-28 21:57:23:5723"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mResponse status: 415\u001b[39m","timestamp":"2025-11-28 21:57:23:5723"}
|
||||
{"level":"\u001b[31merror\u001b[39m","message":"\u001b[31mResponse data: \"<!doctype html><html lang=\\\"fr\\\"><head><title>État HTTP 415 – Type de média non supporté</title><style type=\\\"text/css\\\">body {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>État HTTP 415 – Type de média non supporté</h1><hr class=\\\"line\\\" /><p><b>Type</b> Rapport d'état</p><p><b>message</b> Unsupported Media Type</p><p><b>description</b> Le serveur a refusé cette requête car l'entité de requête est dans un format non supporté par la ressource demandée avec la méthode spécifiée.</p><hr class=\\\"line\\\" /><h3>Apache Tomcat/9.0.70 (Debian)</h3></body></html>\"\u001b[39m","timestamp":"2025-11-28 21:57:23:5723"}
|
||||
2570
package-lock.json
generated
Normal file
2570
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
47
package.json
Normal file
47
package.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "api-ts",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "nodemon src/index.ts"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@prisma/client": "5.22.0",
|
||||
"argon2": "^0.44.0",
|
||||
"axios": "^1.13.2",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.1.0",
|
||||
"express-rate-limit": "^8.2.1",
|
||||
"helmet": "^8.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"morgan": "^1.10.1",
|
||||
"prisma": "5.22.0",
|
||||
"ssh2": "^1.17.0",
|
||||
"uuid": "^13.0.0",
|
||||
"winston": "^3.18.3",
|
||||
"zod": "^3.25.76",
|
||||
"zod-to-json-schema": "^3.25.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cookie-parser": "^1.4.10",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.5",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/morgan": "^1.9.10",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/ssh2": "^1.15.5",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"nodemon": "^3.1.11",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
14
prisma.config.ts.bak
Normal file
14
prisma.config.ts.bak
Normal file
@ -0,0 +1,14 @@
|
||||
// This file was generated by Prisma and assumes you have installed the following:
|
||||
// npm install --save-dev prisma dotenv
|
||||
import "dotenv/config";
|
||||
import { defineConfig, env } from "prisma/config";
|
||||
|
||||
export default defineConfig({
|
||||
schema: "prisma/schema.prisma",
|
||||
migrations: {
|
||||
path: "prisma/migrations",
|
||||
},
|
||||
datasource: {
|
||||
url: env("DATABASE_URL"),
|
||||
},
|
||||
});
|
||||
27
prisma/schema.prisma
Normal file
27
prisma/schema.prisma
Normal file
@ -0,0 +1,27 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
output = "../src/generated/prisma"
|
||||
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "mysql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model terraform_formations {
|
||||
id String @id @db.Char(36)
|
||||
formation_id String @unique(map: "uk_terraform_formations_formation") @db.VarChar(255)
|
||||
remote_path String @db.VarChar(1024)
|
||||
guacamole_group_name String? @db.VarChar(255)
|
||||
created_at DateTime @default(now()) @db.Timestamp(0)
|
||||
}
|
||||
|
||||
model users {
|
||||
id String @id @db.Char(36)
|
||||
email String @unique(map: "uk_users_email") @db.VarChar(255)
|
||||
password_hash String @db.Text
|
||||
display_name String? @db.VarChar(255)
|
||||
is_admin Boolean @default(false)
|
||||
created_at DateTime @default(now()) @db.Timestamp(0)
|
||||
}
|
||||
75
src/config/index.ts
Normal file
75
src/config/index.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import dotenv from 'dotenv';
|
||||
import { z } from 'zod';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const envSchema = z.object({
|
||||
PORT: z.string().default('3000'),
|
||||
DATABASE_URL: z.string(),
|
||||
JWT_SECRET: z.string(),
|
||||
JWT_TTL_SECONDS: z.string().default('3600'),
|
||||
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'),
|
||||
// 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_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'),
|
||||
// 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_INSECURE_TLS: z.string().default('true'),
|
||||
PROXMOX_TARGET_NODE: z.string().default('node1'),
|
||||
PROXMOX_MODEL_WINDOWS: z.string().default('e1000'),
|
||||
PROXMOX_MODEL_LINUX: z.string().default('virtio'),
|
||||
// Cors
|
||||
CORS_ALLOWED_ORIGINS: 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);
|
||||
|
||||
export 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(','),
|
||||
}
|
||||
};
|
||||
196
src/controllers/adminController.ts
Normal file
196
src/controllers/adminController.ts
Normal file
@ -0,0 +1,196 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { z } from 'zod';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import prisma from '../utils/prisma';
|
||||
import * as authService from '../services/authService';
|
||||
import config from '../config';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
const createUserSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
displayName: z.string().optional(),
|
||||
isAdmin: z.boolean().default(false),
|
||||
});
|
||||
|
||||
const updateUserSchema = z.object({
|
||||
email: z.string().email().optional(),
|
||||
password: z.string().min(8).optional(),
|
||||
displayName: z.string().optional().nullable(),
|
||||
isAdmin: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const mapUser = (user: any) => ({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.display_name || user.email,
|
||||
displayName: user.display_name,
|
||||
isAdmin: user.is_admin,
|
||||
});
|
||||
|
||||
export const getDashboard = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const users = await prisma.users.findMany();
|
||||
const summaries = users.map(mapUser);
|
||||
|
||||
|
||||
const currentUser = await prisma.users.findUnique({ where: { id: req.user!.id } });
|
||||
|
||||
res.json({
|
||||
currentUser: mapUser(currentUser),
|
||||
users: summaries,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Dashboard error: ${error}`);
|
||||
res.status(500).json({ message: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
export const listUsers = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const users = await prisma.users.findMany();
|
||||
const summaries = users.map(mapUser);
|
||||
res.json(summaries);
|
||||
} catch (error) {
|
||||
logger.error(`List users error: ${error}`);
|
||||
res.status(500).json({ message: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
export const createUser = async (req: Request, res: Response): Promise<void> => {
|
||||
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.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.users.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
email,
|
||||
password_hash: passwordHash,
|
||||
display_name: displayName,
|
||||
is_admin: isAdmin,
|
||||
},
|
||||
});
|
||||
|
||||
res.status(201).json(mapUser(newUser));
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
res.status(400).json({ message: 'Invalid input', errors: error.issues });
|
||||
return;
|
||||
}
|
||||
logger.error(`Create user error: ${error}`);
|
||||
res.status(500).json({ message: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateUser = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { email, password, displayName, isAdmin } = updateUserSchema.parse(req.body);
|
||||
const currentUser = req.user!;
|
||||
|
||||
const userToUpdate = await prisma.users.findUnique({ where: { id } });
|
||||
if (!userToUpdate) {
|
||||
res.status(404).json({ message: 'User not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultAdminEmail = config.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: any = {};
|
||||
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.users.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
res.json(mapUser(updatedUser));
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
res.status(400).json({ message: 'Invalid input', errors: error.issues });
|
||||
return;
|
||||
}
|
||||
// Handle unique constraint violation for email
|
||||
// Prisma throws P2002
|
||||
if ((error as any).code === 'P2002') {
|
||||
res.status(409).json({ message: 'Email already registered' });
|
||||
return;
|
||||
}
|
||||
logger.error(`Update user error: ${error}`);
|
||||
res.status(500).json({ message: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteUser = async (req: Request, res: Response): Promise<void> => {
|
||||
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.users.findUnique({ where: { id } });
|
||||
if (!userToDelete) {
|
||||
res.status(404).json({ message: 'User not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultAdminEmail = config.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.users.delete({ where: { id } });
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
logger.error(`Delete user error: ${error}`);
|
||||
res.status(500).json({ message: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
104
src/controllers/authController.ts
Normal file
104
src/controllers/authController.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { z } from 'zod';
|
||||
import prisma from '../utils/prisma';
|
||||
import * as authService from '../services/authService';
|
||||
import authGuard from '../services/bruteforceService';
|
||||
import { setSessionCookie, clearSessionCookie } from '../utils/helpers';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
const loginSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string(),
|
||||
});
|
||||
|
||||
export const login = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { email, password } = loginSchema.parse(req.body);
|
||||
|
||||
if (authGuard.isLocked(email)) {
|
||||
res.status(429).json({ message: 'Too many attempts, temporarily locked' });
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await prisma.users.findUnique({ where: { email } });
|
||||
|
||||
if (!user) {
|
||||
authGuard.recordFailure(email);
|
||||
res.status(401).json({ message: 'Invalid credentials' });
|
||||
return;
|
||||
}
|
||||
|
||||
const isValid = await authService.verifyPassword(password, user.password_hash);
|
||||
|
||||
if (!isValid) {
|
||||
authGuard.recordFailure(email);
|
||||
res.status(401).json({ message: 'Invalid credentials' });
|
||||
return;
|
||||
}
|
||||
|
||||
authGuard.recordSuccess(email);
|
||||
|
||||
const token = authService.signToken(user.id, user.email);
|
||||
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 z.ZodError) {
|
||||
res.status(400).json({ message: 'Invalid input', errors: error.issues });
|
||||
return;
|
||||
}
|
||||
logger.error(`Login error: ${error}`);
|
||||
res.status(500).json({ message: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
export const me = async (req: Request, res: Response): Promise<void> => {
|
||||
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.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.error(`Me error: ${error}`);
|
||||
res.status(500).json({ message: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
export const logout = async (req: Request, res: Response): Promise<void> => {
|
||||
clearSessionCookie(res);
|
||||
res.status(204).send();
|
||||
};
|
||||
60
src/controllers/deploymentsController.ts
Normal file
60
src/controllers/deploymentsController.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { z } from 'zod';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import config from '../config';
|
||||
import * as tfvarsService from '../services/tfvarsService';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
const deploymentSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
userCount: z.number().int().min(1),
|
||||
vmTemplate: z.string(),
|
||||
cpu: z.number().int().min(1),
|
||||
ramMb: z.number().int().min(256),
|
||||
diskGb: z.number().int().min(10),
|
||||
ipStartHost: z.number().int().min(2).max(254).optional(),
|
||||
});
|
||||
|
||||
export const createDeployment = async (req: Request, res: Response): Promise<void> => {
|
||||
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 = uuidv4();
|
||||
const tfvarsContent = tfvarsService.generate(payload);
|
||||
|
||||
const dropDir = config.terraform.dropDir;
|
||||
const filename = `${payload.name.replace(/ /g, '_')}.tfvars`;
|
||||
const filePath = path.join(dropDir, filename);
|
||||
|
||||
await fs.mkdir(dropDir, { recursive: true });
|
||||
await fs.writeFile(filePath, tfvarsContent);
|
||||
|
||||
res.json({
|
||||
id,
|
||||
tfvarsPath: filePath,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
res.status(400).json({ error: 'Invalid input', details: error.issues });
|
||||
return;
|
||||
}
|
||||
logger.error(`Create deployment error: ${error}`);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getSchema = async (req: Request, res: Response) => {
|
||||
const schema = zodToJsonSchema(deploymentSchema as any, 'DeploymentRequest');
|
||||
res.json(schema);
|
||||
};
|
||||
153
src/controllers/terraformController.ts
Normal file
153
src/controllers/terraformController.ts
Normal file
@ -0,0 +1,153 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { z } from 'zod';
|
||||
import prisma from '../utils/prisma';
|
||||
import config from '../config';
|
||||
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({
|
||||
name: z.string(),
|
||||
vmid: z.number(),
|
||||
cores: z.number(),
|
||||
memory: z.number(),
|
||||
disk_size: z.number(),
|
||||
template: z.string(),
|
||||
ip: z.string(),
|
||||
guacamoleIp: z.string().optional(),
|
||||
guacamole_ip: z.string().optional(),
|
||||
bridge: z.string().optional(),
|
||||
model: z.enum(['windows', 'linux']).default('linux'),
|
||||
rdpDomain: z.string().optional(),
|
||||
rdpPort: z.number().optional(),
|
||||
}).transform(data => ({
|
||||
...data,
|
||||
guacamoleIp: data.guacamoleIp || data.guacamole_ip
|
||||
}));
|
||||
|
||||
const guacamoleUserSchema = z.object({
|
||||
username: z.string(),
|
||||
password: z.string(),
|
||||
ip: z.string().optional(),
|
||||
displayName: z.string().optional(),
|
||||
});
|
||||
|
||||
const guacamoleRequestSchema = z.object({
|
||||
groupName: z.string().optional(),
|
||||
users: z.array(guacamoleUserSchema),
|
||||
});
|
||||
|
||||
const applySchema = z.object({
|
||||
formationId: z.string().min(1),
|
||||
vms: z.array(vmSchema).min(1),
|
||||
guacamole: guacamoleRequestSchema.optional(),
|
||||
});
|
||||
|
||||
// Helper to clean formation ID
|
||||
const cleanFormationId = (id: string): string => {
|
||||
return id;
|
||||
};
|
||||
|
||||
const isValidFormationId = (id: string): boolean => {
|
||||
return /^[a-zA-Z0-9-_]+$/.test(id);
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
if (!isValidFormationId(formationId)) {
|
||||
res.status(400).json({ message: 'Invalid formation id' });
|
||||
return;
|
||||
}
|
||||
|
||||
const existingFormation = await prisma.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 as unknown as ApplyRequest).catch(err => {
|
||||
logger.error(`Apply job failed for ${formationId}: ${err}`);
|
||||
});
|
||||
|
||||
const remoteDirPreview = path.join(config.terraform.deployBasePath, formationId);
|
||||
|
||||
res.status(202).json({
|
||||
status: 'queued',
|
||||
formationId,
|
||||
remotePath: remoteDirPreview,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`Validation failed: ${JSON.stringify(error.issues)}`);
|
||||
res.status(400).json({ message: 'Invalid input', errors: error.issues });
|
||||
return;
|
||||
}
|
||||
logger.error(`Apply error: ${error}`);
|
||||
res.status(500).json({ message: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getTemplates = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const templates = await proxmoxService.fetchTemplates();
|
||||
res.json({ templates });
|
||||
} catch (error) {
|
||||
logger.error(`Get templates error: ${error}`);
|
||||
res.status(502).json({ message: 'Failed to fetch templates from proxmox' });
|
||||
}
|
||||
};
|
||||
|
||||
export const listFormations = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const formationsRaw = await prisma.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.error(`List formations error: ${error}`);
|
||||
res.status(500).json({ message: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
export const destroyFormation = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { formationId } = req.params;
|
||||
|
||||
const formation = await prisma.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.error(`Background destroy failed for ${formationId}: ${err}`);
|
||||
});
|
||||
|
||||
res.status(202).json({ message: 'Formation destruction queued' });
|
||||
} catch (error: any) {
|
||||
logger.error(`Destroy formation error: ${error}`);
|
||||
res.status(500).json({ message: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
76
src/index.ts
Normal file
76
src/index.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import morgan from 'morgan';
|
||||
import config from './config';
|
||||
import routes from './routes';
|
||||
import logger from './utils/logger';
|
||||
import { errorHandler } from './middlewares/errorHandler';
|
||||
import prisma from './utils/prisma';
|
||||
|
||||
const app = express();
|
||||
|
||||
// Trust the first proxy (Nginx/Traefik)
|
||||
app.set('trust proxy', 1);
|
||||
|
||||
// HTTP Request Logging
|
||||
app.use(morgan('combined', { stream: { write: (message) => logger.http(message.trim()) } }));
|
||||
|
||||
app.use(helmet());
|
||||
app.use(cookieParser());
|
||||
app.use(express.json());
|
||||
|
||||
// Rate Limiting
|
||||
const limiter = rateLimit({
|
||||
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(cors({
|
||||
origin: (origin, callback) => {
|
||||
if (!origin || config.cors.allowedOrigins.includes(origin)) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
logger.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);
|
||||
|
||||
// Error Handling Middleware (must be last)
|
||||
app.use(errorHandler);
|
||||
|
||||
const server = app.listen(config.port, () => {
|
||||
logger.info(`Server running on port ${config.port}`);
|
||||
logger.info(`CORS Allowed Origins: ${config.cors.allowedOrigins.join(', ')}`);
|
||||
});
|
||||
|
||||
// Graceful Shutdown
|
||||
const gracefulShutdown = async () => {
|
||||
logger.info('Received kill signal, shutting down gracefully');
|
||||
server.close(() => {
|
||||
logger.info('Closed out remaining connections');
|
||||
prisma.$disconnect().then(() => {
|
||||
logger.info('Database connection closed');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
logger.error('Could not close connections in time, forcefully shutting down');
|
||||
process.exit(1);
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
process.on('SIGTERM', gracefulShutdown);
|
||||
process.on('SIGINT', gracefulShutdown);
|
||||
60
src/middlewares/authMiddleware.ts
Normal file
60
src/middlewares/authMiddleware.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import * as authService from '../services/authService';
|
||||
import prisma from '../utils/prisma';
|
||||
|
||||
// Extend Express Request to include user
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user?: {
|
||||
id: string;
|
||||
email: string;
|
||||
isAdmin: boolean;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const authenticate = async (req: Request, res: Response, next: NextFunction) => {
|
||||
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.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' });
|
||||
}
|
||||
};
|
||||
|
||||
export const requireAdmin = (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.user || !req.user.isAdmin) {
|
||||
res.status(403).json({ message: 'Admin privileges required' });
|
||||
return;
|
||||
}
|
||||
next();
|
||||
};
|
||||
23
src/middlewares/errorHandler.ts
Normal file
23
src/middlewares/errorHandler.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import logger from '../utils/logger';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const errorHandler = (err: any, req: Request, res: Response, next: NextFunction) => {
|
||||
logger.error(`${err.status || 500} - ${err.message} - ${req.originalUrl} - ${req.method} - ${req.ip}`);
|
||||
|
||||
if (err instanceof 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 }),
|
||||
});
|
||||
};
|
||||
15
src/routes/admin.ts
Normal file
15
src/routes/admin.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Router } from 'express';
|
||||
import * as adminController from '../controllers/adminController';
|
||||
import { authenticate, requireAdmin } from '../middlewares/authMiddleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authenticate, 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);
|
||||
|
||||
export default router;
|
||||
10
src/routes/auth.ts
Normal file
10
src/routes/auth.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Router } from 'express';
|
||||
import * as authController from '../controllers/authController';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/login', authController.login);
|
||||
router.get('/me', authController.me);
|
||||
router.post('/logout', authController.logout);
|
||||
|
||||
export default router;
|
||||
9
src/routes/deployments.ts
Normal file
9
src/routes/deployments.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { Router } from 'express';
|
||||
import * as deploymentsController from '../controllers/deploymentsController';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/', deploymentsController.createDeployment);
|
||||
router.get('/schema', deploymentsController.getSchema);
|
||||
|
||||
export default router;
|
||||
27
src/routes/index.ts
Normal file
27
src/routes/index.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Router } from 'express';
|
||||
import authRoutes from './auth';
|
||||
import deploymentsRoutes from './deployments';
|
||||
import adminRoutes from './admin';
|
||||
import terraformRoutes from './terraform';
|
||||
|
||||
const router = Router();
|
||||
|
||||
import prisma from '../utils/prisma';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
router.get('/healthz', async (req, res) => {
|
||||
try {
|
||||
await prisma.$queryRaw`SELECT 1`;
|
||||
res.status(200).json({ status: 'ok', database: 'connected' });
|
||||
} catch (error) {
|
||||
logger.error(`Health check failed: ${error}`);
|
||||
res.status(503).json({ status: 'error', database: 'disconnected' });
|
||||
}
|
||||
});
|
||||
|
||||
router.use('/auth', authRoutes);
|
||||
router.use('/deployments', deploymentsRoutes);
|
||||
router.use('/admin', adminRoutes);
|
||||
router.use('/terraform', terraformRoutes);
|
||||
|
||||
export default router;
|
||||
11
src/routes/terraform.ts
Normal file
11
src/routes/terraform.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Router } from 'express';
|
||||
import * as terraformController from '../controllers/terraformController';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/apply', terraformController.apply);
|
||||
router.get('/templates', terraformController.getTemplates);
|
||||
router.get('/formations', terraformController.listFormations);
|
||||
router.delete('/formations/:formationId', terraformController.destroyFormation);
|
||||
|
||||
export default router;
|
||||
40
src/services/authService.ts
Normal file
40
src/services/authService.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import argon2 from 'argon2';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import config from '../config';
|
||||
|
||||
export const hashPassword = async (password: string): Promise<string> => {
|
||||
return await argon2.hash(password);
|
||||
};
|
||||
|
||||
export const verifyPassword = async (password: string, hash: string): Promise<boolean> => {
|
||||
try {
|
||||
return await argon2.verify(hash, password);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export interface TokenClaims {
|
||||
userId: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export const signToken = (userId: string, email: string): string => {
|
||||
return jwt.sign(
|
||||
{ sub: userId, email },
|
||||
config.jwt.secret,
|
||||
{ expiresIn: config.jwt.ttlSeconds }
|
||||
);
|
||||
};
|
||||
|
||||
export const verifyToken = (token: string): TokenClaims | null => {
|
||||
try {
|
||||
const decoded = jwt.verify(token, config.jwt.secret) as any;
|
||||
return {
|
||||
userId: decoded.sub,
|
||||
email: decoded.email,
|
||||
};
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
43
src/services/bruteforceService.ts
Normal file
43
src/services/bruteforceService.ts
Normal file
@ -0,0 +1,43 @@
|
||||
interface Attempt {
|
||||
count: number;
|
||||
lockUntil: number | null; // Timestamp in milliseconds
|
||||
}
|
||||
|
||||
class AuthGuard {
|
||||
private attempts: Map<string, Attempt> = new Map();
|
||||
|
||||
public isLocked(key: string): boolean {
|
||||
const attempt = this.attempts.get(key);
|
||||
if (attempt && attempt.lockUntil) {
|
||||
return Date.now() < attempt.lockUntil;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public recordFailure(key: string, maxAttempts: number = 5, lockoutSecs: number = 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
|
||||
}
|
||||
}
|
||||
|
||||
public recordSuccess(key: string) {
|
||||
this.attempts.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
export default new AuthGuard();
|
||||
88
src/services/guacamoleService.ts
Normal file
88
src/services/guacamoleService.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { GuacSession, GuacamoleUser, GuacamoleVm } from '../utils/guacamoleClient';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
|
||||
// Helper to extract numeric suffix for matching
|
||||
const extractNumericSuffix = (s: string): number | null => {
|
||||
const match = s.match(/(\d+)$/);
|
||||
return match ? parseInt(match[1], 10) : null;
|
||||
};
|
||||
|
||||
const matchUserForVm = (vm: GuacamoleVm, users: GuacamoleUser[]): GuacamoleUser | undefined => {
|
||||
const vmKey = extractNumericSuffix(vm.name);
|
||||
if (vmKey === null) return undefined;
|
||||
|
||||
return users.find(user => {
|
||||
const userKey = extractNumericSuffix(user.username);
|
||||
return userKey === vmKey;
|
||||
});
|
||||
};
|
||||
|
||||
export const provision = async (formationId: string, groupName: string | undefined, users: GuacamoleUser[], vms: GuacamoleVm[]) => {
|
||||
if (users.length === 0) return;
|
||||
|
||||
const session = await 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);
|
||||
};
|
||||
|
||||
export const deleteProvisioning = async (formationId: string, groupName?: string) => {
|
||||
const session = await GuacSession.login();
|
||||
const groupIdentifier = groupName || `formation-${formationId}`;
|
||||
|
||||
try {
|
||||
logger.info(`Cleaning up Guacamole for group ${groupIdentifier}`);
|
||||
const { users, connections } = await session.getGroupMembers(groupIdentifier);
|
||||
logger.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.info(`Guacamole cleanup completed for ${groupIdentifier}`);
|
||||
} catch (error: any) {
|
||||
logger.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
|
||||
}
|
||||
};
|
||||
52
src/services/proxmoxService.ts
Normal file
52
src/services/proxmoxService.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import axios from 'axios';
|
||||
import https from 'https';
|
||||
import config from '../config';
|
||||
|
||||
interface ProxmoxTemplate {
|
||||
vmid: number;
|
||||
name: string;
|
||||
diskSizeGb: number;
|
||||
}
|
||||
|
||||
interface QemuVm {
|
||||
vmid?: number;
|
||||
name?: string;
|
||||
template?: number;
|
||||
maxdisk?: number;
|
||||
}
|
||||
|
||||
interface QemuListResponse {
|
||||
data: QemuVm[];
|
||||
}
|
||||
|
||||
export const fetchTemplates = async (): Promise<ProxmoxTemplate[]> => {
|
||||
const { url, targetNode, tokenId, tokenSecret, insecureTls } = config.proxmox;
|
||||
|
||||
const apiUrl = `${url.replace(/\/$/, '')}/nodes/${targetNode}/qemu?full=1`;
|
||||
|
||||
const agent = new https.Agent({
|
||||
rejectUnauthorized: !insecureTls
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await axios.get<QemuListResponse>(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');
|
||||
}
|
||||
};
|
||||
173
src/services/terraformService.ts
Normal file
173
src/services/terraformService.ts
Normal file
@ -0,0 +1,173 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import path from 'path';
|
||||
import prisma from '../utils/prisma';
|
||||
import config from '../config';
|
||||
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';
|
||||
|
||||
// Helper to extract numeric suffix for matching (e.g. "USER-1" -> 1)
|
||||
const extractNumericSuffix = (s: string): number | null => {
|
||||
const match = s.match(/(\d+)$/);
|
||||
return match ? parseInt(match[1], 10) : null;
|
||||
};
|
||||
|
||||
const matchUserForVm = (vmName: string, users: any[]): any | undefined => {
|
||||
const vmKey = extractNumericSuffix(vmName);
|
||||
if (vmKey === null) return undefined;
|
||||
|
||||
return users.find(user => {
|
||||
const userKey = extractNumericSuffix(user.username);
|
||||
return userKey === vmKey;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const prepareRemoteWorkspace = async (formationId: string): Promise<string> => {
|
||||
const remoteDir = path.posix.join(config.terraform.deployBasePath, formationId);
|
||||
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);
|
||||
if (checkRes.code === 0) {
|
||||
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)}`;
|
||||
const copyRes = await shell.executeSsh(copyCmd);
|
||||
|
||||
if (copyRes.code !== 0) {
|
||||
throw new Error(`Remote copy failed: ${copyRes.stderr}`);
|
||||
}
|
||||
};
|
||||
|
||||
const cleanupRemote = async (remoteDir: string) => {
|
||||
const cmd = `rm -rf ${shell.escapeShell(remoteDir)}`;
|
||||
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);
|
||||
|
||||
try {
|
||||
const tfvars = tfvarsService.generateVmDefinitions(req as any);
|
||||
|
||||
// Upload tfvars
|
||||
// We use path.posix.join to ensure forward slashes for remote linux path
|
||||
const uploadRes = await shell.uploadFileSsh(path.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<void> = Promise.resolve();
|
||||
if (req.guacamole) {
|
||||
logger.info(`Starting Guacamole provisioning for formation ${formationId}`);
|
||||
const guacamoleVms = req.vms.map(vm => {
|
||||
// Try to find matching user to get specific IP (e.g. for static IP assignment via UI)
|
||||
const user = matchUserForVm(vm.name, req.guacamole!.users);
|
||||
const targetIp = user?.ip || vm.guacamoleIp || vm.ip;
|
||||
|
||||
return {
|
||||
name: vm.name,
|
||||
ip: targetIp,
|
||||
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,
|
||||
guacamoleVms
|
||||
).then(() => {
|
||||
logger.info(`Guacamole provisioning succeeded for formation ${formationId}`);
|
||||
}).catch((error: any) => {
|
||||
logger.error(`Guacamole provisioning failed for formation ${formationId}: ${error.message}`);
|
||||
if (error.config) logger.error(`Failed request: ${error.config.method?.toUpperCase()} ${error.config.url}`);
|
||||
if (error.response) logger.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.terraform_formations.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
export const destroyFormation = async (formationId: string) => {
|
||||
const formation = await prisma.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.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.error(`Guacamole cleanup failed for ${formation.formation_id}: ${err}`));
|
||||
}
|
||||
};
|
||||
98
src/services/tfvarsService.ts
Normal file
98
src/services/tfvarsService.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import config from '../config';
|
||||
import { VmDefinition, ApplyRequest } from '../types/terraform';
|
||||
|
||||
interface DeploymentRequest {
|
||||
name: string;
|
||||
userCount: number;
|
||||
vmTemplate: string;
|
||||
cpu: number;
|
||||
ramMb: number;
|
||||
diskGb: number;
|
||||
ipStartHost?: number;
|
||||
}
|
||||
|
||||
|
||||
const escape = (s: string) => s.replace(/"/g, '\\"');
|
||||
|
||||
export const generate = (req: DeploymentRequest): string => {
|
||||
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;
|
||||
};
|
||||
|
||||
export const generateVmDefinitions = (req: ApplyRequest): string => {
|
||||
const cfg = config.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;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
25
src/types/terraform.ts
Normal file
25
src/types/terraform.ts
Normal file
@ -0,0 +1,25 @@
|
||||
export interface VmDefinition {
|
||||
name: string;
|
||||
vmid: number;
|
||||
cores: number;
|
||||
memory: number;
|
||||
disk_size: number;
|
||||
template: string;
|
||||
ip: string;
|
||||
guacamoleIp?: string;
|
||||
bridge?: string;
|
||||
model: 'windows' | 'linux';
|
||||
rdpDomain?: string;
|
||||
rdpPort?: number;
|
||||
}
|
||||
|
||||
export interface GuacamoleConfig {
|
||||
groupName?: string;
|
||||
users: { username: string; password: string; ip?: string; displayName?: string }[];
|
||||
}
|
||||
|
||||
export interface ApplyRequest {
|
||||
formationId: string;
|
||||
vms: VmDefinition[];
|
||||
guacamole?: GuacamoleConfig;
|
||||
}
|
||||
309
src/utils/guacamoleClient.ts
Normal file
309
src/utils/guacamoleClient.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
20
src/utils/helpers.ts
Normal file
20
src/utils/helpers.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Response } from 'express';
|
||||
|
||||
export const setSessionCookie = (res: Response, token: string) => {
|
||||
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: '/'
|
||||
});
|
||||
};
|
||||
|
||||
export const clearSessionCookie = (res: Response) => {
|
||||
res.clearCookie('token', {
|
||||
httpOnly: true,
|
||||
secure: process.env.COOKIE_SECURE === 'true',
|
||||
sameSite: 'lax',
|
||||
path: '/'
|
||||
});
|
||||
};
|
||||
52
src/utils/logger.ts
Normal file
52
src/utils/logger.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import winston from '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.addColors(colors);
|
||||
|
||||
const format = winston.format.combine(
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
|
||||
winston.format.colorize({ all: true }),
|
||||
winston.format.printf(
|
||||
(info) => `${info.timestamp} ${info.level}: ${info.message}`,
|
||||
),
|
||||
);
|
||||
|
||||
const transports = [
|
||||
new winston.transports.Console(),
|
||||
new winston.transports.File({
|
||||
filename: 'logs/error.log',
|
||||
level: 'error',
|
||||
format: winston.format.json(),
|
||||
}),
|
||||
new winston.transports.File({ filename: 'logs/all.log', format: winston.format.json() }),
|
||||
];
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: level(),
|
||||
levels,
|
||||
format,
|
||||
transports,
|
||||
});
|
||||
|
||||
export default logger;
|
||||
5
src/utils/prisma.ts
Normal file
5
src/utils/prisma.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { PrismaClient } from '../generated/prisma';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export default prisma;
|
||||
125
src/utils/shell.ts
Normal file
125
src/utils/shell.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { spawn } from 'child_process';
|
||||
import config from '../config';
|
||||
|
||||
interface CommandOutput {
|
||||
code: number | null;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}
|
||||
|
||||
export const executeCommand = (command: string, args: string[], input?: string): Promise<CommandOutput> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args);
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
if (input && child.stdin) {
|
||||
child.stdin.write(input);
|
||||
child.stdin.end();
|
||||
}
|
||||
|
||||
child.stdout.on('data', (data: Buffer) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data: Buffer) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code: number) => {
|
||||
resolve({ code, stdout, stderr });
|
||||
});
|
||||
|
||||
child.on('error', (err: Error) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const escapeShell = (arg: string): string => {
|
||||
return `'${arg.replace(/'/g, "'\\''")}'`;
|
||||
};
|
||||
|
||||
const ALLOWED_SSH_COMMANDS = ['terraform', 'mkdir', 'cp', 'cd', 'rm', 'test', 'cat'];
|
||||
|
||||
const validateSshCommand = (cmd: string) => {
|
||||
// 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(', ')}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const executeSsh = async (remoteCmd: string): Promise<CommandOutput> => {
|
||||
validateSshCommand(remoteCmd);
|
||||
|
||||
const args = [
|
||||
'-i', config.terraform.sshKeyPath,
|
||||
'-o', 'StrictHostKeyChecking=no',
|
||||
'-o', 'UserKnownHostsFile=/dev/null',
|
||||
'-o', 'LogLevel=ERROR',
|
||||
`${config.terraform.sshUser}@${config.terraform.sshHost}`,
|
||||
remoteCmd
|
||||
];
|
||||
return executeCommand('ssh', args);
|
||||
};
|
||||
|
||||
export const uploadFileSsh = async (remotePath: string, content: string): Promise<CommandOutput> => {
|
||||
const escapedPath = escapeShell(remotePath);
|
||||
const args = [
|
||||
'-i', config.terraform.sshKeyPath,
|
||||
'-o', 'StrictHostKeyChecking=no',
|
||||
'-o', 'UserKnownHostsFile=/dev/null',
|
||||
'-o', 'LogLevel=ERROR',
|
||||
`${config.terraform.sshUser}@${config.terraform.sshHost}`,
|
||||
`cat > ${escapedPath}`
|
||||
];
|
||||
return executeCommand('ssh', args, content);
|
||||
};
|
||||
|
||||
export const rsync = async (src: string, dest: string): Promise<CommandOutput> => {
|
||||
const sshCmd = `ssh -i ${config.terraform.sshKeyPath} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null`;
|
||||
const remoteDest = `${config.terraform.sshUser}@${config.terraform.sshHost}:${dest}`;
|
||||
|
||||
const args = [
|
||||
'-az',
|
||||
'--delete',
|
||||
'-e', sshCmd,
|
||||
src,
|
||||
remoteDest
|
||||
];
|
||||
|
||||
return executeCommand('rsync', args);
|
||||
};
|
||||
|
||||
export const copyDirectorySsh = async (src: string, dest: string): Promise<CommandOutput> => {
|
||||
// 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.terraform.sshUser}@${config.terraform.sshHost}:${dest}`;
|
||||
const args = [
|
||||
'-i', config.terraform.sshKeyPath,
|
||||
'-o', 'StrictHostKeyChecking=no',
|
||||
'-o', 'UserKnownHostsFile=/dev/null',
|
||||
'-o', 'LogLevel=ERROR',
|
||||
'-r',
|
||||
src,
|
||||
remoteDest
|
||||
];
|
||||
|
||||
return executeCommand('scp', args);
|
||||
};
|
||||
9
terraform.tfstate
Normal file
9
terraform.tfstate
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"version": 4,
|
||||
"terraform_version": "1.13.3",
|
||||
"serial": 1,
|
||||
"lineage": "a94d6dc1-4ba1-3414-c983-f49070903276",
|
||||
"outputs": {},
|
||||
"resources": [],
|
||||
"check_results": null
|
||||
}
|
||||
20
tsconfig.json
Normal file
20
tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"module": "commonjs",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user