<# .SYNOPSIS Instala la API (que también sirve la web) como servicio de Windows usando WinSW. .EXAMPLE .\instalar-servicio.ps1 -WinSW C:\descargas\WinSW-x64.exe .EXAMPLE .\instalar-servicio.ps1 -WinSW C:\descargas\WinSW-x64.exe -Cuenta 'EMPRESA\svc-devolucion' -UsarCertificadosDelSistema #> #Requires -RunAsAdministrator [CmdletBinding()] param( # Ruta del ejecutable de WinSW (WinSW-x64.exe, de https://github.com/winsw/winsw/releases) [Parameter(Mandatory)][string]$WinSW, # Cuenta con la que corre el servicio (DOMINIO\usuario). Si se omite: LocalSystem. [string]$Cuenta, [string]$NombreServicio = 'DevolucionProveedores', # Agrega NODE_OPTIONS=--use-system-ca: necesario si Fortinet u otro firewall inspecciona el tráfico # HTTPS de salida (por ejemplo hacia el web service de BES) con un certificado propio. [switch]$UsarCertificadosDelSistema ) $ErrorActionPreference = 'Stop' $raiz = $PSScriptRoot $apiDir = Join-Path $raiz 'app\apps\api' function Fallar($mensaje) { Write-Host "ERROR: $mensaje" -ForegroundColor Red; exit 1 } # --- Verificaciones previas ------------------------------------------------------------------- if (-not (Test-Path $WinSW)) { Fallar "No se encontró WinSW en '$WinSW'." } $node = Get-Command node -ErrorAction SilentlyContinue if (-not $node) { Fallar 'Node.js no está instalado (o no está en el PATH). Instale Node 22 LTS.' } $versionNode = [version](& $node.Source --version).TrimStart('v') if ($versionNode.Major -lt 22) { Fallar "Se necesita Node 22 o superior; hay $versionNode." } if (-not (Test-Path (Join-Path $apiDir 'dist\main.js'))) { Fallar "Falta $apiDir\dist\main.js (¿se copió la carpeta app completa?)." } if (-not (Test-Path (Join-Path $apiDir 'node_modules')) -and -not (Test-Path (Join-Path $raiz 'app\node_modules'))) { Fallar 'Faltan las dependencias (app\node_modules).' } if (-not (Test-Path (Join-Path $raiz 'app\apps\web\dist\web\browser\index.html'))) { Fallar 'Falta la web compilada (app\apps\web\dist\web\browser).' } if (-not (Test-Path (Join-Path $apiDir '.env'))) { Fallar "Falta $apiDir\.env. Copie .env.example a .env y complételo (ver DEPLOY.md)." } if (Get-Service -Name $NombreServicio -ErrorAction SilentlyContinue) { Fallar "El servicio '$NombreServicio' ya existe. Ejecute primero .\desinstalar-servicio.ps1." } # --- Archivos del servicio ---------------------------------------------------------------------- $servicioDir = Join-Path $raiz 'servicio' New-Item -ItemType Directory -Force -Path $servicioDir, (Join-Path $raiz 'logs') | Out-Null $exe = Join-Path $servicioDir 'devolucion-api.exe' $xml = Join-Path $servicioDir 'devolucion-api.xml' Copy-Item -Path $WinSW -Destination $exe -Force $nodeOptions = if ($UsarCertificadosDelSistema) { ' ' } else { '' } # .Replace() sustituye texto literal (las rutas pueden tener espacios, puntos o signos que -replace trataría como patrón) $contenido = (Get-Content (Join-Path $raiz 'devolucion-api.xml.plantilla') -Raw -Encoding UTF8). Replace('__ID__', $NombreServicio). Replace('__NODE__', $node.Source). Replace('__RAIZ__', $raiz). Replace('__NODE_OPTIONS__', $nodeOptions) if ($contenido -match '__[A-Z_]+__') { Fallar "La plantilla tiene un marcador sin reemplazar: $($Matches[0])" } # UTF-8 sin BOM [System.IO.File]::WriteAllText($xml, $contenido, (New-Object System.Text.UTF8Encoding($false))) # --- Instalación -------------------------------------------------------------------------------- Write-Host "Instalando el servicio '$NombreServicio'..." & $exe install if ($LASTEXITCODE -ne 0) { Fallar 'WinSW no pudo instalar el servicio.' } if ($Cuenta) { # La contraseña se pide aquí y se pasa a Windows; no queda escrita en ningún archivo. $credencial = Get-Credential -UserName $Cuenta -Message "Contraseña de la cuenta del servicio" $clave = $credencial.GetNetworkCredential().Password & sc.exe config $NombreServicio obj= $credencial.UserName password= $clave | Out-Null if ($LASTEXITCODE -ne 0) { Fallar "No se pudo asignar la cuenta '$Cuenta' al servicio." } Write-Host "Cuenta del servicio: $($credencial.UserName). Recuerde darle 'Iniciar sesión como servicio' y permiso de lectura sobre $raiz." } Write-Host 'Iniciando...' & $exe start Start-Sleep -Seconds 3 & $exe status Write-Host "`nLogs: $(Join-Path $raiz 'logs')" -ForegroundColor Green Write-Host 'Compruebe: Invoke-WebRequest /api/health' -ForegroundColor Green