# ============================================================================= # PragmaCloud — Instalacijska skripta za lokalni deployment (Windows) # Zahtjeva: Docker Desktop za Windows (pokrenut), PowerShell 5.1+ # Upotreba (interaktivno, pita za sve): .\install.ps1 # Upotreba (pre-filled, npr. iz SuperAdmin "Installer" dugmeta — # vidi /superadmin/lokalne-licence): .\install.ps1 -LicenseKey "PRAGMA-..." # -RegistryUser "pragma" -RegistryPassword "..." -InstallDir "C:\pragma" -Port "8080" # -Backend "sqlite"|"mysql"|"postgres" (opcionalno, zadano "sqlite" ako izostavljeno) # ============================================================================= param( [string]$LicenseKey = "", [string]$RegistryUser = "", [string]$RegistryPassword = "", [string]$InstallDir = "", [string]$Port = "", [string]$Backend = "" # "sqlite" (zadano) ili "mysql"/"postgres" ) $ErrorActionPreference = "Stop" # ── Konstante ───────────────────────────────────────────────────────────────── $RegistryHost = "registry.pragma.ba" # Javni download s produkcijskog servera (GitHub repo je privatan — raw URL vraca 404) $ComposeUrl = "https://app.pragma.ba/docker-compose.standalone.yml" function Info($msg) { Write-Host "-> $msg" -ForegroundColor Cyan } function Success($msg) { Write-Host "OK $msg" -ForegroundColor Green } function Warn($msg) { Write-Host "! $msg" -ForegroundColor Yellow } function Fail($msg) { Write-Host "X $msg" -ForegroundColor Red; exit 1 } function Header($msg) { Write-Host ""; Write-Host $msg -ForegroundColor Blue; Write-Host ("-" * 42) } Header "PragmaCloud Instalacija (Windows)" Write-Host "Ova skripta instalira PragmaCloud lokalno koristeci Docker Desktop." Write-Host "" # ── Provjera Docker Desktop-a ──────────────────────────────────────────────── Header "1. Docker Desktop" try { docker version | Out-Null Success "Docker Desktop je pokrenut." } catch { Fail "Docker Desktop nije pokrenut ili nije instaliran. Instalirajte/pokrenite Docker Desktop pa pokrenite skriptu ponovo." } # ── Direktorij instalacije ──────────────────────────────────────────────────── Header "2. Direktoriji" $defaultDir = "C:\pragma" if ([string]::IsNullOrWhiteSpace($InstallDir)) { $installInput = Read-Host "Direktorij instalacije [$defaultDir]" $AppDir = if ([string]::IsNullOrWhiteSpace($installInput)) { $defaultDir } else { $installInput } } else { $AppDir = $InstallDir Info "Direktorij instalacije: $AppDir" } $DataDir = Join-Path $AppDir "tabele" $AuthDir = Join-Path $AppDir "watchtower-auth" New-Item -ItemType Directory -Force -Path $AppDir | Out-Null New-Item -ItemType Directory -Force -Path $DataDir | Out-Null New-Item -ItemType Directory -Force -Path $AuthDir | Out-Null Success "Kreiran: $DataDir" # ── Konfiguracija (.env) ────────────────────────────────────────────────────── Header "3. Konfiguracija" $EnvFile = Join-Path $AppDir ".env" if (Test-Path $EnvFile) { $overwrite = Read-Host ".env vec postoji. Prepisati ga? [y/N]" if ($overwrite -notmatch '^[yY]') { Info "Konfiguracija zadrzana — preskacem generisanje .env." } else { Remove-Item $EnvFile } } if (-not (Test-Path $EnvFile)) { if ([string]::IsNullOrWhiteSpace($Port)) { $portInput = Read-Host "Port na koji PragmaCloud treba slusati [8080]" $AppPort = if ([string]::IsNullOrWhiteSpace($portInput)) { "8080" } else { $portInput } } else { $AppPort = $Port Info "Port: $AppPort" } # Kljuc je OBAVEZAN — nepostojeci kljuc (npr. "TRIAL") server odbija (Valid=false) # i aplikacija se odmah blokira (LicenseExpired). Kljuc se kreira na # app.pragma.ba -> /superadmin/lokalne-licence. while ([string]::IsNullOrWhiteSpace($LicenseKey)) { $LicenseKey = Read-Host "Unesite licencni kljuc (obavezno; dobijate od Pragma tima)" if ([string]::IsNullOrWhiteSpace($LicenseKey)) { Warn "Licencni kljuc je obavezan — bez validnog kljuca aplikacija ce biti blokirana." } } # Dijeljeni RNG za Watchtower token i (opcionalno) MySQL/PostgreSQL lozinke ispod. $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() # Backend baze podataka — SQLite (zadano), MySQL ili PostgreSQL (lokalni kontejner # na ovoj masini, isti obrazac kao produkcijski docker-compose.yml). Wizard "Create # new database" (SuperAdmin -> New workspace) radi identicno produkciji jer # MySqlTenantProvisioner/PostgresTenantProvisioner ocekuju servis imenovan tacno # "mysql"/"postgres". if ([string]::IsNullOrWhiteSpace($Backend)) { Write-Host "" Write-Host " Backend baze podataka:" Write-Host " [1] SQLite (zadano) - fajl-bazirano, najjednostavnije" Write-Host " [2] MySQL (lokalni kontejner) - pokrece dodatni MySQL servis na ovoj masini" Write-Host " [3] PostgreSQL (lokalni kontejner) - pokrece dodatni PostgreSQL servis na ovoj masini" $backendInput = Read-Host "Izbor [1]" $Backend = if ($backendInput -eq "2") { "mysql" } elseif ($backendInput -eq "3") { "postgres" } else { "sqlite" } } $BackendEnvBlock = "" if ($Backend -eq "mysql") { Info "MySQL backend izabran — generisem kredencijale i pripremam init skriptu..." $mysqlInitDir = Join-Path $AppDir "mysql-init" New-Item -ItemType Directory -Force -Path $mysqlInitDir | Out-Null $rootBytes = New-Object byte[] 24 $rng.GetBytes($rootBytes) $MySqlRootPassword = ($rootBytes | ForEach-Object { $_.ToString("x2") }) -join "" $appBytes = New-Object byte[] 24 $rng.GetBytes($appBytes) $MySqlAppPassword = ($appBytes | ForEach-Object { $_.ToString("x2") }) -join "" try { Invoke-WebRequest -Uri "https://app.pragma.ba/mysql-init/01-grant-pragma-app.sql" ` -OutFile (Join-Path $mysqlInitDir "01-grant-pragma-app.sql") -UseBasicParsing } catch { Warn "Preuzimanje mysql-init skripte nije uspjelo — MySQL grant ce se morati rucno postaviti." } $BackendEnvBlock = @" # MySQL backend — lokalni kontejner (aktivira 'mysql' servis u docker-compose.yml) COMPOSE_PROFILES=mysql MYSQL_ROOT_PASSWORD=$MySqlRootPassword MYSQL_APP_PASSWORD=$MySqlAppPassword "@ Success "MySQL kredencijali generisani." } elseif ($Backend -eq "postgres") { Info "PostgreSQL backend izabran — generisem kredencijale i pripremam init skriptu..." $pgInitDir = Join-Path $AppDir "pg-init" New-Item -ItemType Directory -Force -Path $pgInitDir | Out-Null $rootBytes = New-Object byte[] 24 $rng.GetBytes($rootBytes) $PostgresRootPassword = ($rootBytes | ForEach-Object { $_.ToString("x2") }) -join "" $appBytes = New-Object byte[] 24 $rng.GetBytes($appBytes) $PostgresAppPassword = ($appBytes | ForEach-Object { $_.ToString("x2") }) -join "" try { Invoke-WebRequest -Uri "https://app.pragma.ba/pg-init/01-grant-pragma-app.sh" ` -OutFile (Join-Path $pgInitDir "01-grant-pragma-app.sh") -UseBasicParsing } catch { Warn "Preuzimanje pg-init skripte nije uspjelo — PostgreSQL grant ce se morati rucno postaviti." } $BackendEnvBlock = @" # PostgreSQL backend — lokalni kontejner (aktivira 'postgres' servis u docker-compose.yml) COMPOSE_PROFILES=postgres POSTGRES_ROOT_PASSWORD=$PostgresRootPassword POSTGRES_APP_PASSWORD=$PostgresAppPassword "@ Success "PostgreSQL kredencijali generisani." } else { Info "SQLite backend (zadano)." } # Watchtower HTTP API token — omogucava dugme "Azuriraj sada" u aplikaciji $bytes = New-Object byte[] 24 $rng.GetBytes($bytes) $WatchtowerToken = ($bytes | ForEach-Object { $_.ToString("x2") }) -join "" $DataDirDocker = $DataDir -replace '\\', '/' @" # PragmaCloud — lokalna konfiguracija (Windows) # Kreirano: $(Get-Date -Format "yyyy-MM-dd HH:mm") # Port na hostu APP_PORT=$AppPort # Folder s SQLite bazama (Docker Desktop prihvata Windows putanju u compose fajlu) SQLITE_DIR=$DataDirDocker # Licencni kljuc PRAGMA_LICENSE_KEY=$LicenseKey # Watchtower HTTP API token — omogucava dugme "Azuriraj sada" u aplikaciji WATCHTOWER_HTTP_API_TOKEN=$WatchtowerToken $BackendEnvBlock "@ | Out-File -FilePath $EnvFile -Encoding utf8 Success ".env kreiran: $EnvFile" Success "Watchtower HTTP API token generisan (omogucava 'Azuriraj sada' u aplikaciji)." } # ── Preuzimanje docker-compose.standalone.yml ──────────────────────────────── Header "4. Docker Compose" $ComposeFile = Join-Path $AppDir "docker-compose.yml" if (-not (Test-Path $ComposeFile)) { Info "Preuzimanje docker-compose.standalone.yml..." try { Invoke-WebRequest -Uri $ComposeUrl -OutFile $ComposeFile -UseBasicParsing Success "docker-compose.yml kreiran: $ComposeFile" } catch { Fail "Preuzimanje nije uspjelo. Preuzmite rucno docker-compose.standalone.yml u $ComposeFile" } } else { Success "docker-compose.yml vec postoji." } # ── Prijava na privatni Docker registry ────────────────────────────────────── Header "5. Docker registry (registry.pragma.ba)" Info "Prijava na privatni Pragma registry..." if (-not [string]::IsNullOrWhiteSpace($RegistryPassword)) { $RegUser = if ([string]::IsNullOrWhiteSpace($RegistryUser)) { "pragma" } else { $RegistryUser } $RegPass = $RegistryPassword Info "Registry kredencijali prosljedjeni (korisnik: $RegUser)." } else { Write-Host "" Write-Host " Unesite credentials za Pragma privatni Docker registry." Write-Host " Dobijate ih od Pragma tima zajedno s licencnim kljucem." Write-Host "" $regUserInput = Read-Host "Korisnicko ime [pragma]" $RegUser = if ([string]::IsNullOrWhiteSpace($regUserInput)) { "pragma" } else { $regUserInput } $RegPassSecure = Read-Host "Lozinka" -AsSecureString $RegPass = [System.Net.NetworkCredential]::new('', $RegPassSecure).Password } $AuthConfigFile = Join-Path $AuthDir "config.json" if (-not [string]::IsNullOrWhiteSpace($RegPass)) { # docker login na Windows koristi Docker Desktop-ov credential store (Windows Credential # Manager) za SVOJ pull — to je OK za pocetni "docker compose pull" ispod. ALI Watchtower # (poseban kontejner, bez pristupa Windows Credential Manageru) treba SVOJ, samostalan # config.json sa plain base64 auth-om — zato ga generisemo direktno, nezavisno od # "docker login" komande ispod. # NAPOMENA: "--password-stdin" (i cjevovodom i direktnim pisanjem u stdin) dosljedno je # padao s 401 na Docker Desktop + WSL2 backend kombinaciji (potvrdjeno na testu 2026-07-06, # lozinka bajt-za-bajt provjerena ispravna) — izgleda kao poznati bug u toj konfiguraciji. # Fallback: lozinka kao direktan CLI argument (Docker upozorava da je manje sigurno od # --password-stdin, ali skripta se pokrece jednokratno lokalno na klijentovoj masini). $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = "docker" $psi.Arguments = "login `"$RegistryHost`" -u `"$RegUser`" --password `"$RegPass`"" $psi.UseShellExecute = $false $loginProc = [System.Diagnostics.Process]::Start($psi) $loginProc.WaitForExit() if ($loginProc.ExitCode -ne 0) { Fail "Prijava na $RegistryHost nije uspjela. Provjerite kredencijale." } Success "Prijavljen na $RegistryHost (za pocetni pull)." $authString = "$($RegUser):$($RegPass)" $authBytes = [System.Text.Encoding]::UTF8.GetBytes($authString) $authB64 = [Convert]::ToBase64String($authBytes) $configJson = @" { "auths": { "$RegistryHost": { "auth": "$authB64" } } } "@ $configJson | Out-File -FilePath $AuthConfigFile -Encoding ascii Success "Watchtower registry credentials sacuvani ($AuthConfigFile)." } else { Warn "Lozinka nije unesena — Watchtower auto-update nece raditi za privatni registry." '{"auths":{}}' | Out-File -FilePath $AuthConfigFile -Encoding ascii } # ── Pokretanje ──────────────────────────────────────────────────────────────── Header "6. Pokretanje aplikacije" Push-Location $AppDir try { Info "Povlacenje Docker image-a (moze potrajati pri prvom pokretanju)..." docker compose pull if ($LASTEXITCODE -ne 0) { Fail "docker compose pull nije uspio." } Info "Pokretanje kontejnera..." docker compose up -d if ($LASTEXITCODE -ne 0) { Fail "docker compose up nije uspio." } Info "Cekanje da se aplikacija pokrene..." $timeout = 60; $elapsed = 0; $status = "starting" while ($elapsed -lt $timeout) { $status = docker inspect --format='{{.State.Health.Status}}' pragmacloud 2>$null if ($status -eq "healthy") { break } Start-Sleep -Seconds 3 $elapsed += 3 Write-Host "." -NoNewline } Write-Host "" if ($status -eq "healthy") { Success "PragmaCloud je pokrenut i zdrav!" } else { Warn "Aplikacija mozda jos nije spremna (status: $status). Provjerite: docker logs pragmacloud" } } finally { Pop-Location } # ── Finalni izvjestaj ───────────────────────────────────────────────────────── $envContent = Get-Content $EnvFile -Raw $portMatch = [regex]::Match($envContent, 'APP_PORT=(\d+)') $Port = if ($portMatch.Success) { $portMatch.Groups[1].Value } else { "8080" } Write-Host "" Write-Host "================================================" -ForegroundColor Green Write-Host " PragmaCloud je uspjesno instaliran!" -ForegroundColor Green Write-Host "================================================" -ForegroundColor Green Write-Host "" Write-Host " Lokalni pristup: http://localhost:$Port" Write-Host " Baze podataka: $DataDir" Write-Host " Konfiguracija: $EnvFile" Write-Host "" if ($envContent -match '(?m)^MYSQL_ROOT_PASSWORD=') { Write-Host " MySQL backend: aktivan (lokalni kontejner, kredencijali u $EnvFile)" Write-Host " Nove firme na MySQL-u: SuperAdmin -> New workspace -> 'Create new database'" Write-Host "" } if ($envContent -match '(?m)^POSTGRES_ROOT_PASSWORD=') { Write-Host " PostgreSQL backend: aktivan (lokalni kontejner, kredencijali u $EnvFile)" Write-Host " Nove firme na PostgreSQL-u: SuperAdmin -> New workspace -> 'Create new database'" Write-Host "" } Write-Host " Korisne komande:" Write-Host " docker logs pragmacloud # logovi aplikacije" Write-Host " docker compose -f `"$ComposeFile`" ps # status" Write-Host " docker compose -f `"$ComposeFile`" up -d # restart" Write-Host "" Write-Host " Backup: Redovno kopirajte $DataDir na sigurnu lokaciju!" -ForegroundColor Yellow Write-Host ""