#!/usr/bin/env python3 """ IRIDE — first_setup.py: wizard di prima configurazione (CLI) Tecnotel Servizi SRL Eseguito da install.sh come utente iride con il venv dell'app e PYTHONPATH=/opt/iride/app/backend. Scrive: - $IRIDE_CONFIG_DIR/iride.json (cliente, dominio, chiavi di sistema generate) - $IRIDE_CONFIG_DIR/users.json (utente admin con hash bcrypt) Non interattivo con IRIDE_CLIENT_NAME, IRIDE_DOMAIN, IRIDE_ADMIN_USER, IRIDE_ADMIN_PASSWORD nell'ambiente. Rieseguibile: non sovrascrive chiavi e utenti già reali senza conferma. Il wizard web (porta 8888, stile ARGOS) arriva dopo il pilota. """ from __future__ import annotations import getpass import json import os import re import secrets import sys from pathlib import Path CONFIG_DIR = Path(os.environ.get("IRIDE_CONFIG_DIR", "/opt/iride/config")) PLACEHOLDER = "GENERATO-DA-first_setup" INTERACTIVE = sys.stdin.isatty() def ask(label: str, default: str = "", env: str = "", secret: bool = False, required: bool = False) -> str: value = os.environ.get(env, "") if env else "" if value: return value if not INTERACTIVE: if required and not default: sys.exit(f"Manca {env or label} (esecuzione non interattiva)") return default prompt = f"{label}" + (f" [{default}]" if default and not secret else "") + ": " while True: value = getpass.getpass(prompt) if secret else input(prompt) value = value.strip() or default if value or not required: return value print(" valore obbligatorio") def slugify(s: str) -> str: return re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-") or "cliente" def load(path: Path) -> dict: try: return json.loads(path.read_text(encoding="utf-8")) except (OSError, ValueError): return {} def save(path: Path, data: dict) -> None: tmp = path.with_suffix(".tmp") tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") os.chmod(tmp, 0o600) tmp.replace(path) def main() -> None: print("\n── IRIDE: prima configurazione ──") iride = load(CONFIG_DIR / "iride.json") users = load(CONFIG_DIR / "users.json") cliente = iride.setdefault("cliente", {}) system = iride.setdefault("system", {}) already = system.get("secret_key") and system.get("secret_key") != PLACEHOLDER if already and INTERACTIVE: keep = ask("Configurazione già presente: mantenerla? (s/n)", "s") if keep.lower().startswith("s"): print("Configurazione mantenuta.") return full_name = ask("Nome del cliente", cliente.get("full_name") or "", "IRIDE_CLIENT_NAME", required=True) cliente["full_name"] = full_name cliente["name"] = slugify(cliente.get("name") if cliente.get("name") not in ("", "cliente") else full_name) cliente["domain"] = ask("Dominio pubblico dell'istanza (FQDN)", cliente.get("domain") or "", "IRIDE_DOMAIN") cliente.setdefault("type", "customer_service") cliente.setdefault("ai_context", "") system.setdefault("timezone", "Europe/Rome") if not already: system["secret_key"] = secrets.token_urlsafe(48) system["internal_api_key"] = secrets.token_urlsafe(32) ai = iride.setdefault("ai", {}) text = ai.setdefault("text", {"provider": "fake", "options": {}}) provider = ask("Provider AI per il testo (fake/openai)", text.get("provider", "fake"), "IRIDE_AI_PROVIDER") text["provider"] = provider if provider in ("fake", "openai", "anthropic") else "fake" if text["provider"] != "fake": providers = ai.setdefault("providers", {}) key = ask(f"API key {text['provider']} (invio per lasciarla vuota)", "", "IRIDE_AI_API_KEY", secret=True) if key: providers.setdefault(text["provider"], {})["api_key"] = key save(CONFIG_DIR / "iride.json", iride) print(f" iride.json scritto (cliente: {cliente['name']}, dominio: {cliente['domain'] or '-'})") from core.auth import hash_password # dal backend dell'app (PYTHONPATH) admin_user = ask("Utente amministratore", "admin", "IRIDE_ADMIN_USER") existing = (users.get("users") or {}).get(admin_user, {}) if existing.get("password_hash", "").startswith("$2") and PLACEHOLDER not in existing.get("password_hash", ""): print(f" utente {admin_user} già presente: password invariata") else: while True: pwd = ask(f"Password di {admin_user} (min 12 caratteri)", "", "IRIDE_ADMIN_PASSWORD", secret=True, required=True) if len(pwd) < 12: print(" troppo corta") if not INTERACTIVE: sys.exit("password troppo corta") continue if INTERACTIVE and not os.environ.get("IRIDE_ADMIN_PASSWORD"): if getpass.getpass("Conferma password: ") != pwd: print(" non coincidono") continue break users.setdefault("users", {})[admin_user] = { "password_hash": hash_password(pwd), "roles": ["admin"], "totp_secret": "", "enabled": True} save(CONFIG_DIR / "users.json", users) print(f" users.json scritto (admin: {admin_user})") print("── configurazione completata ──\n") if __name__ == "__main__": main()