<# .SYNOPSIS ROA2WEB - Initial Installation Script for Windows Server + IIS .DESCRIPTION This script performs complete installation of ROA2WEB on Windows Server: - Checks prerequisites (Admin rights, IIS) - Installs Python 3.11+ if needed - Installs NSSM (service manager) - Installs IIS URL Rewrite and ARR modules - Creates directory structure - Installs Python dependencies - Creates Windows Service for backend - Configures IIS website - Starts all services .PARAMETER InstallPath Installation path (default: C:\inetpub\wwwroot\roa2web) .PARAMETER PythonVersion Python version to install (default: 3.11.9) .PARAMETER ServicePort Backend service port (default: 8000) .PARAMETER SkipPython Skip Python installation (use existing Python) .PARAMETER SkipIIS Skip IIS configuration .EXAMPLE .\Install-ROA2WEB.ps1 Standard installation with defaults .EXAMPLE .\Install-ROA2WEB.ps1 -InstallPath "D:\Apps\roa2web" -ServicePort 8001 Custom installation path and port .NOTES Author: ROA2WEB Team Requires: PowerShell 5.1+, Administrator privileges #> [CmdletBinding()] param( [string]$InstallPath = "C:\inetpub\wwwroot\roa2web", [string]$PythonVersion = "3.11.9", [int]$ServicePort = 8000, [string]$IISSiteName = "Default Web Site", [string]$IISAppName = "roa2web", [switch]$CreateNewSite, [switch]$SkipPython, [switch]$SkipIIS ) # Strict error handling $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" # ============================================================================= # CONFIGURATION # ============================================================================= $script:Config = @{ AppName = "ROA2WEB" ServiceName = "ROA2WEB-Backend" ServiceDisplayName = "ROA2WEB Unified Backend Service" ServiceDescription = "Unified FastAPI backend for ROA2WEB ERP - includes Reports, Data Entry, and Telegram modules (Ultrathin Monolith)" InstallPath = $InstallPath BackendPath = Join-Path $InstallPath "backend" FrontendPath = Join-Path $InstallPath "frontend" LogsPath = Join-Path $InstallPath "logs" TempPath = Join-Path $InstallPath "temp" # IMPORTANT: venv is OUTSIDE InstallPath to survive deployments! VenvPath = "C:\inetpub\wwwroot\roa2web-venv" PythonVersion = $PythonVersion ServicePort = $ServicePort IISSiteName = $IISSiteName IISAppName = $IISAppName IISAppPoolName = "ROA2WEB-AppPool" CreateNewSite = $CreateNewSite } # ============================================================================= # HELPER FUNCTIONS # ============================================================================= function Write-Step { param([string]$Message) Write-Host "`n[*] $Message" -ForegroundColor Cyan } function Write-Success { param([string]$Message) Write-Host " [OK] $Message" -ForegroundColor Green } function Write-Error { param([string]$Message) Write-Host " [ERROR] $Message" -ForegroundColor Red } function Write-Warning { param([string]$Message) Write-Host " [WARN] $Message" -ForegroundColor Yellow } function Test-Administrator { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]$identity return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } function Test-CommandExists { param([string]$Command) try { if (Get-Command $Command -ErrorAction Stop) { return $true } } catch { return $false } } function Install-Chocolatey { Write-Step "Installing Chocolatey package manager..." if (Test-CommandExists "choco") { Write-Success "Chocolatey already installed" return } try { Set-ExecutionPolicy Bypass -Scope Process -Force [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) # Refresh environment $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") Write-Success "Chocolatey installed successfully" } catch { throw "Failed to install Chocolatey: $_" } } function Install-Python { Write-Step "Checking Python installation..." if ($SkipPython) { Write-Warning "Skipping Python installation (as requested)" return } # Check if Python is already installed try { $pythonCmd = Get-Command python -ErrorAction Stop $pythonVersionOutput = & python --version 2>&1 if ($pythonVersionOutput -match "Python (\d+\.\d+\.\d+)") { $installedVersion = $matches[1] Write-Success "Python $installedVersion already installed at $($pythonCmd.Source)" return } } catch { Write-Warning "Python not found, will install..." } # Install Python via Chocolatey Write-Step "Installing Python $PythonVersion..." try { choco install python --version=$PythonVersion -y --force # Refresh environment $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") Write-Success "Python $PythonVersion installed successfully" } catch { throw "Failed to install Python: $_" } } function Install-NSSM { Write-Step "Installing NSSM (service manager)..." if (Test-Path "C:\nssm\nssm.exe") { Write-Success "NSSM already installed" return } try { choco install nssm -y Write-Success "NSSM installed successfully" } catch { throw "Failed to install NSSM: $_" } } function Install-IISModules { if ($SkipIIS) { Write-Warning "Skipping IIS configuration (as requested)" return } Write-Step "Checking IIS installation..." # Detect OS type (Server vs Desktop) $osInfo = Get-CimInstance -ClassName Win32_OperatingSystem $isServer = $osInfo.ProductType -eq 3 # 1=Workstation, 2=Domain Controller, 3=Server # Check if IIS is installed (different cmdlets for Server vs Desktop) $iisInstalled = $false if ($isServer) { # Windows Server - use Get-WindowsFeature $iisFeature = Get-WindowsFeature -Name Web-Server -ErrorAction SilentlyContinue $iisInstalled = $iisFeature -and $iisFeature.InstallState -eq "Installed" if (-not $iisInstalled) { Write-Error "IIS is not installed. Please install IIS first:" Write-Host " Install-WindowsFeature -Name Web-Server -IncludeManagementTools" -ForegroundColor Yellow throw "IIS not installed" } } else { # Windows Desktop (10/11) - use Get-WindowsOptionalFeature $iisFeature = Get-WindowsOptionalFeature -Online -FeatureName IIS-WebServer -ErrorAction SilentlyContinue $iisInstalled = $iisFeature -and $iisFeature.State -eq "Enabled" if (-not $iisInstalled) { Write-Error "IIS is not installed. Please install IIS first:" Write-Host " Enable-WindowsOptionalFeature -Online -FeatureName IIS-WebServer -All" -ForegroundColor Yellow Write-Host " Or use: Control Panel -> Programs -> Turn Windows features on/off -> Internet Information Services" -ForegroundColor Yellow throw "IIS not installed" } } Write-Success "IIS is installed ($($osInfo.Caption))" # Install URL Rewrite Module Write-Step "Installing IIS URL Rewrite Module..." $urlRewriteInstalled = Get-WebConfiguration -Filter "/system.webServer/rewrite" -PSPath "IIS:\" -ErrorAction SilentlyContinue if (-not $urlRewriteInstalled) { Write-Warning "URL Rewrite not found, installing..." try { $urlRewriteUrl = "https://download.microsoft.com/download/1/2/8/128E2E22-C1B9-44A4-BE2A-5859ED1D4592/rewrite_amd64_en-US.msi" $urlRewritePath = "$env:TEMP\rewrite_amd64.msi" Invoke-WebRequest -Uri $urlRewriteUrl -OutFile $urlRewritePath Start-Process msiexec.exe -ArgumentList "/i", $urlRewritePath, "/quiet", "/norestart" -Wait Remove-Item $urlRewritePath -Force Write-Success "URL Rewrite Module installed" } catch { Write-Error "Failed to install URL Rewrite: $_" Write-Warning "You can download it manually from: https://www.iis.net/downloads/microsoft/url-rewrite" } } else { Write-Success "URL Rewrite Module already installed" } # Install Application Request Routing (ARR) Write-Step "Checking Application Request Routing (ARR)..." try { choco install iis-arr -y Write-Success "ARR installed successfully" } catch { Write-Warning "Could not install ARR via Chocolatey. Download manually from: https://www.iis.net/downloads/microsoft/application-request-routing" } } function New-DirectoryStructure { Write-Step "Creating directory structure..." $directories = @( $Config.InstallPath, $Config.BackendPath, $Config.FrontendPath, $Config.LogsPath, $Config.TempPath, (Join-Path $Config.BackendPath "logs"), (Join-Path $Config.BackendPath "temp") ) foreach ($dir in $directories) { if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null Write-Success "Created: $dir" } else { Write-Success "Already exists: $dir" } } # Set permissions (IIS user needs read access) try { $acl = Get-Acl $Config.InstallPath $accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule("IIS_IUSRS", "ReadAndExecute", "ContainerInherit,ObjectInherit", "None", "Allow") $acl.SetAccessRule($accessRule) Set-Acl -Path $Config.InstallPath -AclObject $acl Write-Success "Permissions set for IIS_IUSRS" } catch { Write-Warning "Could not set permissions: $_" } } function Install-PythonDependencies { Write-Step "Setting up Python virtual environment..." $requirementsPath = Join-Path $Config.BackendPath "requirements.txt" $venvPath = $Config.VenvPath $venvPython = Join-Path $venvPath "Scripts\python.exe" $venvPip = Join-Path $venvPath "Scripts\pip.exe" # Create venv if it doesn't exist if (-not (Test-Path $venvPython)) { Write-Step "Creating virtual environment at $venvPath..." try { & python -m venv $venvPath Write-Success "Virtual environment created" } catch { throw "Failed to create virtual environment: $_" } } else { Write-Success "Virtual environment already exists" } # Upgrade pip in venv Write-Step "Upgrading pip in virtual environment..." try { & $venvPython -m pip install --upgrade pip Write-Success "Pip upgraded" } catch { Write-Warning "Could not upgrade pip: $_" } # Install dependencies if (-not (Test-Path $requirementsPath)) { Write-Warning "requirements.txt not found at $requirementsPath" Write-Warning "Please copy backend files first, then run this script again" return } Write-Step "Installing Python dependencies in virtual environment..." try { & $venvPip install -r $requirementsPath Write-Success "Python dependencies installed successfully in venv" } catch { throw "Failed to install Python dependencies: $_" } } function New-WindowsService { Write-Step "Creating Windows Service for backend..." # Check if service already exists using nssm (more reliable than Get-Service) # Temporarily disable error action to check service status $oldErrorAction = $ErrorActionPreference $ErrorActionPreference = "SilentlyContinue" $nssmOutput = & nssm status $Config.ServiceName 2>&1 $serviceExists = $LASTEXITCODE -eq 0 $ErrorActionPreference = $oldErrorAction if ($serviceExists) { Write-Warning "Service already exists, stopping and removing..." # Check service status $status = & nssm status $Config.ServiceName 2>&1 # Only try to stop if service is running if ($status -match "SERVICE_RUNNING") { Write-Step "Stopping running service..." & nssm stop $Config.ServiceName 2>&1 | Out-Null Start-Sleep -Seconds 2 } else { Write-Step "Service is not running (status: $status)" } # Force remove service & nssm remove $Config.ServiceName confirm 2>&1 | Out-Null Start-Sleep -Seconds 2 Write-Success "Existing service removed" } # Get Python path from venv $venvPython = Join-Path $Config.VenvPath "Scripts\python.exe" if (-not (Test-Path $venvPython)) { throw "Virtual environment Python not found at $venvPython. Run Install-PythonDependencies first." } $uvicornModule = "uvicorn" $appModule = "main:app" # NSSM service creation try { # Install service using venv Python # NOTE: Using --workers 1 because Telegram bot requires single instance (polling conflict) & nssm install $Config.ServiceName $venvPython "-m" $uvicornModule $appModule "--host" "127.0.0.1" "--port" $Config.ServicePort.ToString() "--workers" "1" # Set service configuration & nssm set $Config.ServiceName DisplayName $Config.ServiceDisplayName & nssm set $Config.ServiceName Description $Config.ServiceDescription & nssm set $Config.ServiceName Start SERVICE_AUTO_START & nssm set $Config.ServiceName AppDirectory $Config.BackendPath # Set environment variables (PYTHONPATH for shared modules) # Point to the installation root AND backend/ so both shared/ and app/ modules can be imported $pythonPathRoot = $Config.InstallPath $pythonPathBackend = $Config.BackendPath & nssm set $Config.ServiceName AppEnvironmentExtra "PYTHONPATH=$pythonPathRoot;$pythonPathBackend" # Set logging $stdoutLog = Join-Path $Config.LogsPath "backend-stdout.log" $stderrLog = Join-Path $Config.LogsPath "backend-stderr.log" & nssm set $Config.ServiceName AppStdout $stdoutLog & nssm set $Config.ServiceName AppStderr $stderrLog & nssm set $Config.ServiceName AppStdoutCreationDisposition 4 & nssm set $Config.ServiceName AppStderrCreationDisposition 4 # Set restart policy & nssm set $Config.ServiceName AppExit Default Restart & nssm set $Config.ServiceName AppRestartDelay 5000 Write-Success "Windows Service created successfully" } catch { throw "Failed to create Windows Service: $_" } } function Initialize-IISWebsite { if ($SkipIIS) { Write-Warning "Skipping IIS website configuration (as requested)" return } Write-Step "Configuring IIS application..." Import-Module WebAdministration -ErrorAction Stop # Remove existing app pool if present if (Test-Path "IIS:\AppPools\$($Config.IISAppPoolName)") { Write-Warning "Removing existing app pool..." Remove-WebAppPool -Name $Config.IISAppPoolName -ErrorAction SilentlyContinue } # Create Application Pool Write-Step "Creating IIS Application Pool..." New-WebAppPool -Name $Config.IISAppPoolName -Force | Out-Null Set-ItemProperty -Path "IIS:\AppPools\$($Config.IISAppPoolName)" -Name "managedRuntimeVersion" -Value "" Write-Success "Application Pool created: $($Config.IISAppPoolName)" if ($CreateNewSite) { # Create new website (old behavior) Write-Step "Creating new IIS Website..." # Stop default website if running try { Stop-Website -Name "Default Web Site" -ErrorAction SilentlyContinue Write-Success "Stopped Default Web Site" } catch { Write-Warning "Could not stop Default Web Site: $_" } # Remove existing site if present if (Get-Website -Name $Config.IISSiteName -ErrorAction SilentlyContinue) { Write-Warning "Removing existing website..." Remove-Website -Name $Config.IISSiteName -ErrorAction SilentlyContinue } New-Website -Name $Config.IISSiteName ` -PhysicalPath $Config.FrontendPath ` -ApplicationPool $Config.IISAppPoolName ` -Port 80 ` -Force | Out-Null Write-Success "Website created: $($Config.IISSiteName)" # Start website Start-Website -Name $Config.IISSiteName Write-Success "Website started: $($Config.IISSiteName)" } else { # Create application under existing site (default behavior) Write-Step "Creating IIS Application under '$($Config.IISSiteName)'..." # Verify parent site exists $parentSite = Get-Website -Name $Config.IISSiteName -ErrorAction SilentlyContinue if (-not $parentSite) { throw "Parent website '$($Config.IISSiteName)' does not exist. Use -CreateNewSite to create a new site." } # Remove existing application if present $existingApp = Get-WebApplication -Name $Config.IISAppName -Site $Config.IISSiteName -ErrorAction SilentlyContinue if ($existingApp) { Write-Warning "Removing existing application..." Remove-WebApplication -Name $Config.IISAppName -Site $Config.IISSiteName -ErrorAction SilentlyContinue } # Create application New-WebApplication -Name $Config.IISAppName ` -Site $Config.IISSiteName ` -PhysicalPath $Config.FrontendPath ` -ApplicationPool $Config.IISAppPoolName ` -Force | Out-Null Write-Success "Application created: /$($Config.IISAppName) under $($Config.IISSiteName)" } # Copy web.config to frontend path $webConfigSource = Join-Path $PSScriptRoot "..\config\web.config" $webConfigDest = Join-Path $Config.FrontendPath "web.config" if (Test-Path $webConfigSource) { Copy-Item -Path $webConfigSource -Destination $webConfigDest -Force Write-Success "web.config copied to frontend path" } else { Write-Warning "web.config not found at $webConfigSource" } } function Start-Services { Write-Step "Starting services..." # Start backend service try { Start-Service -Name $Config.ServiceName Start-Sleep -Seconds 3 $service = Get-Service -Name $Config.ServiceName if ($service.Status -eq "Running") { Write-Success "Backend service started successfully" } else { Write-Error "Backend service failed to start (Status: $($service.Status))" } } catch { Write-Error "Failed to start backend service: $_" } # Test backend health Write-Step "Testing backend health..." Start-Sleep -Seconds 5 try { $response = Invoke-WebRequest -Uri "http://localhost:$($Config.ServicePort)/health" -UseBasicParsing -TimeoutSec 10 if ($response.StatusCode -eq 200) { Write-Success "Backend health check passed" } } catch { Write-Warning "Backend health check failed (may need time to start): $_" } } function Show-Summary { Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan Write-Host " ROA2WEB INSTALLATION COMPLETED" -ForegroundColor Green Write-Host ("=" * 80) -ForegroundColor Cyan Write-Host "`nInstallation Details:" -ForegroundColor Yellow Write-Host " Install Path: $($Config.InstallPath)" Write-Host " Backend Path: $($Config.BackendPath)" Write-Host " Virtual Env: $($Config.VenvPath)" Write-Host " Frontend Path: $($Config.FrontendPath)" Write-Host " Service Name: $($Config.ServiceName)" Write-Host " Service Port: $($Config.ServicePort)" Write-Host " IIS Site: $($Config.IISSiteName)" Write-Host "`nAccess Points:" -ForegroundColor Yellow if ($Config.CreateNewSite) { Write-Host " Web Application: http://localhost" } else { Write-Host " Web Application: http://localhost/$($Config.IISAppName)" } Write-Host " API Backend: http://localhost:$($Config.ServicePort)" Write-Host " API Docs: http://localhost:$($Config.ServicePort)/docs" Write-Host " Health Check: http://localhost:$($Config.ServicePort)/health" Write-Host "`nNext Steps:" -ForegroundColor Yellow Write-Host " 1. Copy backend files to: $($Config.BackendPath)" Write-Host " 2. Copy frontend files to: $($Config.FrontendPath)" Write-Host " 3. Configure .env file at: $($Config.BackendPath)\.env" Write-Host "" Write-Host " IMPORTANT - Module Control Flags in .env:" -ForegroundColor Cyan Write-Host " MODULE_REPORTS_ENABLED=true # Enable/disable Reports module" Write-Host " MODULE_DATA_ENTRY_ENABLED=true # Enable/disable Data Entry module" Write-Host " MODULE_TELEGRAM_ENABLED=true # Enable/disable Telegram bot module" Write-Host "" Write-Host " 4. Start service: Start-Service $($Config.ServiceName)" Write-Host "`nManagement Commands:" -ForegroundColor Yellow Write-Host " Start Service: Start-Service $($Config.ServiceName)" Write-Host " Stop Service: Stop-Service $($Config.ServiceName)" Write-Host " Restart Service: Restart-Service $($Config.ServiceName)" Write-Host " View Logs: Get-Content $($Config.LogsPath)\backend-stdout.log -Tail 50" Write-Host " Check Status: Get-Service $($Config.ServiceName)" Write-Host "`nArchitecture:" -ForegroundColor Yellow Write-Host " ULTRATHIN MONOLITH - Single Windows service with multiple modules" Write-Host " All modules share Oracle pool, auth, and cache" Write-Host " Telegram bot runs as background task (not separate service)" Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan } # ============================================================================= # MAIN INSTALLATION FLOW # ============================================================================= function Main { Write-Host @" ==================================================================== ROA2WEB - Windows Server Installation Script Modern ERP Reports Application with FastAPI + Vue.js + IIS ==================================================================== "@ -ForegroundColor Cyan # Check prerequisites Write-Step "Checking prerequisites..." if (-not (Test-Administrator)) { Write-Error "This script must be run as Administrator" Write-Host " Right-click PowerShell and select 'Run as Administrator'" -ForegroundColor Yellow exit 1 } Write-Success "Running as Administrator" try { # Installation steps Install-Chocolatey Install-Python Install-NSSM Install-IISModules New-DirectoryStructure Install-PythonDependencies New-WindowsService Initialize-IISWebsite Start-Services Show-Summary Write-Host "`nInstallation completed successfully!" -ForegroundColor Green } catch { Write-Host "`n[FATAL ERROR] Installation failed: $_" -ForegroundColor Red Write-Host $_.ScriptStackTrace -ForegroundColor Red exit 1 } } # Run main installation Main