param( [ValidateSet("stable", "beta", "internal")] [string]$Channel = "stable", [string]$ProductId = "", [string]$TenantId = "", [string]$LicenseKey = "", [string]$LicenseJwt = "", [string]$DeploymentProfileUrl = "", [ValidateSet("manual", "notify_only", "auto_install", "scheduled_install", "internal_only")] [string]$UpdatePolicy = "notify_only", [switch]$Quiet, [switch]$CheckOnly ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $InstallRoot = "C:\ProgramData\AtiqSoft" $LogRoot = "C:\ProgramData\AtiqSoft\InstallLogs" $DownloadRoot = Join-Path $InstallRoot "Downloads" $StateRoot = Join-Path $InstallRoot "Installed" $AgentRoot = Join-Path $InstallRoot "Agent" $ApiRoot = "https://install.atiqsoft.com/api/packages" $LicenseApiRoot = "https://install.atiqsoft.com/api/license" $LicenseActivationUrl = "https://install.atiqsoft.com/api/license/activate" $LicenseValidationUrl = "https://install.atiqsoft.com/api/license/validate" $Products = @( @{ Menu = "1"; Id = "iqai"; Name = "IQAI Desktop" }, @{ Menu = "2"; Id = "aios"; Name = "AIOS Desktop" }, @{ Menu = "3"; Id = "iqinventory"; Name = "IQInventory Agent" }, @{ Menu = "4"; Id = "iqnetwork"; Name = "IQNetwork Agent" }, @{ Menu = "5"; Id = "iqpentest"; Name = "IQPenTest Agent" }, @{ Menu = "6"; Id = "iqpharmacy"; Name = "IQPharmacy Desktop" }, @{ Menu = "7"; Id = "iqbusiness"; Name = "IQBusiness Desktop" }, @{ Menu = "8"; Id = "deviceagent"; Name = "Device Agent" } ) function Write-Banner { Clear-Host Write-Host "" Write-Host "============================================================" -ForegroundColor Cyan Write-Host " AtiqSoft Install Center " -ForegroundColor Cyan Write-Host " Install, update, repair, and register apps " -ForegroundColor Cyan Write-Host "============================================================" -ForegroundColor Cyan Write-Host "" } function Initialize-InstallFolders { New-Item -ItemType Directory -Path $LogRoot -Force | Out-Null New-Item -ItemType Directory -Path $DownloadRoot -Force | Out-Null New-Item -ItemType Directory -Path $StateRoot -Force | Out-Null New-Item -ItemType Directory -Path $AgentRoot -Force | Out-Null } function Write-InstallLog { param( [Parameter(Mandatory = $true)][string]$Message, [string]$Level = "INFO" ) $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $logFile = Join-Path $LogRoot ("install-{0}.log" -f (Get-Date -Format "yyyyMMdd")) Add-Content -Path $logFile -Value "[$timestamp] [$Level] $Message" } function Test-Windows { if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { throw "AtiqSoft Install Center supports Windows only." } } function Test-Administrator { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]::new($identity) return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } function Show-Menu { Write-Host "[1] IQAI Desktop" Write-Host "[2] AIOS Desktop" Write-Host "[3] IQInventory Agent" Write-Host "[4] IQNetwork Agent" Write-Host "[5] IQPenTest Agent" Write-Host "[6] IQPharmacy Desktop" Write-Host "[7] IQBusiness Desktop" Write-Host "[8] Device Agent" Write-Host "[9] Repair Existing Installation" Write-Host "[10] Uninstall AtiqSoft App" Write-Host "[0] Exit" Write-Host "" } function Select-Channel { Write-Host "Release channel:" Write-Host "[1] Stable" Write-Host "[2] Beta" Write-Host "[3] Internal" $choice = Read-Host "Select channel or press Enter for stable" switch ($choice) { "2" { return "beta" } "3" { return "internal" } default { return "stable" } } } function Select-Product { param([string]$Prompt = "Select AtiqSoft product number") Show-Menu $choice = Read-Host $Prompt return $choice } function Get-ProductByMenu { param([Parameter(Mandatory = $true)][string]$Menu) return $Products | Where-Object { $_.Menu -eq $Menu } | Select-Object -First 1 } function Get-ProductById { param([Parameter(Mandatory = $true)][string]$Id) return $Products | Where-Object { $_.Id -eq $Id } | Select-Object -First 1 } function Get-StatePath { param([Parameter(Mandatory = $true)][string]$Id) return Join-Path $StateRoot ("{0}.json" -f $Id) } function Get-DeviceIdentity { $machineGuidPath = "HKLM:\SOFTWARE\Microsoft\Cryptography" try { $machineGuid = (Get-ItemProperty -Path $machineGuidPath -Name MachineGuid).MachineGuid return "win-$machineGuid" } catch { return "win-$env:COMPUTERNAME" } } function Get-InstalledVersion { param([Parameter(Mandatory = $true)][string]$Id) $statePath = Get-StatePath -Id $Id if (-not (Test-Path -LiteralPath $statePath)) { return "" } try { $state = Get-Content -LiteralPath $statePath -Raw | ConvertFrom-Json return [string]$state.version } catch { Write-InstallLog "Could not read installed version for ${Id}: $($_.Exception.Message)" "WARN" return "" } } function Save-InstalledVersion { param( [Parameter(Mandatory = $true)][string]$Id, [Parameter(Mandatory = $true)]$Package ) $state = [ordered]@{ productId = $Id version = $Package.version channel = $Package.channel installedAt = (Get-Date).ToString("o") downloadUrl = $Package.downloadUrl } $state | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Get-StatePath -Id $Id) } function Get-LatestPackage { param( [Parameter(Mandatory = $true)][string]$Id, [Parameter(Mandatory = $true)][string]$SelectedChannel ) $uri = "$ApiRoot/$Id/latest?channel=$SelectedChannel" Write-InstallLog "Checking latest package from $uri" $response = Invoke-RestMethod -Uri $uri -UseBasicParsing if ($null -ne $response.latestByChannel -and $null -ne $response.latestByChannel.$SelectedChannel) { return $response.latestByChannel.$SelectedChannel } if ($null -ne $response.latest) { return $response.latest } throw "Latest package metadata was not found for $Id on $SelectedChannel." } function Compare-VersionText { param( [string]$Installed, [string]$Latest ) if ([string]::IsNullOrWhiteSpace($Installed)) { return -1 } if ($Installed -eq $Latest) { return 0 } return -1 } function Test-PackageFile { param( [Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)]$Package ) if ($Package.fileSizeBytes -gt 0) { $actualSize = (Get-Item -LiteralPath $Path).Length if ($actualSize -ne [int64]$Package.fileSizeBytes) { throw "File size validation failed. Expected $($Package.fileSizeBytes) bytes but got $actualSize bytes." } } if ($Package.sha256 -like "PLACEHOLDER_*_SHA256") { Write-Host "SHA256 placeholder detected. Replace it before production rollout." -ForegroundColor Yellow Write-InstallLog "SHA256 placeholder used for $Path" "WARN" return $true } $actual = (Get-FileHash -Path $Path -Algorithm SHA256).Hash.ToUpperInvariant() $expected = ([string]$Package.sha256).ToUpperInvariant() if ($actual -ne $expected) { throw "SHA256 validation failed. Expected $expected but got $actual." } Write-InstallLog "Package validation passed for $Path" return $true } function Test-PackageAvailable { param([Parameter(Mandatory = $true)]$Package) try { $response = Invoke-WebRequest -Uri $Package.downloadUrl -Method Head -UseBasicParsing if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 400) { return $true } } catch { Write-InstallLog "Package not available at $($Package.downloadUrl): $($_.Exception.Message)" "ERROR" } Write-Host "" Write-Host "The installer package is not uploaded yet:" -ForegroundColor Yellow Write-Host $Package.downloadUrl Write-Host "Upload the approved AtiqSoft installer file to that package path, then run this command again." Write-InstallLog "Stopped because package is not uploaded: $($Package.downloadUrl)" "WARN" return $false } function Invoke-LicenseValidation { param( [Parameter(Mandatory = $true)]$Product, [string]$Key = "", [string]$Tenant = "", [string]$Jwt = "" ) $hasKey = -not [string]::IsNullOrWhiteSpace($Key) $hasTenant = -not [string]::IsNullOrWhiteSpace($Tenant) if (-not $hasKey -and -not $hasTenant) { Write-Host "No tenant id or license key entered. License validation skipped for trial/testing install." -ForegroundColor Yellow Write-InstallLog "License validation skipped for $($Product.Id): no tenant id or license key was entered" "WARN" return [pscustomobject]@{ valid = $false skipped = $true reason = "No tenant id or license key was entered." } } if (-not $hasKey) { throw "AtiqSoft license is invalid: license key is required when tenant id is provided." } if (-not $hasTenant) { throw "AtiqSoft license is invalid: tenant id is required when license key is provided." } $headers = @{} if (-not [string]::IsNullOrWhiteSpace($Jwt)) { $headers.Authorization = "Bearer $Jwt" } $body = @{ licenseKey = $Key tenantId = $Tenant productId = $Product.Id deviceId = Get-DeviceIdentity } try { Invoke-RestMethod -Uri $LicenseActivationUrl -Method Post -Headers $headers -Body ($body | ConvertTo-Json -Depth 6) -ContentType "application/json" -UseBasicParsing | Out-Null $response = Invoke-RestMethod -Uri $LicenseValidationUrl -Method Post -Headers $headers -Body ($body | ConvertTo-Json -Depth 6) -ContentType "application/json" -UseBasicParsing if ($response.valid -ne $true) { throw "License server rejected the request." } Write-InstallLog "License validation passed for $($Product.Id) tenant $Tenant" return $response } catch { Write-InstallLog "License validation failed for $($Product.Id): $($_.Exception.Message)" "ERROR" throw "AtiqSoft license is invalid. $($_.Exception.Message)" } } function Install-UpdateAgent { param( [Parameter(Mandatory = $true)]$Product, [string]$Tenant = "", [string]$Key = "", [Parameter(Mandatory = $true)]$Package ) Write-Host "Configure update policy: $UpdatePolicy" $agentUrl = "https://install.atiqsoft.com/agent/AtiqSoft.UpdateAgent.ps1" $agentPath = Join-Path $AgentRoot "AtiqSoft.UpdateAgent.ps1" $configPath = Join-Path $AgentRoot "update-agent.json" $deviceId = Get-DeviceIdentity Invoke-WebRequest -Uri $agentUrl -OutFile $agentPath -UseBasicParsing $config = [ordered]@{ serviceName = "AtiqSoft.UpdateAgent" tenantId = $Tenant deviceId = $deviceId productId = $Product.Id installedVersion = $Package.version policy = $UpdatePolicy apiBaseUrl = "https://install.atiqsoft.com" } $config | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $configPath Write-InstallLog "Update agent configured for $($Product.Id) policy $UpdatePolicy" try { Start-Service -Name "AtiqSoft.UpdateAgent" -ErrorAction Stop Write-InstallLog "AtiqSoft.UpdateAgent service started" } catch { Write-InstallLog "AtiqSoft.UpdateAgent service start skipped: $($_.Exception.Message)" "WARN" } } function Invoke-DeploymentProfileInstall { param([string]$ProfileUrl) if ([string]::IsNullOrWhiteSpace($ProfileUrl)) { return } $profile = Invoke-RestMethod -Uri $ProfileUrl -Method Get -UseBasicParsing Write-Host "Auto-register device for tenant deployment profile: $($profile.profile.defaultTenant)" Write-Host "Install only selected products from deployment profile." foreach ($productId in $profile.profile.productsToInstall) { Write-InstallLog "Deployment profile selected product $productId" } } function Invoke-AtiqSoftInstaller { param( [Parameter(Mandatory = $true)]$Product, [Parameter(Mandatory = $true)][string]$SelectedChannel, [string]$Mode = "Install" ) $package = Get-LatestPackage -Id $Product.Id -SelectedChannel $SelectedChannel $installedVersion = Get-InstalledVersion -Id $Product.Id Write-Host "" Write-Host "$Mode selected: $($Product.Name)" -ForegroundColor Cyan Write-Host "Channel: $SelectedChannel" Write-Host "Installed version: $(if ($installedVersion) { $installedVersion } else { 'none recorded' })" Write-Host "Latest version: $($package.version)" Write-Host "Source: $($package.downloadUrl)" Write-Host "" if ($Mode -eq "Install" -and (Compare-VersionText -Installed $installedVersion -Latest $package.version) -eq 0) { Write-Host "Latest version is already recorded locally." -ForegroundColor Green Write-InstallLog "$($Product.Id) already at latest version $($package.version)" if (-not $Quiet) { $continue = Read-Host "Reinstall anyway? Type YES to continue" if ($continue -ne "YES") { return } } } if ($CheckOnly) { Write-InstallLog "Check-only completed for $($Product.Id). Latest version $($package.version)" return } $effectiveLicenseKey = $LicenseKey $effectiveTenantId = $TenantId if (-not $Quiet) { if ([string]::IsNullOrWhiteSpace($effectiveTenantId)) { $effectiveTenantId = Read-Host "Enter AtiqSoft tenant id or press Enter to skip" } if ([string]::IsNullOrWhiteSpace($effectiveLicenseKey)) { $effectiveLicenseKey = Read-Host "Enter AtiqSoft license key or press Enter to skip" } } Invoke-LicenseValidation -Product $Product -Key $effectiveLicenseKey -Tenant $effectiveTenantId -Jwt $LicenseJwt | Out-Null if (-not $Quiet) { $confirm = Read-Host "Continue with this AtiqSoft package? Type YES to continue" if ($confirm -ne "YES") { Write-InstallLog "$Mode cancelled for $($Product.Name)" "WARN" Write-Host "Cancelled." return } } $fileName = Split-Path $package.downloadUrl -Leaf $targetPath = Join-Path $DownloadRoot $fileName if (-not (Test-PackageAvailable -Package $package)) { return } Write-InstallLog "$Mode started for $($Product.Name) $($package.version) on $SelectedChannel" Invoke-WebRequest -Uri $package.downloadUrl -OutFile $targetPath -UseBasicParsing Test-PackageFile -Path $targetPath -Package $package | Out-Null $arguments = $package.installerArgs if ($Mode -eq "Repair") { $arguments = "/repair" } $process = Start-Process -FilePath $targetPath -ArgumentList $arguments -Wait -PassThru Write-InstallLog "$Mode finished for $($Product.Name) $($package.version) with exit code $($process.ExitCode)" if ($process.ExitCode -eq 0) { Save-InstalledVersion -Id $Product.Id -Package $package Install-UpdateAgent -Product $Product -Tenant $effectiveTenantId -Key $effectiveLicenseKey -Package $package } Write-Host "$Mode finished with exit code $($process.ExitCode)." -ForegroundColor Green } function Invoke-Repair { param([Parameter(Mandatory = $true)][string]$SelectedChannel) $choice = Select-Product -Prompt "Select product to repair" $product = Get-ProductByMenu -Menu $choice if ($null -eq $product) { Write-Host "No product selected for repair." -ForegroundColor Yellow return } Invoke-AtiqSoftInstaller -Product $product -SelectedChannel $SelectedChannel -Mode "Repair" } function Invoke-Uninstall { Write-Host "" Write-Host "Opening Windows installed apps. Select the AtiqSoft app to uninstall." -ForegroundColor Cyan Write-InstallLog "Opened installed apps for AtiqSoft uninstall workflow" Start-Process "ms-settings:appsfeatures" } try { Write-Banner Initialize-InstallFolders Test-Windows if (-not (Test-Administrator)) { Write-Host "Administrator permission is required for installers and agents." -ForegroundColor Yellow Write-Host "Close this window, open PowerShell as Administrator, and run the command again." Write-InstallLog "Stopped because administrator permission was missing" "WARN" return } Write-InstallLog "Install Center launched on channel $Channel" Invoke-DeploymentProfileInstall -ProfileUrl $DeploymentProfileUrl if ($ProductId) { $product = Get-ProductById -Id $ProductId if ($null -eq $product) { throw "Unknown AtiqSoft product id: $ProductId" } Invoke-AtiqSoftInstaller -Product $product -SelectedChannel $Channel -Mode "Install" return } if (-not $Quiet) { $Channel = Select-Channel } $selection = Select-Product switch ($selection) { "0" { Write-InstallLog "User exited" Write-Host "Exit selected." } "9" { Invoke-Repair -SelectedChannel $Channel } "10" { Invoke-Uninstall } default { $product = Get-ProductByMenu -Menu $selection if ($null -eq $product) { Write-InstallLog "Invalid selection: $selection" "WARN" Write-Host "Invalid selection." -ForegroundColor Yellow return } Invoke-AtiqSoftInstaller -Product $product -SelectedChannel $Channel -Mode "Install" } } } catch { Initialize-InstallFolders Write-InstallLog $_.Exception.Message "ERROR" Write-Host $_.Exception.Message -ForegroundColor Red Write-Host "Install Center stopped without closing PowerShell. Review the message above and try again." -ForegroundColor Yellow return }