Public Access
The ChatGPT transcript showed each CAMBS practice has its own SFTP account (YSMD is YAM62682 on io.myallylogin.com:14147), not one shared entity login - so the wizard needed a way to set those without hand- writing nine practice.json files. Option 5 does that, and the settings screen now lists which login each practice will actually use. test-login.bat tests one account on its own, with the details typed in rather than read from a config, which isolates the account from whatever a script is doing to the credentials. It reports password length, stray whitespace, and $ / backtick characters - the last being the usual reason a login works in the WinSCP window but fails from a script, since "$pass" interpolates in PowerShell while '$pass' does not. On failure it prints the authentication methods the server offered. check.sh runs what has actually caught bugs here: non-ASCII characters, "$name?" (? is legal in an unbraced variable name, so it renders empty - this bit twice), parse errors, 5.1 syntax compatibility, and JSON validity.
774 lines
30 KiB
PowerShell
774 lines
30 KiB
PowerShell
# Interactive setup - asks plain questions and writes entity.json so nobody has
|
|
# to hand-edit JSON. Called by setup-entity.bat. Safe to re-run any time.
|
|
|
|
$scriptRoot = Split-Path -Parent $PSCommandPath
|
|
$entitiesDir = Join-Path $scriptRoot "Entities"
|
|
$winscpDll = Join-Path $scriptRoot "WinSCP\WinSCPnet.dll"
|
|
|
|
if (-not (Test-Path $entitiesDir)) { New-Item -ItemType Directory -Path $entitiesDir | Out-Null }
|
|
|
|
# -- Asking questions ---------------------------------------------------------
|
|
|
|
function Read-Answer($question, $default, [switch]$AllowBlank) {
|
|
while ($true) {
|
|
if ($default) {
|
|
Write-Host "$question " -NoNewline -ForegroundColor White
|
|
Write-Host "[$default]" -NoNewline -ForegroundColor DarkGray
|
|
Write-Host ": " -NoNewline -ForegroundColor White
|
|
} else {
|
|
Write-Host "${question}: " -NoNewline -ForegroundColor White
|
|
}
|
|
$answer = Read-Host
|
|
if ([string]::IsNullOrWhiteSpace($answer)) {
|
|
if ($default) { return $default }
|
|
if ($AllowBlank) { return $null }
|
|
Write-Host " (this one is required)" -ForegroundColor Yellow
|
|
continue
|
|
}
|
|
return $answer.Trim()
|
|
}
|
|
}
|
|
|
|
function Read-YesNo($question, $default) {
|
|
$hint = if ($default -eq "y") { "Y/n" } else { "y/N" }
|
|
while ($true) {
|
|
Write-Host "$question " -NoNewline -ForegroundColor White
|
|
Write-Host "($hint)" -NoNewline -ForegroundColor DarkGray
|
|
Write-Host ": " -NoNewline -ForegroundColor White
|
|
$answer = Read-Host
|
|
if ([string]::IsNullOrWhiteSpace($answer)) { $answer = $default }
|
|
switch ($answer.Trim().ToLower()) {
|
|
"y" { return $true }
|
|
"yes" { return $true }
|
|
"n" { return $false }
|
|
"no" { return $false }
|
|
}
|
|
Write-Host " (please answer y or n)" -ForegroundColor Yellow
|
|
}
|
|
}
|
|
|
|
function Read-Choice($question, $options, $default) {
|
|
Write-Host ""
|
|
Write-Host $question -ForegroundColor White
|
|
for ($i = 0; $i -lt $options.Count; $i++) {
|
|
Write-Host (" {0}. {1}" -f ($i + 1), $options[$i])
|
|
}
|
|
while ($true) {
|
|
Write-Host "Choose 1-$($options.Count) " -NoNewline -ForegroundColor White
|
|
Write-Host "[$default]" -NoNewline -ForegroundColor DarkGray
|
|
Write-Host ": " -NoNewline -ForegroundColor White
|
|
$answer = Read-Host
|
|
if ([string]::IsNullOrWhiteSpace($answer)) { $answer = [string]$default }
|
|
$n = 0
|
|
if ([int]::TryParse(([string]$answer).Trim(), [ref]$n) -and $n -ge 1 -and $n -le $options.Count) {
|
|
return $n
|
|
}
|
|
Write-Host " (type a number between 1 and $($options.Count))" -ForegroundColor Yellow
|
|
}
|
|
}
|
|
|
|
function Write-Header($text) {
|
|
Write-Host ""
|
|
Write-Host ("=" * 64) -ForegroundColor Cyan
|
|
Write-Host " $text" -ForegroundColor Cyan
|
|
Write-Host ("=" * 64) -ForegroundColor Cyan
|
|
}
|
|
|
|
function Write-Section($text) {
|
|
Write-Host ""
|
|
Write-Host "--- $text " -NoNewline -ForegroundColor Cyan
|
|
Write-Host ("-" * [Math]::Max(1, (58 - $text.Length))) -ForegroundColor Cyan
|
|
}
|
|
|
|
# -- Config file read/write ---------------------------------------------------
|
|
|
|
function Get-EntityNames {
|
|
if (-not (Test-Path $entitiesDir)) { return @() }
|
|
return @(Get-ChildItem -Path $entitiesDir -Directory |
|
|
Where-Object { $_.Name -ne "EXAMPLE" } |
|
|
ForEach-Object { $_.Name })
|
|
}
|
|
|
|
function Save-EntityConfig($entityName, $config) {
|
|
$dir = Join-Path $entitiesDir $entityName
|
|
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir | Out-Null }
|
|
$file = Join-Path $dir "entity.json"
|
|
$config | ConvertTo-Json -Depth 10 | Set-Content -Path $file -Encoding UTF8
|
|
return $file
|
|
}
|
|
|
|
function Get-EntityConfig($entityName) {
|
|
$file = Join-Path (Join-Path $entitiesDir $entityName) "entity.json"
|
|
if (-not (Test-Path $file)) { return $null }
|
|
return Get-Content $file -Raw | ConvertFrom-Json
|
|
}
|
|
|
|
function Copy-RunBat($entityName) {
|
|
$source = Join-Path $scriptRoot "run.bat"
|
|
$target = Join-Path (Join-Path $entitiesDir $entityName) "run.bat"
|
|
if ((Test-Path $source) -and -not (Test-Path $target)) {
|
|
Copy-Item -Path $source -Destination $target
|
|
}
|
|
}
|
|
|
|
# -- Asking about one server --------------------------------------------------
|
|
|
|
# Returns a hashtable of connection settings, or $null if the user backs out.
|
|
function Read-ServerDetails($label, $existing) {
|
|
$defaultHost = $null
|
|
$defaultUser = $null
|
|
$defaultProto = 1
|
|
if ($existing -and $existing.host) {
|
|
$defaultHost = (([string]$existing.host) -replace '^\w+://', '').Split('/')[0].Trim()
|
|
$defaultUser = $existing.username
|
|
switch (Get-ConnProtocol $existing "sftp") {
|
|
"ftps" { $defaultProto = 2 }
|
|
"ftp" { $defaultProto = 3 }
|
|
}
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host "Server address for $label" -ForegroundColor White
|
|
Write-Host " Just the name - no sftp:// or ftp:// in front." -ForegroundColor DarkGray
|
|
Write-Host " Example: sft.polluxsystems.com" -ForegroundColor DarkGray
|
|
$serverHost = Read-Answer "Server address" $defaultHost
|
|
|
|
# Strip a pasted scheme/path so it can't break the connection later.
|
|
$serverHost = ($serverHost -replace '^\w+://', '').Split('/')[0].Trim()
|
|
|
|
$protoChoice = Read-Choice "How does $serverHost receive files?" @(
|
|
"SFTP - secure, port 22 (most common)",
|
|
"FTPS - secure FTP, port 21",
|
|
"FTP - plain, no encryption, port 21"
|
|
) $defaultProto
|
|
|
|
$protocol = @("sftp", "ftps", "ftp")[$protoChoice - 1]
|
|
|
|
Write-Host ""
|
|
$username = Read-Answer "Username" $defaultUser
|
|
|
|
$password = $null
|
|
if ($existing -and $existing.password) {
|
|
Write-Host ""
|
|
Write-Host "A password is already saved for this server." -ForegroundColor DarkGray
|
|
if (Read-YesNo "Keep the saved password?" "y") {
|
|
$password = $existing.password
|
|
}
|
|
}
|
|
if (-not $password) {
|
|
$password = Read-Answer "Password" $null
|
|
}
|
|
|
|
return @{
|
|
host = $serverHost
|
|
protocol = $protocol
|
|
username = $username
|
|
password = $password
|
|
}
|
|
}
|
|
|
|
function Read-RemotePath($what, $default) {
|
|
Write-Host ""
|
|
Write-Host "Where do $what go on the server?" -ForegroundColor White
|
|
Write-Host " Type {practice} where the practice name belongs and it gets" -ForegroundColor DarkGray
|
|
Write-Host " filled in automatically for each one - so one line covers all." -ForegroundColor DarkGray
|
|
Write-Host " Example: /home/clients/CAMBS/{practice}" -ForegroundColor DarkGray
|
|
return Read-Answer "Folder path" $default
|
|
}
|
|
|
|
# -- New / edit entity --------------------------------------------------------
|
|
|
|
function New-Entity {
|
|
Write-Header "Set up a new entity"
|
|
|
|
Write-Host ""
|
|
Write-Host "The entity name is the folder name you use for this group of" -ForegroundColor DarkGray
|
|
Write-Host "practices - for example CAMBS, RMI, INLAND." -ForegroundColor DarkGray
|
|
$entityName = $null
|
|
while (-not $entityName) {
|
|
$candidate = Read-Answer "Entity name" $null
|
|
if ($candidate.IndexOfAny([System.IO.Path]::GetInvalidFileNameChars()) -ge 0) {
|
|
Write-Host " A folder name cannot contain \ / : * ? "" < > | - try again." -ForegroundColor Yellow
|
|
continue
|
|
}
|
|
$entityName = $candidate
|
|
}
|
|
|
|
$entityDir = Join-Path $entitiesDir $entityName
|
|
if (Test-Path (Join-Path $entityDir "entity.json")) {
|
|
Write-Host ""
|
|
Write-Host "$entityName is already set up." -ForegroundColor Yellow
|
|
if (-not (Read-YesNo "Start over and replace its settings?" "n")) { return }
|
|
}
|
|
|
|
Write-Section "The daily upload"
|
|
Write-Host "These are the dated folders (like 20260826) full of scanned images." -ForegroundColor DarkGray
|
|
|
|
$daily = Read-ServerDetails "the daily files" $null
|
|
$tiffPath = Read-RemotePath "the daily image files" $null
|
|
|
|
Write-Host ""
|
|
$pdfPath = $null
|
|
Write-Host "Some entities get PT STMT files mixed into the same dated folder," -ForegroundColor DarkGray
|
|
Write-Host "and those go to a different folder on the server." -ForegroundColor DarkGray
|
|
if (Read-YesNo "Do PT STMT files show up in the dated folders for ${entityName}?" "n") {
|
|
$pdfPath = Read-RemotePath "the PT STMT files" $null
|
|
}
|
|
|
|
Write-Section "Patient files"
|
|
Write-Host "These come from Patient\Export\PT STMT_<date>\ - the folders the" -ForegroundColor DarkGray
|
|
Write-Host "rename script creates. Many entities do not use this at all." -ForegroundColor DarkGray
|
|
|
|
$patient = $null
|
|
$patientPath = $null
|
|
if (Read-YesNo "Does $entityName send patient files too?" "n") {
|
|
$samePlace = Read-Choice "Which server do the patient files go to?" @(
|
|
"The same server as the daily files ($($daily.host))",
|
|
"A different server"
|
|
) 1
|
|
if ($samePlace -eq 1) {
|
|
$patient = @{
|
|
host = $daily.host
|
|
protocol = $daily.protocol
|
|
username = $daily.username
|
|
password = $daily.password
|
|
}
|
|
} else {
|
|
$patient = Read-ServerDetails "the patient files" $null
|
|
}
|
|
$patientPath = Read-RemotePath "the patient .txt/.tif files" $null
|
|
}
|
|
|
|
$config = New-ConfigObject $daily $tiffPath $pdfPath $patient $patientPath
|
|
$file = Save-EntityConfig $entityName $config
|
|
Copy-RunBat $entityName
|
|
|
|
Write-Host ""
|
|
Write-Host "Saved $file" -ForegroundColor Green
|
|
|
|
Add-Practices $entityName
|
|
Write-Host ""
|
|
if (Read-YesNo "Test the connection now?" "y") { Test-Entity $entityName }
|
|
}
|
|
|
|
function New-ConfigObject($daily, $tiffPath, $pdfPath, $patient, $patientPath) {
|
|
$workflow = if ($patient) { "insurance+patient" } else { "insurance" }
|
|
|
|
$ftps = [ordered]@{
|
|
host = $daily.host
|
|
protocol = $daily.protocol
|
|
port = $null
|
|
username = $daily.username
|
|
password = $daily.password
|
|
tiff_path = $tiffPath
|
|
pdf_path = $pdfPath
|
|
}
|
|
|
|
$sftp = [ordered]@{
|
|
host = $null
|
|
protocol = $null
|
|
port = $null
|
|
username = $null
|
|
password = $null
|
|
patient_path = $null
|
|
}
|
|
if ($patient) {
|
|
$sftp.host = $patient.host
|
|
$sftp.protocol = $patient.protocol
|
|
$sftp.username = $patient.username
|
|
$sftp.password = $patient.password
|
|
$sftp.patient_path = $patientPath
|
|
}
|
|
|
|
return [PSCustomObject][ordered]@{
|
|
"_note" = "Written by setup-entity.bat - run it again to change anything. The 'ftps' section is the daily upload (it can be SFTP too - see protocol); the 'sftp' section is the patient files."
|
|
workflow = $workflow
|
|
ftps = [PSCustomObject]$ftps
|
|
sftp = [PSCustomObject]$sftp
|
|
}
|
|
}
|
|
|
|
function Add-Practices($entityName) {
|
|
$export1 = Join-Path (Join-Path $entitiesDir $entityName) "Export1"
|
|
if (-not (Test-Path $export1)) { New-Item -ItemType Directory -Path $export1 | Out-Null }
|
|
|
|
$existing = @(Get-ChildItem -Path $export1 -Directory | ForEach-Object { $_.Name })
|
|
Write-Section "Practices under $entityName"
|
|
|
|
if ($existing.Count -gt 0) {
|
|
Write-Host "Already there: $($existing -join ', ')" -ForegroundColor Gray
|
|
Write-Host ""
|
|
if (-not (Read-YesNo "Add more practice folders?" "n")) { return }
|
|
} else {
|
|
Write-Host "Type each practice name and press Enter." -ForegroundColor DarkGray
|
|
Write-Host "Press Enter on an empty line when you are done." -ForegroundColor DarkGray
|
|
}
|
|
|
|
while ($true) {
|
|
$name = Read-Answer "Practice name (blank to finish)" $null -AllowBlank
|
|
if (-not $name) { break }
|
|
if ($name.IndexOfAny([System.IO.Path]::GetInvalidFileNameChars()) -ge 0) {
|
|
Write-Host " A folder name cannot contain \ / : * ? "" < > | - try again." -ForegroundColor Yellow
|
|
continue
|
|
}
|
|
$dir = Join-Path $export1 $name
|
|
if (Test-Path $dir) {
|
|
Write-Host " $name already exists - skipped." -ForegroundColor Gray
|
|
continue
|
|
}
|
|
New-Item -ItemType Directory -Path $dir | Out-Null
|
|
New-Item -ItemType Directory -Path (Join-Path $dir "Archive sent to FTP") | Out-Null
|
|
New-Item -ItemType Directory -Path (Join-Path $dir "Patient\Export\Archive Sent to FTP") -Force | Out-Null
|
|
Write-Host " Created $name" -ForegroundColor Green
|
|
}
|
|
}
|
|
|
|
function Get-PracticeNames($entityName) {
|
|
$export1 = Join-Path (Join-Path $entitiesDir $entityName) "Export1"
|
|
if (-not (Test-Path $export1)) { return @() }
|
|
return @(Get-ChildItem -Path $export1 -Directory | ForEach-Object { $_.Name })
|
|
}
|
|
|
|
# Same rules engine.ps1 uses: practice.json replaces or adds to entity.json for
|
|
# that one practice, so what gets tested here is what will actually be sent.
|
|
function Get-MergedPracticeConfig($entityName, $practiceName, $entityConfig) {
|
|
$file = Join-Path (Join-Path (Join-Path (Join-Path $entitiesDir $entityName) "Export1") $practiceName) "practice.json"
|
|
$merged = $entityConfig | ConvertTo-Json -Depth 10 | ConvertFrom-Json
|
|
if (-not (Test-Path $file)) { return $merged }
|
|
|
|
$override = Get-Content $file -Raw | ConvertFrom-Json
|
|
foreach ($section in $override.PSObject.Properties) {
|
|
if ($section.Name -like "_*") { continue }
|
|
if ($null -eq $merged.PSObject.Properties[$section.Name]) {
|
|
$merged | Add-Member -NotePropertyName $section.Name -NotePropertyValue $section.Value
|
|
} elseif ($section.Value -is [System.Management.Automation.PSCustomObject]) {
|
|
foreach ($prop in $section.Value.PSObject.Properties) {
|
|
$merged.$($section.Name) | Add-Member -NotePropertyName $prop.Name -NotePropertyValue $prop.Value -Force
|
|
}
|
|
} else {
|
|
Set-Field $merged $section.Name $section.Value
|
|
}
|
|
}
|
|
return $merged
|
|
}
|
|
|
|
function Set-PracticeLogin($entityName, $entityConfig) {
|
|
$practices = Get-PracticeNames $entityName
|
|
if ($practices.Count -eq 0) {
|
|
Write-Host ""
|
|
Write-Host "No practice folders yet - add some first." -ForegroundColor Yellow
|
|
return
|
|
}
|
|
|
|
$choice = Read-Choice "Which practice?" $practices 1
|
|
$practiceName = $practices[$choice - 1]
|
|
$dir = Join-Path (Join-Path (Join-Path $entitiesDir $entityName) "Export1") $practiceName
|
|
$file = Join-Path $dir "practice.json"
|
|
|
|
$existing = $null
|
|
if (Test-Path $file) { $existing = Get-Content $file -Raw | ConvertFrom-Json }
|
|
|
|
Write-Section "$practiceName"
|
|
Write-Host "Anything you set here is used for $practiceName only." -ForegroundColor DarkGray
|
|
Write-Host "Anything you leave alone still comes from $entityName." -ForegroundColor DarkGray
|
|
|
|
$ftps = [ordered]@{}
|
|
if ($existing -and $existing.ftps) {
|
|
foreach ($p in $existing.ftps.PSObject.Properties) { $ftps[$p.Name] = $p.Value }
|
|
}
|
|
|
|
Write-Host ""
|
|
if (Read-YesNo "Does $practiceName have its own username and password?" "y") {
|
|
Write-Host ""
|
|
$ftps["username"] = Read-Answer "Username for $practiceName" $ftps["username"]
|
|
$keep = $false
|
|
if ($ftps["password"]) {
|
|
Write-Host ""
|
|
Write-Host "A password is already saved for $practiceName." -ForegroundColor DarkGray
|
|
$keep = Read-YesNo "Keep it?" "y"
|
|
}
|
|
if (-not $keep) { $ftps["password"] = Read-Answer "Password for $practiceName" $null }
|
|
}
|
|
|
|
Write-Host ""
|
|
if (Read-YesNo "Does $practiceName use a different server than the rest of ${entityName}?" "n") {
|
|
$srv = Read-ServerDetails "$practiceName" $existing.ftps
|
|
$ftps["host"] = $srv.host
|
|
$ftps["protocol"] = $srv.protocol
|
|
$ftps["username"] = $srv.username
|
|
$ftps["password"] = $srv.password
|
|
}
|
|
|
|
Write-Host ""
|
|
if (Read-YesNo "Does $practiceName use a different folder on the server?" "n") {
|
|
$ftps["tiff_path"] = Read-RemotePath "$practiceName's daily image files" $ftps["tiff_path"]
|
|
}
|
|
|
|
if ($ftps.Count -eq 0) {
|
|
if (Test-Path $file) {
|
|
Remove-Item $file -Force
|
|
Write-Host ""
|
|
Write-Host "Nothing set - removed $practiceName's practice.json." -ForegroundColor Gray
|
|
} else {
|
|
Write-Host ""
|
|
Write-Host "Nothing set for $practiceName." -ForegroundColor Gray
|
|
}
|
|
return
|
|
}
|
|
|
|
$out = [ordered]@{
|
|
"_note" = "Settings for $practiceName only. Anything not listed comes from ..\..\entity.json."
|
|
ftps = [PSCustomObject]$ftps
|
|
}
|
|
([PSCustomObject]$out) | ConvertTo-Json -Depth 10 | Set-Content -Path $file -Encoding UTF8
|
|
Write-Host ""
|
|
Write-Host "Saved $file" -ForegroundColor Green
|
|
}
|
|
|
|
function Show-PracticeLogins($entityName, $entityConfig) {
|
|
$practices = Get-PracticeNames $entityName
|
|
if ($practices.Count -eq 0) { return }
|
|
Write-Host ""
|
|
Write-Host " Practices" -ForegroundColor White
|
|
foreach ($p in $practices) {
|
|
$merged = Get-MergedPracticeConfig $entityName $p $entityConfig
|
|
$own = Test-Path (Join-Path (Join-Path (Join-Path (Join-Path $entitiesDir $entityName) "Export1") $p) "practice.json")
|
|
$tag = if ($own) { "own settings" } else { "uses $entityName's" }
|
|
Write-Host (" {0,-12} {1,-18} login: {2}" -f $p, $tag, (Show-Value $merged.ftps.username))
|
|
}
|
|
}
|
|
|
|
function Edit-Entity($entityName) {
|
|
$config = Get-EntityConfig $entityName
|
|
if (-not $config) {
|
|
Write-Host "No entity.json found for $entityName." -ForegroundColor Red
|
|
return
|
|
}
|
|
|
|
# A hand-written entity.json may be missing whole sections.
|
|
foreach ($section in @("ftps", "sftp")) {
|
|
if (-not $config.$section) {
|
|
Set-Field $config $section ([PSCustomObject]@{})
|
|
}
|
|
}
|
|
|
|
while ($true) {
|
|
Write-Header "$entityName - current settings"
|
|
Show-Settings $config
|
|
Show-PracticeLogins $entityName $config
|
|
|
|
$choice = Read-Choice "What do you want to change?" @(
|
|
"The daily-upload server (address, protocol, login)",
|
|
"Where the daily image files go on the server",
|
|
"Where PT STMT files from the dated folder go",
|
|
"Patient files (server and folder)",
|
|
"Give one practice its own login or folder",
|
|
"Add practice folders",
|
|
"Nothing - save and go back"
|
|
) 7
|
|
|
|
switch ($choice) {
|
|
1 {
|
|
$d = Read-ServerDetails "the daily files" $config.ftps
|
|
Set-Field $config.ftps "host" $d.host
|
|
Set-Field $config.ftps "protocol" $d.protocol
|
|
Set-Field $config.ftps "username" $d.username
|
|
Set-Field $config.ftps "password" $d.password
|
|
}
|
|
2 {
|
|
Set-Field $config.ftps "tiff_path" (Read-RemotePath "the daily image files" $config.ftps.tiff_path)
|
|
}
|
|
3 {
|
|
Write-Host ""
|
|
if (Read-YesNo "Do PT STMT files show up in the dated folders?" "n") {
|
|
Set-Field $config.ftps "pdf_path" (Read-RemotePath "the PT STMT files" $config.ftps.pdf_path)
|
|
} else {
|
|
Set-Field $config.ftps "pdf_path" $null
|
|
Write-Host " PT STMT files from the dated folder will be ignored." -ForegroundColor Gray
|
|
}
|
|
}
|
|
4 {
|
|
Write-Host ""
|
|
if (Read-YesNo "Does $entityName send patient files?" "n") {
|
|
$same = Read-Choice "Which server do the patient files go to?" @(
|
|
"The same server as the daily files ($($config.ftps.host))",
|
|
"A different server"
|
|
) 1
|
|
if ($same -eq 1) {
|
|
Set-Field $config.sftp "host" $config.ftps.host
|
|
Set-Field $config.sftp "protocol" $config.ftps.protocol
|
|
Set-Field $config.sftp "username" $config.ftps.username
|
|
Set-Field $config.sftp "password" $config.ftps.password
|
|
} else {
|
|
$p = Read-ServerDetails "the patient files" $config.sftp
|
|
Set-Field $config.sftp "host" $p.host
|
|
Set-Field $config.sftp "protocol" $p.protocol
|
|
Set-Field $config.sftp "username" $p.username
|
|
Set-Field $config.sftp "password" $p.password
|
|
}
|
|
Set-Field $config.sftp "patient_path" (Read-RemotePath "the patient .txt/.tif files" $config.sftp.patient_path)
|
|
Set-Field $config "workflow" "insurance+patient"
|
|
} else {
|
|
Set-Field $config "workflow" "insurance"
|
|
Write-Host " Patient files turned off for $entityName." -ForegroundColor Gray
|
|
}
|
|
}
|
|
5 { Set-PracticeLogin $entityName $config }
|
|
6 { Add-Practices $entityName }
|
|
7 {
|
|
$file = Save-EntityConfig $entityName $config
|
|
Copy-RunBat $entityName
|
|
Write-Host ""
|
|
Write-Host "Saved $file" -ForegroundColor Green
|
|
return
|
|
}
|
|
}
|
|
|
|
Save-EntityConfig $entityName $config | Out-Null
|
|
}
|
|
}
|
|
|
|
function Set-Field($object, $name, $value) {
|
|
$object | Add-Member -NotePropertyName $name -NotePropertyValue $value -Force
|
|
}
|
|
|
|
function Show-Settings($config) {
|
|
Write-Host ""
|
|
Write-Host " Daily upload" -ForegroundColor White
|
|
Write-Host " Server : $(Show-Value $config.ftps.host) over $(Show-Protocol $config.ftps 'ftp')"
|
|
Write-Host " Login : $(Show-Value $config.ftps.username) / $(Show-Password $config.ftps.password)"
|
|
Write-Host " Images to : $(Show-Value $config.ftps.tiff_path)"
|
|
Write-Host " PT STMT to: $(Show-Value $config.ftps.pdf_path)"
|
|
Write-Host ""
|
|
Write-Host " Patient files" -ForegroundColor White
|
|
if ($config.workflow -eq "insurance+patient") {
|
|
Write-Host " Server : $(Show-Value $config.sftp.host) over $(Show-Protocol $config.sftp 'sftp')"
|
|
Write-Host " Login : $(Show-Value $config.sftp.username) / $(Show-Password $config.sftp.password)"
|
|
Write-Host " Files to : $(Show-Value $config.sftp.patient_path)"
|
|
} else {
|
|
Write-Host " Not used for this entity." -ForegroundColor Gray
|
|
}
|
|
}
|
|
|
|
# Reports the protocol the upload will actually use, so a legacy config that
|
|
# only carries an "ftps://" prefix on the host doesn't read as unset.
|
|
function Show-Protocol($conn, $fallback) {
|
|
if ([string]::IsNullOrWhiteSpace([string]$conn.host)) { return "(not set)" }
|
|
$protocol = (Get-ConnProtocol $conn $fallback).ToUpper()
|
|
if ($conn.protocol) { return $protocol }
|
|
return "$protocol (assumed)"
|
|
}
|
|
|
|
function Show-Value($value) {
|
|
if ([string]::IsNullOrWhiteSpace([string]$value)) { return "(not set)" }
|
|
return [string]$value
|
|
}
|
|
|
|
function Show-Password($value) {
|
|
if ([string]::IsNullOrWhiteSpace([string]$value)) { return "(not set)" }
|
|
return "*" * ([string]$value).Length
|
|
}
|
|
|
|
# -- Connection test ----------------------------------------------------------
|
|
|
|
# Works out the protocol exactly the way engine.ps1 does, so a passing test
|
|
# here means the real run connects the same way.
|
|
function Get-ConnProtocol($conn, $fallback) {
|
|
if ($conn.protocol) {
|
|
$p = ([string]$conn.protocol).ToLower().Trim()
|
|
if ($p -in @("sftp", "ftps", "ftp")) { return $p }
|
|
}
|
|
if (([string]$conn.host) -imatch '^(\w+)://') {
|
|
$scheme = $Matches[1].ToLower()
|
|
if ($scheme -in @("sftp", "ftps", "ftp")) { return $scheme }
|
|
}
|
|
if ($conn.tls -eq $true) { return "ftps" }
|
|
return $fallback
|
|
}
|
|
|
|
function Get-SessionOptions($conn, $fallback) {
|
|
$opts = New-Object WinSCP.SessionOptions
|
|
# Older configs kept the protocol as an "sftp://" prefix on the host.
|
|
$opts.HostName = (([string]$conn.host) -replace '^\w+://', '').Split('/')[0].Trim()
|
|
$opts.UserName = $conn.username
|
|
$opts.Password = $conn.password
|
|
|
|
$protocol = Get-ConnProtocol $conn $fallback
|
|
if ($protocol -eq "sftp") {
|
|
$opts.Protocol = [WinSCP.Protocol]::Sftp
|
|
$opts.PortNumber = if ($conn.port) { [int]$conn.port } else { 22 }
|
|
$opts.GiveUpSecurityAndAcceptAnySshHostKey = $true
|
|
} else {
|
|
$opts.Protocol = [WinSCP.Protocol]::Ftp
|
|
$opts.PortNumber = if ($conn.port) { [int]$conn.port } else { 21 }
|
|
if ($protocol -eq "ftps") {
|
|
$opts.FtpSecure = [WinSCP.FtpSecure]::Explicit
|
|
$opts.GiveUpSecurityAndAcceptAnyTlsHostCertificate = $true
|
|
} else {
|
|
$opts.FtpSecure = [WinSCP.FtpSecure]::None
|
|
}
|
|
}
|
|
return $opts
|
|
}
|
|
|
|
function Test-ServerConnection($label, $conn, $pathTemplate, $practices, $fallback) {
|
|
Write-Host ""
|
|
Write-Host " $label" -ForegroundColor White
|
|
|
|
if ([string]::IsNullOrWhiteSpace([string]$conn.host)) {
|
|
Write-Host " Not set up - nothing to test." -ForegroundColor Gray
|
|
return
|
|
}
|
|
|
|
$opts = Get-SessionOptions $conn $fallback
|
|
$protocol = (Get-ConnProtocol $conn $fallback).ToUpper()
|
|
Write-Host " Connecting to $($opts.HostName) over $protocol as $($conn.username) ... " -NoNewline
|
|
|
|
$session = New-Object WinSCP.Session
|
|
try {
|
|
$session.Open($opts)
|
|
Write-Host "OK" -ForegroundColor Green
|
|
} catch {
|
|
Write-Host "FAILED" -ForegroundColor Red
|
|
Write-Host " $($_.Exception.Message)" -ForegroundColor Red
|
|
Write-Host ""
|
|
Write-Host " $(Get-ConnectionHint $_.Exception.Message)" -ForegroundColor Yellow
|
|
$session.Dispose()
|
|
return
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace([string]$pathTemplate)) {
|
|
Write-Host " No folder path set yet - skipping the folder check." -ForegroundColor Gray
|
|
$session.Dispose()
|
|
return
|
|
}
|
|
|
|
try {
|
|
foreach ($practice in $practices) {
|
|
$remote = $pathTemplate -replace '\{practice\}', $practice
|
|
Write-Host " $remote ... " -NoNewline
|
|
if ($session.FileExists($remote)) {
|
|
Write-Host "found" -ForegroundColor Green
|
|
} else {
|
|
Write-Host "NOT THERE" -ForegroundColor Red
|
|
}
|
|
}
|
|
} catch {
|
|
Write-Host "could not check - $($_.Exception.Message)" -ForegroundColor Yellow
|
|
} finally {
|
|
$session.Dispose()
|
|
}
|
|
}
|
|
|
|
function Get-ConnectionHint($message) {
|
|
if ($message -imatch "cannot connect to an SFTP server using an FTP") {
|
|
return "This server wants SFTP. Run setup again and pick SFTP for this server."
|
|
}
|
|
if ($message -imatch "cannot connect to an FTP server using an SFTP|expected SSH") {
|
|
return "This server wants FTP or FTPS. Run setup again and pick FTPS for this server."
|
|
}
|
|
if ($message -imatch "authentication|password|login incorrect|530") {
|
|
return "The username or password was rejected. Run setup again and retype the login."
|
|
}
|
|
if ($message -imatch "host does not exist|no such host|could not resolve") {
|
|
return "That server address could not be found. Check it for a typo."
|
|
}
|
|
if ($message -imatch "timed out|timeout|refused") {
|
|
return "No answer from the server. Check the address, or ask IT whether the firewall allows it."
|
|
}
|
|
return "Write down this message - it says what the server objected to."
|
|
}
|
|
|
|
function Test-Entity($entityName) {
|
|
Write-Header "Testing $entityName"
|
|
|
|
if (-not (Test-Path $winscpDll)) {
|
|
Write-Host "WinSCP is not installed yet - run setup.bat first." -ForegroundColor Red
|
|
return
|
|
}
|
|
Get-ChildItem -Path (Join-Path $scriptRoot "WinSCP") -Recurse -File |
|
|
Unblock-File -ErrorAction SilentlyContinue
|
|
try {
|
|
Add-Type -Path $winscpDll
|
|
} catch {
|
|
Write-Host "Could not load WinSCP - $($_.Exception.Message)" -ForegroundColor Red
|
|
Write-Host "Try deleting the WinSCP folder and running setup.bat again." -ForegroundColor Yellow
|
|
return
|
|
}
|
|
|
|
$config = Get-EntityConfig $entityName
|
|
if (-not $config) {
|
|
Write-Host "No entity.json found for $entityName." -ForegroundColor Red
|
|
return
|
|
}
|
|
|
|
$export1 = Join-Path (Join-Path $entitiesDir $entityName) "Export1"
|
|
$practices = @()
|
|
if (Test-Path $export1) {
|
|
$practices = @(Get-ChildItem -Path $export1 -Directory | ForEach-Object { $_.Name })
|
|
}
|
|
if ($practices.Count -eq 0) {
|
|
Write-Host "No practice folders yet - testing the login only." -ForegroundColor Gray
|
|
}
|
|
|
|
Test-ServerConnection "Daily upload" $config.ftps $config.ftps.tiff_path $practices "ftp"
|
|
if ($config.ftps.pdf_path) {
|
|
Test-ServerConnection "PT STMT folders" $config.ftps $config.ftps.pdf_path $practices "ftp"
|
|
}
|
|
if ($config.workflow -eq "insurance+patient") {
|
|
Test-ServerConnection "Patient files" $config.sftp $config.sftp.patient_path $practices "sftp"
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host "A folder marked NOT THERE means the upload will fail for that" -ForegroundColor Yellow
|
|
Write-Host "practice. Either the path is wrong, or the partner has not made" -ForegroundColor Yellow
|
|
Write-Host "the folder yet - check the path first, then ask them." -ForegroundColor Yellow
|
|
}
|
|
|
|
# -- Main menu ----------------------------------------------------------------
|
|
|
|
function Select-Entity($purpose) {
|
|
$names = Get-EntityNames
|
|
if ($names.Count -eq 0) {
|
|
Write-Host ""
|
|
Write-Host "No entities set up yet - pick 'Set up a new entity' first." -ForegroundColor Yellow
|
|
return $null
|
|
}
|
|
$choice = Read-Choice "Which entity do you want to $purpose" $names 1
|
|
return $names[$choice - 1]
|
|
}
|
|
|
|
Write-Header "Melissa Uploader - Setup"
|
|
Write-Host ""
|
|
Write-Host "This asks a few questions and writes the settings files for you." -ForegroundColor Gray
|
|
Write-Host "You never have to open one by hand." -ForegroundColor Gray
|
|
|
|
while ($true) {
|
|
$names = Get-EntityNames
|
|
Write-Host ""
|
|
if ($names.Count -gt 0) {
|
|
Write-Host "Entities set up so far: $($names -join ', ')" -ForegroundColor Gray
|
|
} else {
|
|
Write-Host "No entities set up yet." -ForegroundColor Gray
|
|
}
|
|
|
|
$choice = Read-Choice "What do you want to do?" @(
|
|
"Set up a new entity",
|
|
"Change an entity's settings",
|
|
"Test an entity's connection and folders",
|
|
"Quit"
|
|
) 4
|
|
|
|
switch ($choice) {
|
|
1 { New-Entity }
|
|
2 { $name = Select-Entity "change"; if ($name) { Edit-Entity $name } }
|
|
3 { $name = Select-Entity "test"; if ($name) { Test-Entity $name } }
|
|
4 {
|
|
Write-Host ""
|
|
Write-Host "Done. Double-click run-all.bat when you are ready to upload." -ForegroundColor Green
|
|
Write-Host ""
|
|
exit 0
|
|
}
|
|
}
|
|
}
|