<# .SYNOPSIS install.ps1 — installer con un comando solo per "OptiWize Onboarding Inbox" (Windows, PowerShell; per macOS/Linux usare install.sh). .DESCRIPTION COSA FA QUESTO SCRIPT, IN CHIARO (leggilo prima di lanciarlo, e' normale diffidare di uno script ricevuto per mail): 1. Controlla che Docker Desktop sia installato e ACCESO (non basta installato). 2. Scarica ("pull") l'immagine del pannello da un registro Docker pubblico (ghcr.io). Se l'immagine e' privata, chiede un token di accesso (vedi CONFIGURAZIONE piu' sotto) SOLO a schermo, non lo scrive mai su disco ne' lo stampa. 3. La prima volta genera una chiave segreta casuale (serve a cifrare le password dei device salvate dal pannello) e la conserva in un file sul tuo computer, fuori dal container: ai riavvii successivi la trova e la riusa, cosi' i dati gia' salvati restano leggibili. 4. Crea un volume Docker per il database e avvia il container. 5. Apre il browser sul pannello. Non installa nient'altro sul sistema, non modifica nient'altro fuori dalla cartella indicata in $SecretKeyFile piu' sotto e dalla configurazione di Docker stesso (volume + container). Compatibile con PowerShell 5.1 (quello di serie su Windows 10/11): non usa funzionalita' di PowerShell 7. Rilanciarlo in un secondo momento AGGIORNA il pannello all'ultima immagine senza perdere i dati (e' pensato per essere rilanciato quante volte serve). .PARAMETER Token Facoltativo: token GitHub (permesso "read:packages") da usare se l'immagine e' privata, in alternativa alla variabile d'ambiente INBOX_GHCR_TOKEN o alla richiesta interattiva. Attenzione: se passato come parametro sulla riga di comando puo' restare nella cronologia di PowerShell; preferire la variabile d'ambiente o la richiesta interattiva quando possibile. .EXAMPLE Uso tipico (ricevuto per mail), da PowerShell: iwr -useb | iex #> [CmdletBinding()] param( [string]$Token ) # NB: niente "$ErrorActionPreference = 'Stop'" globale di proposito. Con lo # stderr dei comandi Docker catturato piu' sotto (via "*> $null" / "2>$null") # un EAP globale a 'Stop' trasformerebbe un normale "Docker non e' avviato" # in un'eccezione PowerShell non gestita invece che nel messaggio in # italiano gestito da Test-DockerReady. Restiamo quindi sul default # 'Continue' e usiamo "-ErrorAction Stop" solo sui singoli comandi # PowerShell dove un fallimento silenzioso lascerebbe lo stato inconsistente # (vedi Resolve-SecretKey), avvolti dal try/catch di sicurezza in fondo al # file. # ============================================================================= # CONFIGURAZIONE — unico punto da cambiare # ============================================================================= # # $ImageIsPrivate: # $true = il pacchetto Docker su GHCR e' privato. Lo script prova # comunque prima un download anonimo (potrebbe funzionare se nel # frattempo e' stato reso pubblico); se fallisce, chiede un token # GitHub con permesso "read:packages" per autenticarsi. # $false = il pacchetto e' pubblico: download diretto, mai chiesta nessuna # credenziale. Cambiare qui quando il repository/pacchetto GHCR # viene reso pubblico, e non serve toccare nient'altro nello # script. $ImageIsPrivate = $false # Immagine da usare. Sovrascrivibile con la variabile d'ambiente INBOX_IMAGE, # utile sia per testare un'immagine locale sia in produzione per puntare a # un tag specifico invece di "latest". $Image = if ($env:INBOX_IMAGE) { $env:INBOX_IMAGE } else { "ghcr.io/evoseed/optiwize-onboarding-inbox:latest" } $ContainerName = "optiwize-onboarding-inbox" $VolumeName = "optiwize_inbox_data" $ContainerPort = 8080 # porta interna del pannello nel container (fissa) $HostPortStart = 8099 # prima porta candidata sul PC dell'utente $HostPortScanTries = 20 # quante porte successive provare se occupata # Cartella/file in cui viene conservata la INBOX_SECRET_KEY generata al # primo avvio. Deve restare stabile fra un riavvio e l'altro: cambiarla # rende illeggibili le password dei device gia' salvate nel database. $SecretKeyDir = if ($env:LOCALAPPDATA) { Join-Path $env:LOCALAPPDATA "optiwize-onboarding-inbox" } else { Join-Path $env:USERPROFILE ".optiwize-onboarding-inbox" } $SecretKeyFile = Join-Path $SecretKeyDir "secret.key" # ============================================================================= # Utilita' di stampa # ============================================================================= function Write-Info { param([string]$Message) Write-Host $Message } function Write-Warn { param([string]$Message) Write-Host "[ATTENZIONE] $Message" -ForegroundColor Yellow } function Write-Step { param([string]$Message) Write-Host "`n==> $Message" -ForegroundColor Cyan } function Fail { param([string]$Message) Write-Host "`n[ERRORE] $Message" -ForegroundColor Red exit 1 } # ============================================================================= # 1. Docker installato e acceso # ============================================================================= function Show-DockerInstallHint { @" Docker Desktop non risulta installato su questo PC. Cosa fare: 1. Scaricalo da: https://www.docker.com/products/docker-desktop/ 2. Installalo (l'installer di Windows potrebbe chiedere un riavvio). 3. Aprilo almeno una volta (Docker Desktop deve girare in background, lo trovi nell'area di notifica accanto all'orologio). 4. Rilancia questo comando. "@ | Write-Host } function Show-DockerNotRunningHint { @" Docker Desktop e' installato ma non e' avviato (e' la causa piu' comune di questo errore: Docker Desktop non parte da solo all'accensione del PC). Cosa fare: 1. Apri Docker Desktop dal menu Start (cerca "Docker Desktop"). 2. Aspetta che l'icona nell'area di notifica indichi che e' avviato (passandoci sopra il mouse compare "Docker Desktop is running"). 3. Rilancia questo comando. Se Docker Desktop segnala un problema con WSL2, apri PowerShell come amministratore ed esegui: wsl --update "@ | Write-Host } function Test-DockerReady { Write-Step "Verifico Docker" $dockerCmd = Get-Command docker -ErrorAction SilentlyContinue if (-not $dockerCmd) { Show-DockerInstallHint exit 1 } docker info *> $null if ($LASTEXITCODE -ne 0) { Show-DockerNotRunningHint exit 1 } Write-Info "Docker c'e' ed e' in esecuzione." } # ============================================================================= # 2. Pull dell'immagine (con eventuale login GHCR per immagine privata) # ============================================================================= function Test-ImageExistsLocally { docker image inspect $Image *> $null return ($LASTEXITCODE -eq 0) } function Invoke-GhcrLogin { Write-Info "L'immagine e' privata: serve autenticarsi a ghcr.io con un token GitHub." Write-Info "Serve un Personal Access Token con permesso `"read:packages`" (nessun altro permesso)." Write-Info "Si crea da: https://github.com/settings/tokens" Write-Info "Il token NON verra' salvato su disco ne' mostrato a schermo." $user = $env:INBOX_GHCR_USER $plainToken = $env:INBOX_GHCR_TOKEN if (-not $plainToken -and $Token) { # Passato come parametro sulla riga di comando: vedi avviso nella # guida in cima al file sul rischio di finire in cronologia. $plainToken = $Token } if (-not $user) { $user = Read-Host "Il tuo username GitHub" } if (-not $plainToken) { $secureToken = Read-Host "Token GitHub (read:packages, non verra' mostrato)" -AsSecureString $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureToken) try { $plainToken = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) } finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } } if (-not $user) { Fail "Username GitHub vuoto: impossibile continuare." } if (-not $plainToken) { Fail "Token vuoto: impossibile continuare." } $plainToken | docker login ghcr.io -u $user --password-stdin *> $null if ($LASTEXITCODE -ne 0) { Fail "Login a ghcr.io non riuscito. Controlla username e token (permesso read:packages, non scaduto)." } Write-Info "Login a ghcr.io riuscito." } function Get-InboxImage { Write-Step "Scarico l'immagine del pannello ($Image)" # L'output serve per distinguere "non autorizzato" (immagine non accessibile) # da un guasto di rete: mandare l'utente a controllare la connessione quando # il registry ha risposto "unauthorized" lo fa cercare nel posto sbagliato. $esito = docker pull $Image 2>&1 $esito | ForEach-Object { Write-Host $_ } if ($LASTEXITCODE -eq 0) { return } $testoEsito = ($esito | Out-String) $nonAutorizzato = $testoEsito -match 'unauthorized|denied|authentication required|forbidden' if ($ImageIsPrivate) { Write-Warn "Il download diretto non ha funzionato: molto probabilmente perche' l'immagine e' privata." Invoke-GhcrLogin Write-Info "Riprovo il download..." docker pull $Image if ($LASTEXITCODE -eq 0) { return } if (Test-ImageExistsLocally) { Write-Warn "Il download non e' riuscito nemmeno dopo il login, ma l'immagine '$Image' e' gia' presente su questo computer da un'esecuzione precedente: la uso cosi' com'e' (potrebbe non essere l'ultima versione)." return } Fail "Il download dell'immagine non e' riuscito nemmeno dopo il login. Verifica che il token abbia il permesso 'read:packages' e non sia scaduto, e che l'immagine '$Image' sia corretta." } if (Test-ImageExistsLocally) { Write-Warn "Il download non e' riuscito, ma l'immagine '$Image' e' gia' presente su questo computer: la uso cosi' com'e' (potrebbe non essere l'ultima versione)." return } if ($nonAutorizzato) { Fail @" Il registry ha risposto 'non autorizzato' per l'immagine '$Image'. La connessione a Internet funziona: il problema e' che l'immagine non risulta pubblicamente accessibile. Non e' qualcosa che puoi risolvere da qui: segnala a chi ti ha inviato queste istruzioni che il pacchetto va reso pubblico, oppure chiedi le credenziali per scaricarlo. "@ } Fail "Il download dell'immagine '$Image' non e' riuscito. Controlla la connessione a Internet e riprova. Se il problema persiste, contatta chi ti ha inviato queste istruzioni." } # ============================================================================= # 3. Chiave segreta: genera la prima volta, riusa sempre dopo # ============================================================================= # Ordine di priorita' (il punto piu' delicato di questo script): # 1. file locale gia' presente da un'esecuzione precedente -> riusa quella; # 2. nessun file ma un container con lo stesso nome esiste gia' -> recupera # la chiave dal container esistente invece di generarne una nuova (che # renderebbe illeggibili i dati gia' salvati) e la salva nel file; # 3. nessuno dei due -> genera una chiave nuova e la salva nel file. function Resolve-SecretKey { Write-Step "Preparo la chiave di cifratura (INBOX_SECRET_KEY)" if (Test-Path $SecretKeyFile) { $existingFromFile = (Get-Content -Raw $SecretKeyFile -ErrorAction Stop).Trim() if ($existingFromFile) { Write-Info "Trovata una chiave gia' generata in precedenza, la riuso." return $existingFromFile } } docker inspect $ContainerName *> $null if ($LASTEXITCODE -eq 0) { $envLines = docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' $ContainerName $line = $envLines | Where-Object { $_ -like "INBOX_SECRET_KEY=*" } | Select-Object -First 1 if ($line) { $existingFromContainer = $line.Substring("INBOX_SECRET_KEY=".Length) if ($existingFromContainer) { Write-Warn "Il file con la chiave non c'era piu', ma ho trovato un container gia' avviato in precedenza: ne riuso la chiave invece di generarne una nuova (altrimenti i dati gia' salvati diventerebbero illeggibili)." New-Item -ItemType Directory -Force -Path $SecretKeyDir -ErrorAction Stop | Out-Null Set-Content -Path $SecretKeyFile -Value $existingFromContainer -NoNewline -ErrorAction Stop return $existingFromContainer } } } Write-Info "Prima installazione: genero una nuova chiave casuale e la conservo per i prossimi avvii." New-Item -ItemType Directory -Force -Path $SecretKeyDir -ErrorAction Stop | Out-Null $bytes = New-Object byte[] 48 $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() try { $rng.GetBytes($bytes) } finally { $rng.Dispose() } $newKey = [Convert]::ToBase64String($bytes) Set-Content -Path $SecretKeyFile -Value $newKey -NoNewline -ErrorAction Stop Write-Info "Chiave salvata in: $SecretKeyFile (non cancellarla: serve a rileggere i dati gia' salvati)." return $newKey } # ============================================================================= # 4. Porta libera # ============================================================================= function Test-PortInUse { param([int]$Port) $result = Test-NetConnection -ComputerName "127.0.0.1" -Port $Port -InformationLevel Quiet -WarningAction SilentlyContinue return [bool]$result } function Find-HostPort { param([int]$Preferred) $port = $Preferred for ($i = 0; $i -lt $HostPortScanTries; $i++) { if (-not (Test-PortInUse -Port $port)) { return $port } $port++ } Fail "Non ho trovato una porta libera tra $Preferred e $($Preferred + $HostPortScanTries - 1). Libera una porta o modifica `$HostPortStart nello script." } # ============================================================================= # 5. Avvio (gestisce anche l'aggiornamento se il container esiste gia') # ============================================================================= function Open-Browser { param([string]$Url) try { Start-Process $Url | Out-Null } catch { Write-Warn "Non sono riuscito ad aprire il browser automaticamente. Apri manualmente: $Url" } } function Wait-PanelReady { param([int]$Port) Write-Step "Attendo che il pannello risponda" for ($i = 0; $i -lt 30; $i++) { if (Test-PortInUse -Port $Port) { Write-Info "Il pannello risponde." return } Start-Sleep -Seconds 1 } Write-Warn "Il container e' partito ma il pannello non risponde ancora sulla porta $Port. Controlla i log con: docker logs $ContainerName" } function Main { Test-DockerReady $existingContainer = $false $preferredPort = $HostPortStart docker inspect $ContainerName *> $null if ($LASTEXITCODE -eq 0) { $existingContainer = $true # Se il container e' fermo, "docker port" non restituisce nulla: in # quel caso si ripiega semplicemente su $HostPortStart piu' sotto. $portOutput = docker port $ContainerName "$ContainerPort/tcp" 2>$null if ($LASTEXITCODE -eq 0 -and $portOutput) { $firstLine = ($portOutput | Select-Object -First 1) if ($firstLine -match ':(\d+)$') { $preferredPort = [int]$Matches[1] } } } $secretKey = Resolve-SecretKey Get-InboxImage Write-Step "Preparo il volume dati" docker volume create $VolumeName | Out-Null if ($LASTEXITCODE -ne 0) { Fail "Non sono riuscito a creare il volume dati Docker '$VolumeName'. Controlla i messaggi sopra." } Write-Info "Volume pronto: $VolumeName" # Il vecchio container va rimosso PRIMA di scegliere la porta: se non lo # si facesse, la sua stessa porta risulterebbe "occupata" (e' ancora lui # ad ascoltarci) e a ogni riavvio dello script la porta salirebbe di uno # invece di restare stabile. Il volume dati non e' toccato da questa # rimozione: vive separatamente dal container. if ($existingContainer) { Write-Step "Aggiorno il container esistente (il volume dati non viene toccato)" docker rm -f $ContainerName | Out-Null } else { Write-Step "Avvio il container" } $hostPort = Find-HostPort -Preferred $preferredPort if ($hostPort -ne $preferredPort) { Write-Warn "La porta $preferredPort risultava occupata: uso la porta $hostPort al suo posto." } docker run -d ` --name $ContainerName ` --restart unless-stopped ` -p "${hostPort}:${ContainerPort}" ` -e "INBOX_SECRET_KEY=$secretKey" ` -v "${VolumeName}:/data" ` $Image | Out-Null if ($LASTEXITCODE -ne 0) { Fail "Non sono riuscito ad avviare il container. Controlla i messaggi sopra (es. 'docker logs $ContainerName' se il container risulta creato ma fermo)." } $url = "http://localhost:$hostPort" Wait-PanelReady -Port $hostPort Open-Browser -Url $url Write-Host "" Write-Host "============================================================" Write-Host " OptiWize Onboarding Inbox e' pronto" Write-Host "============================================================" Write-Host " Pannello: $url" Write-Host " Fermarlo: docker stop $ContainerName" Write-Host " Riavviarlo: docker start $ContainerName" Write-Host " Aggiornarlo: rilancia questo stesso comando" Write-Host "============================================================" } try { Main } catch { # Rete di sicurezza per errori imprevisti (es. .NET) non gia' gestiti # con un messaggio dedicato piu' sopra: meglio un messaggio leggibile # che una traccia PowerShell grezza. Write-Host "" Write-Host "[ERRORE] Si e' verificato un problema imprevisto: $($_.Exception.Message)" -ForegroundColor Red Write-Host "Se il problema persiste, contatta chi ti ha inviato queste istruzioni." -ForegroundColor Red exit 1 }