diff --git a/PowerShell/PolarisClient.ps1 b/PowerShell/PolarisClient.ps1 new file mode 100644 index 0000000..b60065f --- /dev/null +++ b/PowerShell/PolarisClient.ps1 @@ -0,0 +1,87 @@ +<# + Minimal client for the Polaris Leap (PAPI) Notices endpoint. + PowerShell port of ../polaris_client.py -- see that file's docstrings and + ../README.md ("Notes on the API") for the non-obvious quirks this encodes. +#> + +class PolarisClient { + [string]$AuthUrl + [string]$BaseUrl + [string]$SiteDomain + [string]$AccessToken + [string]$AccessSecret + + PolarisClient( + [string]$Server, + [string]$SiteDomain, + [string]$OrganizationId, + [string]$WorkstationId, + [string]$Language, + [string]$ProductId + ) { + # authentication/staffuser is the one route that omits siteDomain/orgId/workstationId -- + # those describe a *logged-on* session, and you're not logged on yet at this point. + $this.AuthUrl = "https://$Server/Polaris.ApplicationServices/api/v1/$Language/$ProductId/authentication/staffuser" + $this.BaseUrl = "https://$Server/Polaris.ApplicationServices/api/v1/$Language/$ProductId/$SiteDomain/$OrganizationId/$WorkstationId" + $this.SiteDomain = $SiteDomain + } + + # Exchange a staff username/password for an AccessToken/AccessSecret. + # Username must include the domain, e.g. MYLIB\Aladdin. + [void] Authenticate([string]$Username, [string]$Password) { + $pair = "${Username}:${Password}" + $basicAuth = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($pair)) + $headers = @{ + Authorization = "Basic $basicAuth" + Accept = "application/json" + } + + try { + $response = Invoke-RestMethod -Uri $this.AuthUrl -Method Post -Headers $headers + } catch { + $statusCode = $null + if ($_.Exception.Response) { $statusCode = [int]$_.Exception.Response.StatusCode } + if ($statusCode -eq 401) { + throw "Authentication failed: check username/password." + } + throw "Authentication failed ($statusCode): $($_.ErrorDetails.Message)" + } + + $this.AccessToken = $response.AccessToken + $this.AccessSecret = $response.AccessSecret + } + + hidden [string] AuthHeader() { + if (-not $this.AccessToken -or -not $this.AccessSecret) { + throw "Not authenticated yet -- call Authenticate() first." + } + return "PAS $($this.SiteDomain):$($this.AccessToken):$($this.AccessSecret)" + } + + # POST notices for the given notice type to the given org IDs. + # DeliveryOption: only 1 (Mail) is currently supported by the API. + [bool] PostNotices([int]$NoticeType, [int[]]$OrganizationIds, [int]$DeliveryOption) { + $orgIdsCsv = ($OrganizationIds -join ",") + $uri = "$($this.BaseUrl)/notices/post?noticeType=$NoticeType&deliveryOption=$DeliveryOption" + $headers = @{ + Authorization = $this.AuthHeader() + Accept = "application/json" + } + # Polaris expects the org-id CSV as a JSON-encoded string literal, matching + # json.dumps(org_ids_csv) on the Python side -- not a raw/unquoted string. + $bodyJson = $orgIdsCsv | ConvertTo-Json + + try { + $result = Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -ContentType "application/json" -Body $bodyJson + } catch { + $statusCode = $null + if ($_.Exception.Response) { $statusCode = [int]$_.Exception.Response.StatusCode } + throw "Notice post failed ($statusCode): $($_.ErrorDetails.Message)" + } + + if ($result -ne $true) { + throw "Polaris reported failure (returned $result) for noticeType=$NoticeType, organizationIds=$orgIdsCsv" + } + return $result + } +} diff --git a/PowerShell/README.md b/PowerShell/README.md new file mode 100644 index 0000000..2d6a8f5 --- /dev/null +++ b/PowerShell/README.md @@ -0,0 +1,33 @@ +# PowerShell version + +Same functionality as the Python scripts in the repo root, ported for running on Windows +Servers via Task Scheduler without needing a Python install. + +## Setup + +No dependencies to install — `Invoke-RestMethod` is built into Windows PowerShell 5.1+. + +Uses the same `.env` file as the Python version (see `../.env.example`); by default it looks +for `..\.env` relative to this script. Pass `-EnvFile` to point elsewhere, e.g. if you'd rather +keep credentials outside the repo checkout entirely. + +## Usage + +```powershell +.\post_notices.ps1 -NoticeType 2 -OrgIds 3,4,5,6,7,8 +``` + +- `-NoticeType`: the Notification Type ID to post. +- `-OrgIds`: comma-separated Organization IDs to post notices for. +- `-DeliveryOption`: optional, defaults to `1` (Mail) — currently the only value Polaris accepts. + +Same caveat as the Python version: every run sends real notices to real patrons, and there's no +dry-run mode. + +## Task Scheduler + +Action: `powershell.exe` +Arguments: `-NoProfile -ExecutionPolicy Bypass -File "C:\path\to\PowerShell\post_notices.ps1" -NoticeType 2 -OrgIds 3,4,5,6,7,8` + +If your Windows PowerShell execution policy blocks unsigned scripts by default, `-ExecutionPolicy +Bypass` on the scheduled task action avoids having to change the machine-wide policy. diff --git a/PowerShell/post_notices.ps1 b/PowerShell/post_notices.ps1 new file mode 100644 index 0000000..d9b2478 --- /dev/null +++ b/PowerShell/post_notices.ps1 @@ -0,0 +1,94 @@ +<# +.SYNOPSIS + Posts notices to Polaris via the Leap (PAPI) API. PowerShell port of ../post_notices.py. + +.EXAMPLE + .\post_notices.ps1 -NoticeType 2 -OrgIds 3,4,5,6,7,8 +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [int]$NoticeType, + + [Parameter(Mandatory = $true)] + [string]$OrgIds, + + [int]$DeliveryOption = 1, + + # Defaults to a .env file next to the repo root (one level up from this script), + # matching where the Python version expects it. + [string]$EnvFile = (Join-Path $PSScriptRoot "..\.env") +) + +$ErrorActionPreference = "Stop" + +. (Join-Path $PSScriptRoot "PolarisClient.ps1") + +function Import-DotEnv { + param([string]$Path) + if (-not (Test-Path -LiteralPath $Path)) { return } + + foreach ($line in Get-Content -LiteralPath $Path) { + $trimmed = $line.Trim() + if (-not $trimmed -or $trimmed.StartsWith('#')) { continue } + + $idx = $trimmed.IndexOf('=') + if ($idx -lt 1) { continue } + + $key = $trimmed.Substring(0, $idx).Trim() + $value = $trimmed.Substring($idx + 1).Trim() + if ($value.Length -ge 2 -and ( + ($value[0] -eq '"' -and $value[-1] -eq '"') -or + ($value[0] -eq "'" -and $value[-1] -eq "'") + )) { + $value = $value.Substring(1, $value.Length - 2) + } + + # Don't clobber a value already set in the environment (e.g. via Task Scheduler). + if (-not (Get-Item -Path "Env:$key" -ErrorAction SilentlyContinue).Value) { + Set-Item -Path "Env:$key" -Value $value + } + } +} + +Import-DotEnv -Path $EnvFile + +$requiredEnv = @( + "POLARIS_SERVER", + "POLARIS_SITE_DOMAIN", + "POLARIS_ORGANIZATION_ID", + "POLARIS_WORKSTATION_ID", + "POLARIS_USERNAME", + "POLARIS_PASSWORD" +) +$missing = $requiredEnv | Where-Object { [string]::IsNullOrWhiteSpace((Get-Item -Path "Env:$_" -ErrorAction SilentlyContinue).Value) } +if ($missing) { + Write-Error "Missing required .env values: $($missing -join ', ')" + Write-Error "Copy .env.example to .env and fill it in (or set these as environment variables)." + exit 1 +} + +$orgIdList = $OrgIds -split "," | Where-Object { $_.Trim() } | ForEach-Object { [int]$_.Trim() } + +$language = if ($env:POLARIS_LANGUAGE) { $env:POLARIS_LANGUAGE } else { "eng" } +$productId = if ($env:POLARIS_PRODUCT_ID) { $env:POLARIS_PRODUCT_ID } else { "7" } + +$client = [PolarisClient]::new( + $env:POLARIS_SERVER, + $env:POLARIS_SITE_DOMAIN, + $env:POLARIS_ORGANIZATION_ID, + $env:POLARIS_WORKSTATION_ID, + $language, + $productId +) + +try { + $client.Authenticate($env:POLARIS_USERNAME, $env:POLARIS_PASSWORD) + $client.PostNotices($NoticeType, $orgIdList, $DeliveryOption) | Out-Null +} catch { + Write-Error "Error: $($_.Exception.Message)" + exit 1 +} + +Write-Output "Notices posted successfully: noticeType=$NoticeType, orgIds=$($orgIdList -join ','), deliveryOption=$DeliveryOption"