129 lines
5.2 KiB
Python
Executable File
129 lines
5.2 KiB
Python
Executable File
#!/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()
|