chore: initial commit
Sposta installer di prima fase e Web Setup Wizard dal repo argos al nuovo repo pubblico argos-setup. File provenienti da argos/scripts/: - install.sh -> first-setup.sh (rinominato) - setup_server.py - setup.html - gen_config.py Rimane in argos/scripts/: - update.sh (continua a vivere nel codice runtime) Il nuovo repo e' pubblico: il contenuto non include secret ne' codice proprietario di ARGOS, solo il tooling di installazione.
This commit is contained in:
commit
7d2c1d8809
|
|
@ -0,0 +1,29 @@
|
||||||
|
# ARGOS SOC — Setup & Installer
|
||||||
|
|
||||||
|
Tecnotel Servizi SRL — [www.tecnotelsrl.com](https://www.tecnotelsrl.com)
|
||||||
|
|
||||||
|
Repository pubblico contenente l'installer di prima fase e il Web Setup Wizard di ARGOS SOC.
|
||||||
|
|
||||||
|
## Contenuto
|
||||||
|
|
||||||
|
| File | Scopo |
|
||||||
|
|---|---|
|
||||||
|
| `first-setup.sh` | Installer ambiente di prima fase: sistema base, utenti, firewall, virtualenv, nginx. |
|
||||||
|
| `setup_server.py` | Backend Flask del Web Installer (porta 8888). |
|
||||||
|
| `setup.html` | Frontend del Web Installer, wizard in 5 step. |
|
||||||
|
| `gen_config.py` | Helper per generazione iniziale di `argos.json`. |
|
||||||
|
|
||||||
|
## Flusso d'installazione
|
||||||
|
|
||||||
|
1. Il cliente esegue `first-setup.sh` per predisporre l'ambiente base (Ubuntu 24.04).
|
||||||
|
2. Al termine dello script viene avviato il Web Installer su `http://IP:8888`.
|
||||||
|
3. Il wizard completa la configurazione: cliente, rete/SSL, SIEM, utente admin.
|
||||||
|
4. Al termine del wizard, `argos-setup` richiede la licenza ARGOS per sbloccare il clone del repository privato `tecnotel/argos` e completare l'installazione dei componenti runtime.
|
||||||
|
|
||||||
|
## Repository correlati
|
||||||
|
|
||||||
|
- [`tecnotel/argos`](https://argos-update.tecnotelsrl.com/tecnotel/argos) — codice runtime di ARGOS SOC (privato).
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
Questo repository segue lo stesso schema di versioning di `argos` (SemVer tag `vX.Y.Z`).
|
||||||
|
|
@ -0,0 +1,199 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# ARGOS SOC — Installer ambiente
|
||||||
|
# Tecnotel Servizi SRL — Ubuntu 24.04 LTS
|
||||||
|
# Uso: sudo bash install.sh
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||||||
|
info() { echo -e "${CYAN}[INFO]${NC} $1"; }
|
||||||
|
success() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||||
|
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||||
|
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
|
||||||
|
section() { echo -e "\n${BLUE}══════════════════════════════════════${NC}"; echo -e "${BLUE} $1${NC}"; echo -e "${BLUE}══════════════════════════════════════${NC}"; }
|
||||||
|
|
||||||
|
[[ $EUID -ne 0 ]] && error "Eseguire come root: sudo bash install.sh"
|
||||||
|
. /etc/os-release
|
||||||
|
[[ "$ID" != "ubuntu" || "$VERSION_ID" != "24.04" ]] && error "Richiesto Ubuntu 24.04 LTS"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}╔══════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${BLUE}║ ARGOS SOC — Installer v1.0.0 ║${NC}"
|
||||||
|
echo -e "${BLUE}║ Tecnotel Servizi SRL ║${NC}"
|
||||||
|
echo -e "${BLUE}╚══════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
|
||||||
|
GITEA_REPO="https://3eefb5a2802e8c5a9395396b1bb98e2d5fe46101@argos-update.tecnotelsrl.com:3443/tecnotel/argos.git"
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
section "1. Sistema base"
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
timedatectl set-timezone Europe/Rome
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get upgrade -y -qq
|
||||||
|
apt-get install -y -qq \
|
||||||
|
curl wget git vim htop unzip jq \
|
||||||
|
python3 python3-pip python3-venv \
|
||||||
|
nginx certbot python3-certbot-nginx \
|
||||||
|
ufw fail2ban \
|
||||||
|
build-essential libssl-dev libffi-dev python3-dev \
|
||||||
|
sqlite3 net-tools dnsutils lsof tcpdump nmap \
|
||||||
|
ca-certificates gnupg apt-transport-https
|
||||||
|
success "Pacchetti sistema installati"
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
section "2. Node.js 20 LTS"
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
if ! node --version 2>/dev/null | grep -q "v2[02]"; then
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - >/dev/null 2>&1
|
||||||
|
apt-get install -y nodejs -qq
|
||||||
|
fi
|
||||||
|
success "Node.js $(node --version) installato"
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
section "3. Utente applicazione"
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
if ! id "argos" &>/dev/null; then
|
||||||
|
useradd -r -s /bin/bash -m -d /home/argos argos
|
||||||
|
success "Utente argos creato"
|
||||||
|
else
|
||||||
|
warn "Utente argos già esistente"
|
||||||
|
fi
|
||||||
|
# Aggiungi argos al gruppo systemd-journal per lettura log via journalctl
|
||||||
|
# (necessario per la pagina 'Log Servizi' in UI)
|
||||||
|
usermod -aG systemd-journal argos
|
||||||
|
success "Utente argos aggiunto al gruppo systemd-journal"
|
||||||
|
|
||||||
|
# Permetti a user 'argos' di restart dei servizi via UI (Backup & Restore)
|
||||||
|
# Scope ristretto: solo restart dei 4 demoni ARGOS, nessun altro comando.
|
||||||
|
cat > /etc/sudoers.d/argos-systemctl <<'SUDOEOF'
|
||||||
|
argos ALL=(ALL) NOPASSWD: /bin/systemctl restart argos-backend
|
||||||
|
argos ALL=(ALL) NOPASSWD: /bin/systemctl restart argos-sync
|
||||||
|
argos ALL=(ALL) NOPASSWD: /bin/systemctl restart argos-ops
|
||||||
|
argos ALL=(ALL) NOPASSWD: /bin/systemctl restart argos-analytics
|
||||||
|
SUDOEOF
|
||||||
|
chmod 440 /etc/sudoers.d/argos-systemctl
|
||||||
|
visudo -cf /etc/sudoers.d/argos-systemctl > /dev/null
|
||||||
|
success "Sudoers per restart servizi configurato"
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
section "4. Struttura cartelle"
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
mkdir -p /opt/argos/{app,config,data,feeds,logs,certs,backups,setup}
|
||||||
|
mkdir -p /opt/argos/config/assets
|
||||||
|
mkdir -p /opt/argos/data/{reports,models}
|
||||||
|
chown -R argos:argos /opt/argos
|
||||||
|
chmod -R 750 /opt/argos
|
||||||
|
# /opt/argos/feeds: pubblicamente leggibili (FortiGate ETF via nginx/www-data)
|
||||||
|
chmod 755 /opt/argos/feeds
|
||||||
|
success "Struttura /opt/argos/ creata"
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
section "5. Clone repository"
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
git config --global --add safe.directory /opt/argos/app 2>/dev/null || true
|
||||||
|
if [[ -d /opt/argos/app/.git ]]; then
|
||||||
|
warn "Repository già presente — aggiorno"
|
||||||
|
git -C /opt/argos/app pull origin main
|
||||||
|
else
|
||||||
|
git clone "$GITEA_REPO" /opt/argos/app
|
||||||
|
fi
|
||||||
|
chown -R argos:argos /opt/argos/app
|
||||||
|
success "Repository clonato in /opt/argos/app/"
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
section "6. Python virtualenv"
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
python3 -m venv /opt/argos/app/backend/venv
|
||||||
|
/opt/argos/app/backend/venv/bin/pip install --upgrade pip -q
|
||||||
|
if [[ -f /opt/argos/app/backend/requirements.txt ]]; then
|
||||||
|
/opt/argos/app/backend/venv/bin/pip install -r /opt/argos/app/backend/requirements.txt -q
|
||||||
|
success "Dipendenze Python installate"
|
||||||
|
else
|
||||||
|
warn "requirements.txt non trovato — installare manualmente dopo"
|
||||||
|
fi
|
||||||
|
chown -R argos:argos /opt/argos/app/backend/venv
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
section "7. Firewall UFW"
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
ufw --force reset >/dev/null
|
||||||
|
ufw default deny incoming >/dev/null
|
||||||
|
ufw default allow outgoing >/dev/null
|
||||||
|
ufw allow 22/tcp comment 'SSH' >/dev/null
|
||||||
|
ufw allow 80/tcp comment 'HTTP' >/dev/null
|
||||||
|
ufw allow 443/tcp comment 'HTTPS' >/dev/null
|
||||||
|
ufw allow 8888/tcp comment 'ARGOS Web Installer (temporaneo)' >/dev/null
|
||||||
|
ufw --force enable >/dev/null
|
||||||
|
success "Firewall UFW configurato"
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
section "8. Fail2ban"
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
systemctl enable --now fail2ban >/dev/null 2>&1
|
||||||
|
success "Fail2ban attivo"
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
section "9. Nginx temporaneo"
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
rm -f /etc/nginx/sites-enabled/default
|
||||||
|
cat > /etc/nginx/sites-available/argos-setup << 'NGINX'
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
server_name _;
|
||||||
|
return 200 'ARGOS SOC Setup in corso — vai a http://IP:8888';
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
}
|
||||||
|
NGINX
|
||||||
|
ln -sf /etc/nginx/sites-available/argos-setup /etc/nginx/sites-enabled/
|
||||||
|
nginx -t && systemctl restart nginx
|
||||||
|
success "Nginx temporaneo configurato"
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
section "10. Web Installer"
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# Copia file setup
|
||||||
|
cp /opt/argos/app/scripts/setup_server.py /opt/argos/setup/
|
||||||
|
cp /opt/argos/app/scripts/setup.html /opt/argos/setup/
|
||||||
|
|
||||||
|
# Systemd service web installer
|
||||||
|
cat > /etc/systemd/system/argos-setup.service << 'EOF'
|
||||||
|
[Unit]
|
||||||
|
Description=ARGOS SOC Web Installer
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/opt/argos/setup
|
||||||
|
ExecStart=/opt/argos/app/backend/venv/bin/python3 /opt/argos/setup/setup_server.py
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=3
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=argos-setup
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now argos-setup
|
||||||
|
success "Web installer avviato"
|
||||||
|
|
||||||
|
SERVER_IP=$(hostname -I | awk '{print $1}')
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}╔══════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${GREEN}║ Ambiente pronto! ║${NC}"
|
||||||
|
echo -e "${GREEN}╚══════════════════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${CYAN}Completa la configurazione aprendo nel browser:${NC}"
|
||||||
|
echo -e " ${YELLOW}→ http://${SERVER_IP}:8888${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${YELLOW}NOTA:${NC} La porta 8888 verrà chiusa automaticamente"
|
||||||
|
echo -e " al termine dell'installazione."
|
||||||
|
echo ""
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
ARGOS SOC — Generatore argos.json
|
||||||
|
Legge le variabili d'ambiente impostate dall'installer e genera /opt/argos/config/argos.json
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
def e(key, default=""):
|
||||||
|
return os.environ.get(key, default)
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"_version": "1.0",
|
||||||
|
"_cliente": e("CLIENTE"),
|
||||||
|
"_domain": e("DOMAIN"),
|
||||||
|
"_installed": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||||
|
|
||||||
|
"cliente": {
|
||||||
|
"name": e("CLIENTE"),
|
||||||
|
"full_name": e("CLIENTE_FULL"),
|
||||||
|
"domain": e("CLIENTE_DOMAIN"),
|
||||||
|
"type": e("CLIENTE_TYPE", "enterprise"),
|
||||||
|
"sharepoint_tenant": e("SP_TENANT")
|
||||||
|
},
|
||||||
|
|
||||||
|
"system": {
|
||||||
|
"secret_key": e("SECRET_KEY"),
|
||||||
|
"internal_api_key": e("INTERNAL_KEY"),
|
||||||
|
"anthropic_key": e("ANTHROPIC_KEY"),
|
||||||
|
"ai_context": e("AI_CONTEXT", "ARGOS SOC"),
|
||||||
|
"tz": "Europe/Rome"
|
||||||
|
},
|
||||||
|
|
||||||
|
"paths": {
|
||||||
|
"data_dir": "/opt/argos/data",
|
||||||
|
"feeds_dir": "/opt/argos/feeds",
|
||||||
|
"logs_dir": "/opt/argos/logs",
|
||||||
|
"config_dir": "/opt/argos/config",
|
||||||
|
"analytics_db": "/opt/argos/data/analytics.db",
|
||||||
|
"analytics_exclude_entities": ""
|
||||||
|
},
|
||||||
|
|
||||||
|
"ports": {
|
||||||
|
"backend": 8080,
|
||||||
|
"sync": 8081,
|
||||||
|
"ops": 8082,
|
||||||
|
"analytics": 8083
|
||||||
|
},
|
||||||
|
|
||||||
|
"opensearch": {
|
||||||
|
"url": e("OS_URL"),
|
||||||
|
"user": e("OS_USER", "admin"),
|
||||||
|
"password": e("OS_PASS")
|
||||||
|
},
|
||||||
|
|
||||||
|
"wazuh": {
|
||||||
|
"api_url": e("WAZUH_API_URL"),
|
||||||
|
"api_user": e("WAZUH_API_USER", "wazuh"),
|
||||||
|
"api_pass": e("WAZUH_API_PASS"),
|
||||||
|
"manager_name": e("WAZUH_MANAGER", "wazuh")
|
||||||
|
},
|
||||||
|
|
||||||
|
"entra": {
|
||||||
|
"tenant_id": e("ENTRA_TENANT"),
|
||||||
|
"client_id": e("ENTRA_CLIENT"),
|
||||||
|
"client_secret": e("ENTRA_SECRET")
|
||||||
|
},
|
||||||
|
|
||||||
|
"eset": {
|
||||||
|
"region": "eu",
|
||||||
|
"api_user": e("ESET_USER"),
|
||||||
|
"api_pass": e("ESET_PASS")
|
||||||
|
},
|
||||||
|
|
||||||
|
"fortigate": {
|
||||||
|
"hosts": e("FGT_HOSTS"),
|
||||||
|
"port": e("FGT_PORT", "443"),
|
||||||
|
"tokens": e("FGT_TOKENS"),
|
||||||
|
"names": e("FGT_NAMES")
|
||||||
|
},
|
||||||
|
|
||||||
|
"smtp": {
|
||||||
|
"host": e("SMTP_HOST"),
|
||||||
|
"port": int(e("SMTP_PORT", "587")),
|
||||||
|
"user": e("SMTP_USER"),
|
||||||
|
"password": e("SMTP_PASS"),
|
||||||
|
"from_email": e("FROM_EMAIL")
|
||||||
|
},
|
||||||
|
|
||||||
|
"threat_intel": {
|
||||||
|
"abuseipdb_key": e("ABUSEIPDB_KEY"),
|
||||||
|
"maltiverse_key": e("MALTIVERSE_KEY")
|
||||||
|
},
|
||||||
|
|
||||||
|
"pdf": {
|
||||||
|
"org_name": e("PDF_ORG_NAME"),
|
||||||
|
"author": e("PDF_AUTHOR", "Tecnotel Servizi SRL"),
|
||||||
|
"motto": e("PDF_MOTTO", "Controllo totale. Difesa continua."),
|
||||||
|
"classification": e("PDF_CLASS", "RISERVATO - USO INTERNO"),
|
||||||
|
"app_logo": "/opt/argos/app/frontend/dist/logo_argos_bianco.png",
|
||||||
|
"client_logo": "/opt/argos/data/logo_cliente_pdf.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out = "/opt/argos/config/argos.json"
|
||||||
|
with open(out, "w") as f:
|
||||||
|
json.dump(config, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
print(f"argos.json generato: {out}")
|
||||||
|
|
@ -0,0 +1,419 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ARGOS SOC — Installazione</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Barlow:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
:root{
|
||||||
|
--bg:#080a0e;--bg2:#0e1117;--bg3:#161b24;--bg4:#1c2333;
|
||||||
|
--border:#1e2840;--border2:#2a3a58;
|
||||||
|
--text:#e2e8f4;--text2:#8a9ab8;--text3:#445068;
|
||||||
|
--brand:#F09000;--brand2:#cc7a00;--brand-dim:#F0900018;
|
||||||
|
--ok:#10b981;--ok-dim:#10b98118;--err:#ef4444;--err-dim:#ef444418;--warn:#f59e0b;
|
||||||
|
--mono:'JetBrains Mono',monospace;--sans:'Barlow',sans-serif;--r:10px;
|
||||||
|
}
|
||||||
|
html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--sans);font-size:14px;line-height:1.6}
|
||||||
|
.shell{min-height:100vh;display:grid;grid-template-rows:auto 1fr auto}
|
||||||
|
.hdr{background:var(--bg2);border-bottom:1px solid var(--border);padding:16px 32px;display:flex;align-items:center;gap:16px}
|
||||||
|
.hdr-logo{font-size:22px;font-weight:800;color:var(--brand);letter-spacing:3px}
|
||||||
|
.hdr-sep{width:1px;height:24px;background:var(--border2)}
|
||||||
|
.hdr-sub{font-size:12px;color:var(--text3);font-family:var(--mono);text-transform:uppercase;letter-spacing:1.5px}
|
||||||
|
.hdr-badge{margin-left:auto;font-size:10px;font-family:var(--mono);background:var(--brand-dim);color:var(--brand);padding:4px 10px;border-radius:20px;border:1px solid #F0900033}
|
||||||
|
.main{display:flex;max-width:1000px;margin:0 auto;width:100%;padding:32px 24px;gap:28px}
|
||||||
|
.sidebar{width:200px;flex-shrink:0}
|
||||||
|
.sidebar-title{font-size:10px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:1.5px;padding:0 12px 12px;font-family:var(--mono)}
|
||||||
|
.tab-item{display:flex;align-items:center;gap:10px;padding:10px 12px;border-radius:var(--r);cursor:pointer;transition:all .15s;color:var(--text2);font-size:13px;font-weight:500;margin-bottom:2px;border:1px solid transparent}
|
||||||
|
.tab-item:hover{background:var(--bg3);color:var(--text)}
|
||||||
|
.tab-item.active{background:var(--bg3);color:var(--brand);border-color:var(--border2)}
|
||||||
|
.tab-item.done .tab-num{background:var(--ok);color:#000}
|
||||||
|
.tab-item.active .tab-num{background:var(--brand);color:#000}
|
||||||
|
.tab-num{width:22px;height:22px;border-radius:50%;background:var(--bg4);color:var(--text3);font-size:10px;font-weight:700;font-family:var(--mono);display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:all .2s}
|
||||||
|
.content{flex:1;min-width:0}
|
||||||
|
.tab-panel{display:none}
|
||||||
|
.tab-panel.active{display:block}
|
||||||
|
.panel-title{font-size:22px;font-weight:700;margin-bottom:4px}
|
||||||
|
.panel-sub{font-size:13px;color:var(--text2);margin-bottom:24px}
|
||||||
|
.progress{display:flex;gap:6px;margin-bottom:28px}
|
||||||
|
.prog-step{flex:1;height:3px;border-radius:2px;background:var(--border2);transition:background .3s}
|
||||||
|
.prog-step.done{background:var(--ok)}
|
||||||
|
.prog-step.active{background:var(--brand)}
|
||||||
|
.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
||||||
|
.form-full{grid-column:1/-1}
|
||||||
|
.form-label{display:block;font-size:11px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:.8px;margin-bottom:6px}
|
||||||
|
.form-label span{color:var(--err);margin-left:2px}
|
||||||
|
.form-input,.form-select{width:100%;background:var(--bg3);border:1px solid var(--border2);border-radius:var(--r);padding:10px 14px;color:var(--text);font-family:var(--sans);font-size:13px;outline:none;transition:border-color .15s,box-shadow .15s}
|
||||||
|
.form-input:focus,.form-select:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--brand-dim)}
|
||||||
|
.form-input::placeholder{color:var(--text3)}
|
||||||
|
.mono-input{font-family:var(--mono);font-size:12px}
|
||||||
|
.form-hint{font-size:11px;color:var(--text3);margin-top:5px}
|
||||||
|
.sec-div{grid-column:1/-1;font-size:10px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:1.5px;font-family:var(--mono);border-top:1px solid var(--border);padding-top:18px;margin-top:4px}
|
||||||
|
.sec-div:first-child{border-top:none;padding-top:0;margin-top:0}
|
||||||
|
.ssl-toggle{grid-column:1/-1;display:flex;gap:8px}
|
||||||
|
.ssl-btn{flex:1;padding:12px 16px;background:var(--bg3);border:1px solid var(--border2);border-radius:var(--r);cursor:pointer;text-align:center;font-size:13px;font-weight:600;color:var(--text2);transition:all .15s}
|
||||||
|
.ssl-btn.active{background:var(--brand-dim);border-color:var(--brand);color:var(--brand)}
|
||||||
|
.ssl-btn small{display:block;font-weight:400;font-size:11px;opacity:.7;margin-top:2px}
|
||||||
|
.upload-area{grid-column:1/-1;border:2px dashed var(--border2);border-radius:var(--r);padding:20px;text-align:center;cursor:pointer;transition:all .15s}
|
||||||
|
.upload-area:hover,.upload-area.drag{border-color:var(--brand);background:var(--brand-dim)}
|
||||||
|
.upload-preview{display:none;align-items:center;justify-content:center;gap:10px;margin-top:10px}
|
||||||
|
.upload-preview img{max-height:50px;max-width:180px;border-radius:6px}
|
||||||
|
.alert{padding:12px 16px;border-radius:var(--r);font-size:13px;margin-bottom:0}
|
||||||
|
.alert-err{background:var(--err-dim);border:1px solid #ef444433;color:var(--err)}
|
||||||
|
.alert-ok{background:var(--ok-dim);border:1px solid #10b98133;color:var(--ok)}
|
||||||
|
.alert-warn{background:#f59e0b15;border:1px solid #f59e0b33;color:var(--warn)}
|
||||||
|
.nav-row{display:flex;justify-content:space-between;align-items:center;margin-top:28px;padding-top:20px;border-top:1px solid var(--border)}
|
||||||
|
.btn{padding:10px 22px;border-radius:var(--r);border:none;font-family:var(--sans);font-size:13px;font-weight:700;cursor:pointer;transition:all .15s;display:inline-flex;align-items:center;gap:8px}
|
||||||
|
.btn-brand{background:var(--brand);color:#000}
|
||||||
|
.btn-brand:hover{background:var(--brand2)}
|
||||||
|
.btn-ghost{background:transparent;color:var(--text2);border:1px solid var(--border2)}
|
||||||
|
.btn-ghost:hover{background:var(--bg3);color:var(--text)}
|
||||||
|
.btn-ok{background:var(--ok);color:#000;font-size:15px;padding:14px 36px}
|
||||||
|
.btn-ok:hover{filter:brightness(1.1)}
|
||||||
|
.btn:disabled{opacity:.4;cursor:not-allowed}
|
||||||
|
.summary-table{background:var(--bg3);border:1px solid var(--border);border-radius:var(--r);overflow:hidden;margin-bottom:16px}
|
||||||
|
.summary-section{font-size:10px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:1px;padding:10px 14px;font-family:var(--mono);background:var(--bg4);border-bottom:1px solid var(--border)}
|
||||||
|
.summary-row{display:flex;border-bottom:1px solid #1e284044}
|
||||||
|
.summary-row:last-child{border-bottom:none}
|
||||||
|
.summary-label{width:160px;padding:8px 14px;font-size:12px;color:var(--text3);flex-shrink:0}
|
||||||
|
.summary-value{padding:8px 14px;font-size:12px;color:var(--text);font-family:var(--mono);flex:1}
|
||||||
|
.summary-value.empty{color:var(--text3);font-style:italic;font-family:var(--sans)}
|
||||||
|
.footer{background:var(--bg2);border-top:1px solid var(--border);padding:12px 32px;text-align:center;font-size:11px;color:var(--text3);font-family:var(--mono)}
|
||||||
|
@keyframes spin{to{transform:rotate(360deg)}}
|
||||||
|
@keyframes pulse{0%,100%{opacity:.3;transform:scale(.8)}50%{opacity:1;transform:scale(1.2)}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="shell">
|
||||||
|
<header class="hdr">
|
||||||
|
<div class="hdr-logo">ARGOS</div>
|
||||||
|
<div class="hdr-sep"></div>
|
||||||
|
<div class="hdr-sub">Setup Wizard — Tecnotel Servizi SRL</div>
|
||||||
|
<div class="hdr-badge">v1.0.0</div>
|
||||||
|
</header>
|
||||||
|
<div class="main">
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="sidebar-title">Configurazione</div>
|
||||||
|
<div class="tab-item active" onclick="goTab(0)" id="tab-0"><div class="tab-num">1</div> Cliente</div>
|
||||||
|
<div class="tab-item" onclick="goTab(1)" id="tab-1"><div class="tab-num">2</div> Rete & SSL</div>
|
||||||
|
<div class="tab-item" onclick="goTab(2)" id="tab-2"><div class="tab-num">3</div> SIEM</div>
|
||||||
|
<div class="tab-item" onclick="goTab(3)" id="tab-3"><div class="tab-num">4</div> Utente admin</div>
|
||||||
|
<div class="tab-item" onclick="goTab(4)" id="tab-4"><div class="tab-num">5</div> Installa</div>
|
||||||
|
</aside>
|
||||||
|
<div class="content">
|
||||||
|
<div class="progress">
|
||||||
|
<div class="prog-step active" id="ps-0"></div>
|
||||||
|
<div class="prog-step" id="ps-1"></div>
|
||||||
|
<div class="prog-step" id="ps-2"></div>
|
||||||
|
<div class="prog-step" id="ps-3"></div>
|
||||||
|
<div class="prog-step" id="ps-4"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab 1: Cliente -->
|
||||||
|
<div class="tab-panel active" id="panel-0">
|
||||||
|
<div class="panel-title">👤 Informazioni cliente</div>
|
||||||
|
<div class="panel-sub">Dati dell'organizzazione che utilizzerà ARGOS SOC.</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div><label class="form-label">Nome breve <span>*</span></label><input class="form-input" id="cliente_name" placeholder="es. NomeAzienda"><div class="form-hint">Usato nei titoli e intestazioni</div></div>
|
||||||
|
<div><label class="form-label">Tipo organizzazione</label><select class="form-select" id="cliente_type"><option value="enterprise">Azienda / Enterprise</option><option value="healthcare">Sanità / Healthcare</option><option value="pa">Pubblica Amministrazione</option><option value="education">Istruzione</option></select></div>
|
||||||
|
<div class="form-full"><label class="form-label">Nome completo</label><input class="form-input" id="cliente_full" placeholder="es. Nome Azienda Srl"></div>
|
||||||
|
<div><label class="form-label">Dominio email <span>*</span></label><input class="form-input" id="cliente_domain" placeholder="es. azienda.it"><div class="form-hint">Dominio utenti (es. nome@azienda.it)</div></div>
|
||||||
|
<div><label class="form-label">SharePoint tenant</label><input class="form-input" id="sp_tenant" placeholder="es. nomeazienda"><div class="form-hint">Solo se Microsoft 365. Vuoto se non usato.</div></div>
|
||||||
|
<div class="sec-div">Logo cliente (per i PDF report)</div>
|
||||||
|
<div class="upload-area form-full" id="logo-drop" ondragover="event.preventDefault();this.classList.add('drag')" ondragleave="this.classList.remove('drag')" ondrop="handleLogoDrop(event)" onclick="document.getElementById('logo-input').click()">
|
||||||
|
<input type="file" id="logo-input" accept="image/*" style="display:none" onchange="handleLogoFile(this)">
|
||||||
|
<div style="font-size:24px;margin-bottom:6px">🖼️</div>
|
||||||
|
<div style="font-size:13px;color:var(--text2)">Trascina il logo o clicca per selezionare</div>
|
||||||
|
<div style="font-size:11px;color:var(--text3);margin-top:3px">PNG, JPG — max 2MB — opzionale</div>
|
||||||
|
<div class="upload-preview" id="logo-preview"><img id="logo-img" src=""><div style="font-size:11px;color:var(--ok);font-family:var(--mono)" id="logo-fname"></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-row"><div></div><button class="btn btn-brand" onclick="goTab(1)">Avanti →</button></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab 2: Rete & SSL -->
|
||||||
|
<div class="tab-panel" id="panel-1">
|
||||||
|
<div class="panel-title">🌐 Rete & SSL</div>
|
||||||
|
<div class="panel-sub">Configurazione dominio e certificato SSL. Il DNS deve già puntare a questo server.</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-full"><label class="form-label">Hostname principale <span>*</span></label><input class="form-input mono-input" id="domain" placeholder="es. soc.azienda.it"></div>
|
||||||
|
<div class="form-full"><label class="form-label">Nomi alternativi</label><input class="form-input mono-input" id="aliases" placeholder="es. argos.azienda.it soc2.azienda.it"><div class="form-hint">Separati da spazio — opzionale</div></div>
|
||||||
|
<div class="sec-div">Certificato SSL</div>
|
||||||
|
<div class="ssl-toggle">
|
||||||
|
<div class="ssl-btn active" id="ssl-le" onclick="setSsl('letsencrypt')">🔒 Let's Encrypt<small>Automatico e gratuito</small></div>
|
||||||
|
<div class="ssl-btn" id="ssl-manual" onclick="setSsl('manual')">📄 Certificato esistente<small>Carica .crt e .key</small></div>
|
||||||
|
</div>
|
||||||
|
<div id="le-block" class="form-full"><label class="form-label">Email amministratore <span>*</span></label><input class="form-input" id="admin_email" placeholder="admin@tecnotelsrl.com"><div class="form-hint">Per notifiche di scadenza certificato</div></div>
|
||||||
|
<div id="manual-block" class="form-full" style="display:none">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div><label class="form-label">File .crt <span>*</span></label><input type="file" class="form-input" accept=".crt,.pem,.cer" onchange="uploadSsl(this,'cert')"><div class="form-hint" id="crt-status">Nessun file selezionato</div></div>
|
||||||
|
<div><label class="form-label">File .key <span>*</span></label><input type="file" class="form-input" accept=".key,.pem" onchange="uploadSsl(this,'key')"><div class="form-hint" id="key-status">Nessun file selezionato</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-row"><button class="btn btn-ghost" onclick="goTab(0)">← Indietro</button><button class="btn btn-brand" onclick="goTab(2)">Avanti →</button></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab 3: SIEM -->
|
||||||
|
<div class="tab-panel" id="panel-2">
|
||||||
|
<div class="panel-title">🔍 SIEM — OpenSearch</div>
|
||||||
|
<div class="panel-sub">Connessione al backend SIEM. Le altre integrazioni si configurano dall'interfaccia ARGOS dopo l'installazione.</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-full"><label class="form-label">URL OpenSearch <span>*</span></label><input class="form-input mono-input" id="os_url" placeholder="https://10.0.0.30:9200"></div>
|
||||||
|
<div><label class="form-label">Username</label><input class="form-input" id="os_user" placeholder="admin" value="admin"></div>
|
||||||
|
<div><label class="form-label">Password <span>*</span></label><input class="form-input" type="password" id="os_pass" placeholder="••••••••"></div>
|
||||||
|
<div class="form-full"><div class="alert alert-warn">ℹ️ Le integrazioni (ESET, FortiGate, Entra ID, ecc.) si configurano dopo il primo accesso dalla sezione <strong>Integrazioni</strong> di ARGOS.</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-row"><button class="btn btn-ghost" onclick="goTab(1)">← Indietro</button><button class="btn btn-brand" onclick="goTab(3)">Avanti →</button></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab 4: Admin -->
|
||||||
|
<div class="tab-panel" id="panel-3">
|
||||||
|
<div class="panel-title">🔑 Utente amministratore</div>
|
||||||
|
<div class="panel-sub">Crea il primo account admin per accedere a ARGOS SOC.</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div><label class="form-label">Username <span>*</span></label><input class="form-input" id="admin_username" placeholder="admin" value="admin"></div>
|
||||||
|
<div><label class="form-label">Email</label><input class="form-input" id="admin_email_user" placeholder="admin@azienda.it"></div>
|
||||||
|
<div><label class="form-label">Password <span>*</span></label><input class="form-input" type="password" id="admin_password" placeholder="Min. 8 caratteri"></div>
|
||||||
|
<div><label class="form-label">Conferma password <span>*</span></label><input class="form-input" type="password" id="admin_password2" placeholder="Ripeti la password"></div>
|
||||||
|
<div class="form-full" id="pw-error" style="display:none"><div class="alert alert-err">Le password non corrispondono</div></div>
|
||||||
|
<div class="form-full"><div class="alert alert-ok">✓ Potrai creare altri utenti con ruoli diversi dall'interfaccia ARGOS dopo l'installazione.</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-row"><button class="btn btn-ghost" onclick="goTab(2)">← Indietro</button><button class="btn btn-brand" onclick="goToInstall()">Avanti →</button></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab 5: Installa -->
|
||||||
|
<div class="tab-panel" id="panel-4">
|
||||||
|
<div class="panel-title">🚀 Riepilogo & Installazione</div>
|
||||||
|
<div class="panel-sub">Verifica i dati e avvia l'installazione.</div>
|
||||||
|
|
||||||
|
<!-- Riepilogo -->
|
||||||
|
<div id="summary-box"></div>
|
||||||
|
|
||||||
|
<!-- Errori validazione -->
|
||||||
|
<div id="errors-box" style="display:none"></div>
|
||||||
|
|
||||||
|
<!-- Spinner installazione -->
|
||||||
|
<div id="spinner-box" style="display:none;text-align:center;padding:40px 20px">
|
||||||
|
<div style="font-size:40px;margin-bottom:16px">⚙️</div>
|
||||||
|
<div style="font-size:18px;font-weight:700;margin-bottom:8px">Installazione in corso...</div>
|
||||||
|
<div style="font-size:13px;color:var(--text2);margin-bottom:24px">Attendere il completamento. Richiede circa 5-10 minuti.</div>
|
||||||
|
<div style="display:flex;justify-content:center;gap:8px;margin-bottom:20px">
|
||||||
|
<div style="width:12px;height:12px;border-radius:50%;background:var(--brand);animation:pulse 1.2s ease-in-out 0s infinite"></div>
|
||||||
|
<div style="width:12px;height:12px;border-radius:50%;background:var(--brand);animation:pulse 1.2s ease-in-out 0.4s infinite"></div>
|
||||||
|
<div style="width:12px;height:12px;border-radius:50%;background:var(--brand);animation:pulse 1.2s ease-in-out 0.8s infinite"></div>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:12px;color:var(--text3);font-family:var(--mono)" id="spinner-status">Avvio installazione...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Successo con tasto -->
|
||||||
|
<div id="success-box" style="display:none;text-align:center;padding:40px 20px">
|
||||||
|
<div style="font-size:56px;margin-bottom:16px">✅</div>
|
||||||
|
<div style="font-size:24px;font-weight:800;color:var(--ok);margin-bottom:8px">Installazione completata!</div>
|
||||||
|
<div style="font-size:14px;color:var(--text2);margin-bottom:24px">ARGOS SOC è operativo e pronto all'uso.</div>
|
||||||
|
<button class="btn btn-ok" id="btn-open" onclick="openArgos()">Apri ARGOS SOC →</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nav -->
|
||||||
|
<div class="nav-row" id="install-nav">
|
||||||
|
<button class="btn btn-ghost" onclick="goTab(3)">← Indietro</button>
|
||||||
|
<button class="btn btn-ok" id="btn-install" onclick="avviaInstallazione()">🚀 Avvia installazione</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="footer">ARGOS SOC — Tecnotel Servizi SRL · Web Installer v1.0.0 · La porta 8888 verrà chiusa al termine</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentTab = 0;
|
||||||
|
let sslMode = 'letsencrypt';
|
||||||
|
let certUploaded = false;
|
||||||
|
let keyUploaded = false;
|
||||||
|
let installing = false;
|
||||||
|
let installDomain = '';
|
||||||
|
|
||||||
|
function goTab(n) {
|
||||||
|
if (n > currentTab) document.getElementById('tab-' + currentTab).classList.add('done');
|
||||||
|
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.prog-step').forEach((s,i) => {
|
||||||
|
s.classList.remove('active','done');
|
||||||
|
if (i < n) s.classList.add('done');
|
||||||
|
else if (i === n) s.classList.add('active');
|
||||||
|
});
|
||||||
|
document.getElementById('panel-' + n).classList.add('active');
|
||||||
|
document.getElementById('tab-' + n).classList.add('active');
|
||||||
|
currentTab = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToInstall() {
|
||||||
|
const pw1 = g('admin_password'), pw2 = g('admin_password2');
|
||||||
|
const err = document.getElementById('pw-error');
|
||||||
|
if (pw1 !== pw2) { err.style.display = 'block'; return; }
|
||||||
|
err.style.display = 'none';
|
||||||
|
goTab(4);
|
||||||
|
buildSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSsl(mode) {
|
||||||
|
sslMode = mode;
|
||||||
|
document.getElementById('ssl-le').classList.toggle('active', mode === 'letsencrypt');
|
||||||
|
document.getElementById('ssl-manual').classList.toggle('active', mode === 'manual');
|
||||||
|
document.getElementById('le-block').style.display = mode === 'letsencrypt' ? 'block' : 'none';
|
||||||
|
document.getElementById('manual-block').style.display = mode === 'manual' ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadSsl(input, type) {
|
||||||
|
const file = input.files[0]; if (!file) return;
|
||||||
|
const sid = type === 'cert' ? 'crt-status' : 'key-status';
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/upload/' + type, { method: 'POST', body: await file.arrayBuffer() });
|
||||||
|
const d = await r.json();
|
||||||
|
if (d.ok) {
|
||||||
|
document.getElementById(sid).textContent = '✓ ' + file.name;
|
||||||
|
document.getElementById(sid).style.color = 'var(--ok)';
|
||||||
|
if (type === 'cert') certUploaded = true; else keyUploaded = true;
|
||||||
|
}
|
||||||
|
} catch(e) { document.getElementById(sid).textContent = 'Errore: ' + e.message; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLogoDrop(e) { e.preventDefault(); document.getElementById('logo-drop').classList.remove('drag'); processLogo(e.dataTransfer.files[0]); }
|
||||||
|
function handleLogoFile(input) { processLogo(input.files[0]); }
|
||||||
|
async function processLogo(file) {
|
||||||
|
if (!file || file.size > 2*1024*1024) return;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = e => {
|
||||||
|
document.getElementById('logo-img').src = e.target.result;
|
||||||
|
document.getElementById('logo-fname').textContent = file.name;
|
||||||
|
document.getElementById('logo-preview').style.display = 'flex';
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
fetch('/api/upload/logo', { method: 'POST', body: await file.arrayBuffer() });
|
||||||
|
}
|
||||||
|
|
||||||
|
function g(id) { return (document.getElementById(id)?.value || '').trim(); }
|
||||||
|
|
||||||
|
function collectData() {
|
||||||
|
return {
|
||||||
|
cliente_name: g('cliente_name'), cliente_full: g('cliente_full'),
|
||||||
|
cliente_domain: g('cliente_domain'), cliente_type: g('cliente_type'),
|
||||||
|
sp_tenant: g('sp_tenant'), domain: g('domain'), aliases: g('aliases'),
|
||||||
|
ssl_mode: sslMode, admin_email: g('admin_email'),
|
||||||
|
os_url: g('os_url'), os_user: g('os_user') || 'admin', os_pass: g('os_pass'),
|
||||||
|
admin_username: g('admin_username') || 'admin',
|
||||||
|
admin_email_user: g('admin_email_user'), admin_password: g('admin_password'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSummary() {
|
||||||
|
const d = collectData();
|
||||||
|
const errors = [];
|
||||||
|
if (!d.cliente_name) errors.push('Nome cliente obbligatorio');
|
||||||
|
if (!d.cliente_domain) errors.push('Dominio email obbligatorio');
|
||||||
|
if (!d.domain) errors.push('Hostname obbligatorio');
|
||||||
|
if (!d.os_url) errors.push('URL OpenSearch obbligatorio');
|
||||||
|
if (!d.os_pass) errors.push('Password OpenSearch obbligatoria');
|
||||||
|
if (!d.admin_password) errors.push('Password admin obbligatoria');
|
||||||
|
if (sslMode === 'letsencrypt' && !d.admin_email) errors.push("Email per Let's Encrypt obbligatoria");
|
||||||
|
if (sslMode === 'manual' && (!certUploaded || !keyUploaded)) errors.push('Certificato SSL (.crt e .key) non caricato');
|
||||||
|
|
||||||
|
const row = (l, v) => '<div class="summary-row"><div class="summary-label">' + l + '</div><div class="summary-value ' + (v ? '' : 'empty') + '">' + (v || 'non configurato') + '</div></div>';
|
||||||
|
|
||||||
|
document.getElementById('summary-box').innerHTML = '<div class="summary-table">' +
|
||||||
|
'<div class="summary-section">Cliente</div>' +
|
||||||
|
row('Nome', d.cliente_name) + row('Nome completo', d.cliente_full) +
|
||||||
|
row('Dominio email', d.cliente_domain) + row('Tipo', d.cliente_type) +
|
||||||
|
'<div class="summary-section">Rete & SSL</div>' +
|
||||||
|
row('Hostname', d.domain) + row('Alias', d.aliases) +
|
||||||
|
row('SSL', sslMode === 'letsencrypt' ? "Let's Encrypt" : 'Certificato manuale') +
|
||||||
|
'<div class="summary-section">SIEM</div>' +
|
||||||
|
row('OpenSearch URL', d.os_url) + row('Username', d.os_user) +
|
||||||
|
'<div class="summary-section">Utente admin</div>' +
|
||||||
|
row('Username', d.admin_username) + row('Email', d.admin_email_user) +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
const errBox = document.getElementById('errors-box');
|
||||||
|
if (errors.length) {
|
||||||
|
errBox.style.display = 'block';
|
||||||
|
errBox.innerHTML = '<div class="alert alert-err" style="margin-bottom:16px">⚠️ <strong>Correggere prima di procedere:</strong><ul style="margin-top:8px;padding-left:16px">' + errors.map(e => '<li style="margin-top:4px">' + e + '</li>').join('') + '</ul></div>';
|
||||||
|
document.getElementById('btn-install').disabled = true;
|
||||||
|
} else {
|
||||||
|
errBox.style.display = 'none';
|
||||||
|
document.getElementById('btn-install').disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function avviaInstallazione() {
|
||||||
|
if (installing) return;
|
||||||
|
installing = true;
|
||||||
|
const data = collectData();
|
||||||
|
installDomain = data.domain;
|
||||||
|
|
||||||
|
document.getElementById('summary-box').style.display = 'none';
|
||||||
|
document.getElementById('errors-box').style.display = 'none';
|
||||||
|
document.getElementById('install-nav').style.display = 'none';
|
||||||
|
document.getElementById('spinner-box').style.display = 'block';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/install', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
const res = await r.json();
|
||||||
|
if (!res.ok) throw new Error(res.error || 'Errore sconosciuto');
|
||||||
|
pollStatus();
|
||||||
|
} catch(e) {
|
||||||
|
document.getElementById('spinner-status').textContent = 'ERRORE: ' + e.message;
|
||||||
|
document.getElementById('spinner-status').style.color = 'var(--err)';
|
||||||
|
installing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pollStatus() {
|
||||||
|
let errors = 0;
|
||||||
|
const statusEl = document.getElementById('spinner-status');
|
||||||
|
const steps = ['Generazione configurazione...','Build frontend React...','Installazione dipendenze...','Creazione utente admin...','Configurazione SSL...','Configurazione nginx...','Avvio servizi...','Finalizzazione...'];
|
||||||
|
let step = 0;
|
||||||
|
statusEl.textContent = steps[0];
|
||||||
|
|
||||||
|
const stepIv = setInterval(() => { if (step < steps.length-1) statusEl.textContent = steps[++step]; }, 20000);
|
||||||
|
|
||||||
|
const iv = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/status');
|
||||||
|
const d = await r.json();
|
||||||
|
errors = 0;
|
||||||
|
if (d.log && d.log.length) {
|
||||||
|
const last = d.log[d.log.length-1];
|
||||||
|
if (last.includes('frontend')) statusEl.textContent = 'Build frontend React...';
|
||||||
|
else if (last.includes('Python')) statusEl.textContent = 'Installazione dipendenze...';
|
||||||
|
else if (last.includes('admin')) statusEl.textContent = 'Creazione utente admin...';
|
||||||
|
else if (last.includes('SSL') || last.includes('certbot')) statusEl.textContent = 'Configurazione SSL...';
|
||||||
|
else if (last.includes('nginx')) statusEl.textContent = 'Configurazione nginx...';
|
||||||
|
else if (last.includes('avviato')) statusEl.textContent = 'Avvio servizi...';
|
||||||
|
else if (last.includes('COMPLETATA')) statusEl.textContent = 'Completato!';
|
||||||
|
}
|
||||||
|
if (d.done) { clearInterval(iv); clearInterval(stepIv); showSuccess(); }
|
||||||
|
if (d.error) { clearInterval(iv); clearInterval(stepIv); statusEl.textContent = 'Errore — controlla i log del server'; statusEl.style.color = 'var(--err)'; }
|
||||||
|
} catch(e) {
|
||||||
|
errors++;
|
||||||
|
if (errors >= 6) { clearInterval(iv); clearInterval(stepIv); showSuccess(); }
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSuccess() {
|
||||||
|
document.getElementById('spinner-box').style.display = 'none';
|
||||||
|
document.getElementById('success-box').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function openArgos() {
|
||||||
|
window.location.href = 'https://' + installDomain;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,483 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
ARGOS SOC — Web Installer Server
|
||||||
|
Tecnotel Servizi SRL
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import secrets
|
||||||
|
import shutil
|
||||||
|
import hashlib
|
||||||
|
import threading
|
||||||
|
import signal
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
APP_DIR = Path("/opt/argos/app")
|
||||||
|
CONFIG_DIR = Path("/opt/argos/config")
|
||||||
|
DATA_DIR = Path("/opt/argos/data")
|
||||||
|
FEEDS_DIR = Path("/opt/argos/feeds")
|
||||||
|
LOGS_DIR = Path("/opt/argos/logs")
|
||||||
|
CERTS_DIR = Path("/opt/argos/certs")
|
||||||
|
SETUP_DIR = Path("/opt/argos/setup")
|
||||||
|
APP_USER = "argos"
|
||||||
|
PORT = 8888
|
||||||
|
|
||||||
|
install_log = []
|
||||||
|
install_done = False
|
||||||
|
install_error = False
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg):
|
||||||
|
ts = datetime.now().strftime("%H:%M:%S")
|
||||||
|
line = f"[{ts}] {msg}"
|
||||||
|
install_log.append(line)
|
||||||
|
print(line, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def run(cmd, check=True):
|
||||||
|
log(f"$ {cmd}")
|
||||||
|
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
||||||
|
if result.stdout.strip(): log(result.stdout.strip())
|
||||||
|
if result.stderr.strip(): log(result.stderr.strip())
|
||||||
|
if check and result.returncode != 0:
|
||||||
|
raise RuntimeError(f"Comando fallito (exit {result.returncode}): {cmd}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def chown(path):
|
||||||
|
run(f"chown -R {APP_USER}:{APP_USER} {path}", check=False)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_argos_json(data):
|
||||||
|
# Hostname primario dal wizard (campo "domain" nella UI = FQDN tipo soc.azienda.it)
|
||||||
|
hostname = data.get("domain", "").strip().lower()
|
||||||
|
# Alias dal wizard: stringa separata da spazi -> lista pulita
|
||||||
|
aliases_raw = data.get("aliases", "").strip()
|
||||||
|
aliases = [a.strip().lower() for a in aliases_raw.split() if a.strip()]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"_version": "1.2",
|
||||||
|
"_installed": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||||
|
"cliente": {
|
||||||
|
"name": data.get("cliente_name", ""),
|
||||||
|
"full_name": data.get("cliente_full", ""),
|
||||||
|
"domain": data.get("cliente_domain", ""),
|
||||||
|
"type": data.get("cliente_type", "enterprise"),
|
||||||
|
"ai_context": "",
|
||||||
|
"sharepoint_tenant": "",
|
||||||
|
"console_url": ""
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"hostname": hostname,
|
||||||
|
"aliases": aliases
|
||||||
|
},
|
||||||
|
"system": {
|
||||||
|
"secret_key": secrets.token_hex(32),
|
||||||
|
"internal_api_key": secrets.token_hex(24),
|
||||||
|
"tz": "Europe/Rome"
|
||||||
|
},
|
||||||
|
"opensearch": {
|
||||||
|
"url": data.get("os_url", ""),
|
||||||
|
"user": data.get("os_user", "admin"),
|
||||||
|
"password": data.get("os_pass", "")
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"data_dir": str(DATA_DIR),
|
||||||
|
"feeds_dir": str(FEEDS_DIR),
|
||||||
|
"logs_dir": str(LOGS_DIR),
|
||||||
|
"config_dir": str(CONFIG_DIR),
|
||||||
|
"analytics_db": str(DATA_DIR / "analytics.db")
|
||||||
|
},
|
||||||
|
"ports": {
|
||||||
|
"backend": 8080,
|
||||||
|
"sync": 8081,
|
||||||
|
"ops": 8082,
|
||||||
|
"analytics": 8083
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def generate_integrations_json():
|
||||||
|
example = APP_DIR / "config/integrations.json.example"
|
||||||
|
if example.exists():
|
||||||
|
with open(example) as f:
|
||||||
|
return json.load(f)
|
||||||
|
# fallback minimale
|
||||||
|
return {
|
||||||
|
"_version": "1.0",
|
||||||
|
"endpoint": {}, "firewall": {}, "identity": {},
|
||||||
|
"notifications": {}, "threat_intel": {},
|
||||||
|
"pdf": {
|
||||||
|
"org_name": "", "author": "Tecnotel Servizi SRL",
|
||||||
|
"motto": "Controllo totale. Difesa continua.",
|
||||||
|
"classification": "RISERVATO - USO INTERNO",
|
||||||
|
"app_logo": str(APP_DIR / "frontend/dist/logo_argos_bianco.png"),
|
||||||
|
"client_logo": str(CONFIG_DIR / "assets" / "logo_cliente.png")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def create_admin_user(data):
|
||||||
|
username = data.get("admin_username", "admin").strip()
|
||||||
|
email = data.get("admin_email_user", "").strip()
|
||||||
|
password = data.get("admin_password", "").strip()
|
||||||
|
|
||||||
|
if not username or not password:
|
||||||
|
log("WARN: credenziali admin mancanti — skip creazione utente")
|
||||||
|
return
|
||||||
|
|
||||||
|
import os as _os
|
||||||
|
# Hash compatibile con auth.py: salt:sha256(salt+pw+"argos-2026")
|
||||||
|
salt = _os.urandom(16).hex()
|
||||||
|
pw_hash = f"{salt}:{hashlib.sha256((salt + password + 'argos-2026').encode()).hexdigest()}"
|
||||||
|
|
||||||
|
users_file = DATA_DIR / "argos_users.json"
|
||||||
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Leggi utenti esistenti o crea struttura vuota
|
||||||
|
try:
|
||||||
|
with open(users_file) as f:
|
||||||
|
users_data = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
users_data = {"users": {}}
|
||||||
|
|
||||||
|
users_data["users"][username.lower()] = {
|
||||||
|
"password": pw_hash,
|
||||||
|
"role": "admin",
|
||||||
|
"name": username,
|
||||||
|
"email": email,
|
||||||
|
"enabled": True
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(users_file, "w") as f:
|
||||||
|
json.dump(users_data, f, indent=2)
|
||||||
|
|
||||||
|
os.chmod(users_file, 0o600)
|
||||||
|
chown(users_file)
|
||||||
|
log(f"Utente admin '{username}' creato in argos_users.json")
|
||||||
|
|
||||||
|
|
||||||
|
def install(data):
|
||||||
|
global install_done, install_error
|
||||||
|
try:
|
||||||
|
log("=== AVVIO INSTALLAZIONE ARGOS SOC ===")
|
||||||
|
|
||||||
|
# 1. argos.json
|
||||||
|
log("── Generazione argos.json ──")
|
||||||
|
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
config = generate_argos_json(data)
|
||||||
|
cfg_path = CONFIG_DIR / "argos.json"
|
||||||
|
with open(cfg_path, "w") as f:
|
||||||
|
json.dump(config, f, indent=2, ensure_ascii=False)
|
||||||
|
os.chmod(cfg_path, 0o600)
|
||||||
|
chown(CONFIG_DIR)
|
||||||
|
log("argos.json creato")
|
||||||
|
|
||||||
|
# 2. integrations.json
|
||||||
|
log("── Generazione integrations.json ──")
|
||||||
|
integ = generate_integrations_json()
|
||||||
|
# Aggiorna pdf con nome organizzazione
|
||||||
|
integ.setdefault("pdf", {})
|
||||||
|
integ["pdf"]["org_name"] = data.get("cliente_full") or data.get("cliente_name", "")
|
||||||
|
integ["pdf"]["client_logo"] = str(CONFIG_DIR / "assets" / "logo_cliente.png")
|
||||||
|
integ_path = CONFIG_DIR / "integrations.json"
|
||||||
|
with open(integ_path, "w") as f:
|
||||||
|
json.dump(integ, f, indent=2, ensure_ascii=False)
|
||||||
|
os.chmod(integ_path, 0o600)
|
||||||
|
chown(integ_path)
|
||||||
|
log("integrations.json creato")
|
||||||
|
|
||||||
|
# 3. modules.json
|
||||||
|
mods = APP_DIR / "config/modules.json.example"
|
||||||
|
if mods.exists():
|
||||||
|
shutil.copy(mods, CONFIG_DIR / "modules.json")
|
||||||
|
chown(CONFIG_DIR / "modules.json")
|
||||||
|
log("modules.json copiato")
|
||||||
|
|
||||||
|
# 4. Logo cliente
|
||||||
|
logo_src = SETUP_DIR / "logo_cliente.png"
|
||||||
|
if logo_src.exists():
|
||||||
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy(logo_src, CONFIG_DIR / "assets" / "logo_cliente.png")
|
||||||
|
chown(CONFIG_DIR / "assets" / "logo_cliente.png")
|
||||||
|
log("Logo cliente copiato")
|
||||||
|
|
||||||
|
# 5. Build frontend
|
||||||
|
log("── Build frontend React ──")
|
||||||
|
run(f"cd {APP_DIR}/frontend && npm install --silent")
|
||||||
|
run(f"cd {APP_DIR}/frontend && npm run build")
|
||||||
|
chown(APP_DIR / "frontend")
|
||||||
|
# Permessi lettura per nginx (www-data)
|
||||||
|
run(f"chmod 755 /opt/argos /opt/argos/app /opt/argos/app/frontend {APP_DIR}/frontend/dist")
|
||||||
|
run(f"chmod -R 755 {APP_DIR}/frontend/dist/")
|
||||||
|
log("Frontend buildato")
|
||||||
|
|
||||||
|
# 6. Dipendenze Python
|
||||||
|
log("── Dipendenze Python ──")
|
||||||
|
venv_pip = APP_DIR / "backend/venv/bin/pip"
|
||||||
|
req = APP_DIR / "backend/requirements.txt"
|
||||||
|
if req.exists():
|
||||||
|
run(f"{venv_pip} install -r {req} -q")
|
||||||
|
log("Dipendenze Python installate")
|
||||||
|
|
||||||
|
# 7. Utente admin
|
||||||
|
log("── Creazione utente admin ──")
|
||||||
|
create_admin_user(data)
|
||||||
|
|
||||||
|
# 8. SSL
|
||||||
|
log("── Configurazione SSL ──")
|
||||||
|
domain = data.get("domain", "").strip()
|
||||||
|
aliases = data.get("aliases", "").strip()
|
||||||
|
ssl_mode = data.get("ssl_mode", "letsencrypt")
|
||||||
|
all_names = (domain + " " + aliases).strip()
|
||||||
|
|
||||||
|
if ssl_mode == "manual":
|
||||||
|
crt_src = SETUP_DIR / "uploaded.crt"
|
||||||
|
key_src = SETUP_DIR / "uploaded.key"
|
||||||
|
if not crt_src.exists() or not key_src.exists():
|
||||||
|
raise RuntimeError("File SSL .crt o .key non trovati in /opt/argos/setup/")
|
||||||
|
CERTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy(crt_src, CERTS_DIR / "fullchain.pem")
|
||||||
|
shutil.copy(key_src, CERTS_DIR / "privkey.pem")
|
||||||
|
os.chmod(CERTS_DIR / "privkey.pem", 0o600)
|
||||||
|
ssl_crt = str(CERTS_DIR / "fullchain.pem")
|
||||||
|
ssl_key = str(CERTS_DIR / "privkey.pem")
|
||||||
|
log("Certificato SSL manuale copiato")
|
||||||
|
else:
|
||||||
|
_write_nginx_http(all_names)
|
||||||
|
run("nginx -t && systemctl restart nginx")
|
||||||
|
certbot_d = " ".join(f"-d {n}" for n in all_names.split())
|
||||||
|
email = data.get("admin_email", "admin@tecnotelsrl.com")
|
||||||
|
run(f"certbot --nginx {certbot_d} --non-interactive --agree-tos -m {email}")
|
||||||
|
ssl_crt = f"/etc/letsencrypt/live/{domain}/fullchain.pem"
|
||||||
|
ssl_key = f"/etc/letsencrypt/live/{domain}/privkey.pem"
|
||||||
|
log("Certificato Let's Encrypt ottenuto")
|
||||||
|
|
||||||
|
# 9. Nginx finale
|
||||||
|
log("── Nginx configurazione finale ──")
|
||||||
|
_write_nginx_final(all_names, ssl_crt, ssl_key)
|
||||||
|
run("nginx -t && systemctl restart nginx")
|
||||||
|
log("Nginx configurato")
|
||||||
|
|
||||||
|
# 10. Systemd services
|
||||||
|
log("── Creazione e avvio servizi ──")
|
||||||
|
_write_services()
|
||||||
|
run("systemctl daemon-reload")
|
||||||
|
for svc in ["argos-backend", "argos-sync", "argos-ops", "argos-analytics"]:
|
||||||
|
run(f"systemctl enable --now {svc}")
|
||||||
|
log(f"{svc} avviato")
|
||||||
|
|
||||||
|
# 11. Chiudi web installer
|
||||||
|
log("── Chiusura web installer ──")
|
||||||
|
run("systemctl disable --now argos-setup", check=False)
|
||||||
|
run("ufw delete allow 8888/tcp", check=False)
|
||||||
|
log("Porta 8888 chiusa — web installer disabilitato")
|
||||||
|
|
||||||
|
log("=== INSTALLAZIONE COMPLETATA ===")
|
||||||
|
install_done = True
|
||||||
|
|
||||||
|
# Spegni il processo dopo 8s (tempo per inviare la risposta al browser)
|
||||||
|
def shutdown():
|
||||||
|
import time
|
||||||
|
time.sleep(15)
|
||||||
|
os.kill(os.getpid(), signal.SIGTERM)
|
||||||
|
threading.Thread(target=shutdown, daemon=True).start()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log(f"ERRORE: {e}")
|
||||||
|
install_log.append(f"__ERROR__: {e}")
|
||||||
|
install_error = True
|
||||||
|
|
||||||
|
|
||||||
|
def _write_nginx_http(all_names):
|
||||||
|
conf = f"""server {{
|
||||||
|
listen 80;
|
||||||
|
server_name {all_names};
|
||||||
|
location /.well-known/acme-challenge/ {{ root /var/www/html; }}
|
||||||
|
location / {{ return 301 https://$host$request_uri; }}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
_write_nginx_conf(conf)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_nginx_final(all_names, ssl_crt, ssl_key):
|
||||||
|
conf = f"""limit_req_zone $binary_remote_addr zone=argos:10m rate=20r/s;
|
||||||
|
|
||||||
|
server {{
|
||||||
|
listen 80;
|
||||||
|
server_name {all_names};
|
||||||
|
location / {{ return 301 https://$host$request_uri; }}
|
||||||
|
location /.well-known/acme-challenge/ {{ root /var/www/html; }}
|
||||||
|
}}
|
||||||
|
|
||||||
|
server {{
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name {all_names};
|
||||||
|
|
||||||
|
ssl_certificate {ssl_crt};
|
||||||
|
ssl_certificate_key {ssl_key};
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
|
||||||
|
add_header X-Frame-Options SAMEORIGIN;
|
||||||
|
add_header X-Content-Type-Options nosniff;
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000" always;
|
||||||
|
|
||||||
|
client_max_body_size 50m;
|
||||||
|
|
||||||
|
location /api/analytics/ {{
|
||||||
|
limit_req zone=argos burst=40 nodelay;
|
||||||
|
proxy_pass http://127.0.0.1:8083;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
proxy_read_timeout 120s;
|
||||||
|
}}
|
||||||
|
location /feeds/ {{
|
||||||
|
alias {FEEDS_DIR}/;
|
||||||
|
autoindex off;
|
||||||
|
default_type "text/plain; charset=utf-8";
|
||||||
|
access_log {LOGS_DIR}/nginx-feeds.log;
|
||||||
|
}}
|
||||||
|
location /api/ {{
|
||||||
|
limit_req zone=argos burst=40 nodelay;
|
||||||
|
proxy_pass http://127.0.0.1:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
proxy_read_timeout 120s;
|
||||||
|
}}
|
||||||
|
|
||||||
|
location / {{
|
||||||
|
root {APP_DIR}/frontend/dist;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
expires 1h;
|
||||||
|
}}
|
||||||
|
|
||||||
|
access_log {LOGS_DIR}/nginx-access.log;
|
||||||
|
error_log {LOGS_DIR}/nginx-error.log;
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
_write_nginx_conf(conf)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_nginx_conf(conf):
|
||||||
|
with open("/etc/nginx/sites-available/argos", "w") as f:
|
||||||
|
f.write(conf)
|
||||||
|
p = Path("/etc/nginx/sites-enabled/argos")
|
||||||
|
if not p.exists():
|
||||||
|
p.symlink_to("/etc/nginx/sites-available/argos")
|
||||||
|
# Rimuovi default e setup temporaneo
|
||||||
|
for f in ["/etc/nginx/sites-enabled/default", "/etc/nginx/sites-enabled/argos-setup"]:
|
||||||
|
if Path(f).exists(): Path(f).unlink()
|
||||||
|
|
||||||
|
|
||||||
|
def _write_services():
|
||||||
|
venv_py = str(APP_DIR / "backend/venv/bin/python3")
|
||||||
|
backend_dir = str(APP_DIR / "backend")
|
||||||
|
services = {
|
||||||
|
"backend": ("services/argos_backend.py", "Backend API"),
|
||||||
|
"sync": ("services/argos_sync.py", "Sync Daemon"),
|
||||||
|
"ops": ("services/argos_ops.py", "Ops Daemon"),
|
||||||
|
"analytics": ("services/argos_analytics.py", "Analytics Daemon"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for svc, (script, desc) in services.items():
|
||||||
|
unit = f"""[Unit]
|
||||||
|
Description=ARGOS {desc}
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User={APP_USER}
|
||||||
|
Group={APP_USER}
|
||||||
|
WorkingDirectory={backend_dir}
|
||||||
|
Environment=PYTHONPATH={backend_dir}
|
||||||
|
Environment=CONFIG_FILE={CONFIG_DIR}/argos.json
|
||||||
|
Environment=INTEGRATIONS_FILE={CONFIG_DIR}/integrations.json
|
||||||
|
Environment=DATA_DIR={DATA_DIR}
|
||||||
|
Environment=FEEDS_DIR={FEEDS_DIR}
|
||||||
|
Environment=LOGS_DIR={LOGS_DIR}
|
||||||
|
ExecStart={venv_py} {backend_dir}/{script}
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=argos-{svc}
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
"""
|
||||||
|
with open(f"/etc/systemd/system/argos-{svc}.service", "w") as f:
|
||||||
|
f.write(unit)
|
||||||
|
|
||||||
|
|
||||||
|
class SetupHandler(BaseHTTPRequestHandler):
|
||||||
|
def log_message(self, *args): pass
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
path = urlparse(self.path).path
|
||||||
|
if path in ("/", "/setup"):
|
||||||
|
html_path = Path(__file__).parent / "setup.html"
|
||||||
|
if not html_path.exists():
|
||||||
|
self.send_response(404); self.end_headers(); return
|
||||||
|
html = html_path.read_bytes()
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
self.send_header("Content-Length", len(html))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(html)
|
||||||
|
elif path == "/api/status":
|
||||||
|
self._json({"done": install_done, "error": install_error, "log": install_log[-60:]})
|
||||||
|
else:
|
||||||
|
self.send_response(404); self.end_headers()
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
path = urlparse(self.path).path
|
||||||
|
length = int(self.headers.get("Content-Length", 0))
|
||||||
|
body = self.rfile.read(length)
|
||||||
|
|
||||||
|
if path == "/api/install":
|
||||||
|
try:
|
||||||
|
data = json.loads(body)
|
||||||
|
threading.Thread(target=install, args=(data,), daemon=True).start()
|
||||||
|
self._json({"ok": True})
|
||||||
|
except Exception as e:
|
||||||
|
self._json({"ok": False, "error": str(e)}, 400)
|
||||||
|
elif path == "/api/upload/cert":
|
||||||
|
SETUP_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
(SETUP_DIR / "uploaded.crt").write_bytes(body)
|
||||||
|
self._json({"ok": True})
|
||||||
|
elif path == "/api/upload/key":
|
||||||
|
SETUP_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
(SETUP_DIR / "uploaded.key").write_bytes(body)
|
||||||
|
self._json({"ok": True})
|
||||||
|
elif path == "/api/upload/logo":
|
||||||
|
SETUP_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
(SETUP_DIR / "logo_cliente.png").write_bytes(body)
|
||||||
|
self._json({"ok": True})
|
||||||
|
else:
|
||||||
|
self.send_response(404); self.end_headers()
|
||||||
|
|
||||||
|
def _json(self, data, code=200):
|
||||||
|
body = json.dumps(data).encode()
|
||||||
|
self.send_response(code)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.send_header("Content-Length", len(body))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(f"\n{'='*55}")
|
||||||
|
print(f" ARGOS SOC — Web Installer")
|
||||||
|
print(f" Tecnotel Servizi SRL")
|
||||||
|
print(f" Apri: http://<IP_SERVER>:{PORT}")
|
||||||
|
print(f"{'='*55}\n")
|
||||||
|
HTTPServer(("0.0.0.0", PORT), SetupHandler).serve_forever()
|
||||||
Loading…
Reference in New Issue