Public/Connect-PagerArchive.ps1

function Connect-PagerArchive {
    <#
    .SYNOPSIS
    Signs in to a PagerArchive server.

    .DESCRIPTION
    Opens your browser at PagerArchive's normal sign-in (email and
    password). When you've signed in, the browser says you can close it,
    and the module is connected. On a machine without a browser, use
    -UseDeviceCode: the module shows a code to enter on any device.

    The sign-in is saved, so new PowerShell windows use it without signing
    in again:

    - On Windows, in a file in your profile, encrypted with Windows' data
      protection (DPAPI), so only your Windows account on this machine can
      read it. Nothing needs installing.
    - On Linux and macOS, in your default SecretManagement vault. With no
      vault registered, the sign-in lasts for this session only, and a
      warning says how to set one up.
    - Anywhere, -Vault names a SecretManagement vault to use instead.

    The module renews its access automatically. A sign-in that isn't used
    for 30 days ends. If one is already saved for the server, it's used
    without opening the browser.

    PowerShell access is for admins (R62): for anyone else the sign-in is
    refused and not saved.

    .EXAMPLE
    Connect-PagerArchive -Uri https://pager.example.org

    Uri Name Email Role SavedIn
    --- ---- ----- ---- -------
    https://pager.example.org Alex Example alex@example.org admin DPAPI file

    .EXAMPLE
    Connect-PagerArchive -Uri https://pager.example.org -UseDeviceCode

    WARNING: To sign in, open https://auth.example.org/device on any device and enter the code ABCD-EFGH

    .EXAMPLE
    Connect-PagerArchive -Uri http://192.168.1.20:8095 -AllowUnencryptedAuthentication

    Signs in to a test copy on your own network over plain HTTP, with a warning.

    .INPUTS
    System.Uri. Objects with a Uri property, such as a PagerArchive.Connection.

    .OUTPUTS
    PagerArchive.Connection. The server (Uri), who you're signed in as
    (Name, Email, Role), where the sign-in is saved (SavedIn) and when the
    current access token expires (ExpiresAt).

    .LINK
    Disconnect-PagerArchive

    .LINK
    about_PagerArchive
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingAllowUnencryptedAuthentication', '', Justification = 'Only when the user passes -AllowUnencryptedAuthentication for a LAN test copy, with a warning (guidelines section 9).')]
    [CmdletBinding()]
    [OutputType('PagerArchive.Connection')]
    param(
        # The PagerArchive app's URL, e.g. https://pager.example.org. No
        # default; no wildcards.
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [uri] $Uri,

        # Sign in with a code entered on any device, for a machine with no
        # browser. Off by default.
        [switch] $UseDeviceCode,

        # The SecretManagement vault to save the sign-in in. Default: a
        # DPAPI-encrypted file on Windows, the default vault on Linux and
        # macOS. Needs the Microsoft.PowerShell.SecretManagement module. No
        # wildcards.
        [ValidateNotNullOrEmpty()]
        [string] $Vault,

        # Allow a plain-HTTP URL (a test copy on your own network), which
        # sends the sign-in unencrypted. Off by default.
        [switch] $AllowUnencryptedAuthentication
    )
    process {
        try {
            $origin = Get-PaOrigin -Uri $Uri -AllowUnencryptedAuthentication:$AllowUnencryptedAuthentication
            if ($origin.StartsWith('http:')) {
                Write-Warning -Message "$origin uses plain HTTP: your sign-in is sent unencrypted."
            }
            $settings = Get-PaServerSetting -Origin $origin -AllowUnencryptedAuthentication:$AllowUnencryptedAuthentication
            $store = Resolve-PaStore -Vault $Vault

            # A saved sign-in for this server: use it, without the browser.
            $saved = Read-PaSignIn -Origin $origin
            if ($saved -and (-not $Vault -or ($saved.store -eq 'vault' -and $saved.vault -eq $Vault))) {
                try {
                    $connection = Open-PaSavedSignIn -Record $saved
                    $connection.User = Invoke-PaApi -Method GET -Path '/api/v1/me'
                    return New-PaConnectionObject -Connection $connection
                } catch {
                    $script:Connection = $null
                    # Refused for who you are: signing in again won't help.
                    if ($_.Exception.Data['PagerArchive.Code'] -in @('powershell_admins_only', 'deactivated', 'not_signed_in')) {
                        throw
                    }
                    Write-Verbose -Message "The saved sign-in didn't work ($($_.Exception.Data['PagerArchive.Code'])); signing in again."
                }
            }
            if ($saved) {
                Remove-PaSignIn -Origin $origin -WhatIf:$false -Confirm:$false
            }

            $reply = if ($UseDeviceCode) { Invoke-PaDeviceSignIn -Settings $settings } else { Invoke-PaBrowserSignIn -Settings $settings }
            $connection = @{
                Uri              = $origin
                Issuer           = $settings.Issuer
                ClientId         = $settings.ClientId
                Scope            = $settings.Scope
                Endpoints        = $settings.Endpoints
                AccessToken      = $null
                ExpiresAt        = [datetimeoffset]::MinValue
                RefreshToken     = $null
                Store            = $store.Store
                Vault            = $store.Vault
                Version          = $null
                AllowUnencrypted = [bool]$AllowUnencryptedAuthentication
                User             = $null
            }
            Import-PaTokenReply -Connection $connection -Reply $reply
            if ($reply['id_token']) {
                $reply['id_token'].Dispose()
            }

            # The API decides: only active admins (R62).
            $script:Connection = $connection
            try {
                $connection.User = Invoke-PaApi -Method GET -Path '/api/v1/me'
            } catch {
                $script:Connection = $null
                $null = Revoke-PaToken -Settings $connection -Token $connection.RefreshToken
                throw
            }
            if ($connection.Store -eq 'session') {
                Write-Warning -Message ('No SecretManagement vault is registered, so this sign-in lasts until this PowerShell session ends. ' +
                    'To stay signed in, register a vault: Install-PSResource -Name Microsoft.PowerShell.SecretManagement, Microsoft.PowerShell.SecretStore; ' +
                    'Register-SecretVault -Name SecretStore -ModuleName Microsoft.PowerShell.SecretStore -DefaultVault')
            } else {
                Save-PaSignIn -Connection $connection
                $connection.RefreshToken = $null
            }
            New-PaConnectionObject -Connection $connection
        } catch {
            $script:Connection = $null
            $PSCmdlet.ThrowTerminatingError((Copy-PaErrorRecord -ErrorRecord $_ -TargetObject $Uri))
        }
    }
}