Public/User/New-VergeAPIKey.ps1

function New-VergeAPIKey {
    <#
    .SYNOPSIS
        Creates a new API key for a user in VergeOS.

    .DESCRIPTION
        New-VergeAPIKey creates a new API key for a specified user.
        The API key secret is only displayed once at creation time.

    .PARAMETER User
        The username or user object to create the API key for.

    .PARAMETER UserKey
        The unique key (ID) of the user to create the API key for.

    .PARAMETER Name
        The name for the API key. Must be unique per user.

    .PARAMETER Description
        An optional description for the API key.

    .PARAMETER ExpiresIn
        The duration until the API key expires (e.g., "30d", "1y", "never").
        Supported units: d (days), w (weeks), m (months), y (years).
        Default is "never" (no expiration).

    .PARAMETER Expires
        A specific DateTime when the API key should expire.

    .PARAMETER IPAllowList
        Array of IP addresses or CIDR ranges that are allowed to use this key.

    .PARAMETER IPDenyList
        Array of IP addresses or CIDR ranges that are denied from using this key.

    .PARAMETER PassThru
        Return the created API key object (without the secret).

    .PARAMETER Server
        The VergeOS connection to use. Defaults to the current default connection.

    .EXAMPLE
        New-VergeAPIKey -User "admin" -Name "automation-key"

        Creates a new API key for the admin user.

    .EXAMPLE
        New-VergeAPIKey -User "apiuser" -Name "ci-key" -ExpiresIn "90d" -Description "CI/CD automation"

        Creates an API key that expires in 90 days.

    .EXAMPLE
        Get-VergeUser -Name "apiuser" | New-VergeAPIKey -Name "restricted-key" -IPAllowList @("10.0.0.0/8")

        Creates an API key restricted to a specific IP range.

    .OUTPUTS
        PSCustomObject containing the API key and secret (secret only shown once)

    .NOTES
        IMPORTANT: The API key secret is only displayed at creation time.
        Store it securely as it cannot be retrieved later.
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium', DefaultParameterSetName = 'ByUser')]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory, Position = 0, ParameterSetName = 'ByUser', ValueFromPipeline)]
        [object]$User,

        [Parameter(Mandatory, ParameterSetName = 'ByUserKey')]
        [int]$UserKey,

        [Parameter(Mandatory, Position = 1)]
        [ValidateNotNullOrEmpty()]
        [ValidateLength(1, 128)]
        [string]$Name,

        [Parameter()]
        [ValidateLength(0, 2048)]
        [string]$Description,

        [Parameter()]
        [ValidatePattern('^(never|\d+[dwmy])$', ErrorMessage = "ExpiresIn must be 'never' or a duration like '30d', '1w', '3m', '1y'")]
        [string]$ExpiresIn = 'never',

        [Parameter()]
        [datetime]$Expires,

        [Parameter()]
        [string[]]$IPAllowList,

        [Parameter()]
        [string[]]$IPDenyList,

        [Parameter()]
        [switch]$PassThru,

        [Parameter()]
        [object]$Server
    )

    begin {
        # Resolve connection
        if (-not $Server) {
            $Server = $script:DefaultConnection
        }
        if (-not $Server) {
            throw [System.InvalidOperationException]::new(
                'Not connected to VergeOS. Use Connect-VergeOS to establish a connection.'
            )
        }
    }

    process {
        # Resolve user key
        $resolvedUserKey = $null
        $userName = $null

        if ($User) {
            if ($User -is [PSCustomObject] -and $User.PSObject.TypeNames -contains 'Verge.User') {
                $resolvedUserKey = $User.Key
                $userName = $User.Name
            }
            elseif ($User -is [int]) {
                $resolvedUserKey = $User
                $existingUser = Get-VergeUser -Key $User -Server $Server -ErrorAction SilentlyContinue
                $userName = if ($existingUser) { $existingUser.Name } else { "User $User" }
            }
            elseif ($User -is [string]) {
                $existingUser = Get-VergeUser -Name $User -Server $Server -ErrorAction SilentlyContinue
                if (-not $existingUser) {
                    Write-Error -Message "User not found: $User" -ErrorId 'UserNotFound' -Category ObjectNotFound
                    return
                }
                $resolvedUserKey = $existingUser.Key
                $userName = $User
            }
        }
        elseif ($UserKey) {
            $resolvedUserKey = $UserKey
            $existingUser = Get-VergeUser -Key $UserKey -Server $Server -ErrorAction SilentlyContinue
            $userName = if ($existingUser) { $existingUser.Name } else { "User $UserKey" }
        }

        if (-not $resolvedUserKey) {
            Write-Error -Message "Could not resolve user" -ErrorId 'UserNotFound' -Category ObjectNotFound
            return
        }

        # Build request body
        $body = @{
            user = $resolvedUserKey
            name = $Name
        }

        if ($Description) {
            $body['description'] = $Description
        }

        # Handle expiration
        if ($PSBoundParameters.ContainsKey('Expires')) {
            $body['expires'] = [int][DateTimeOffset]::new($Expires).ToUnixTimeSeconds()
            $body['expires_type'] = 'date'
        }
        elseif ($ExpiresIn -eq 'never') {
            $body['expires_type'] = 'never'
        }
        else {
            # Parse duration string
            $match = [regex]::Match($ExpiresIn, '^(\d+)([dwmy])$')
            if ($match.Success) {
                $value = [int]$match.Groups[1].Value
                $unit = $match.Groups[2].Value
                $expirationDate = switch ($unit) {
                    'd' { [datetime]::Now.AddDays($value) }
                    'w' { [datetime]::Now.AddDays($value * 7) }
                    'm' { [datetime]::Now.AddMonths($value) }
                    'y' { [datetime]::Now.AddYears($value) }
                }
                $body['expires'] = [int][DateTimeOffset]::new($expirationDate).ToUnixTimeSeconds()
                $body['expires_type'] = 'date'
            }
        }

        # IP lists
        if ($IPAllowList -and $IPAllowList.Count -gt 0) {
            $body['ip_allow_list'] = $IPAllowList -join ','
        }

        if ($IPDenyList -and $IPDenyList.Count -gt 0) {
            $body['ip_deny_list'] = $IPDenyList -join ','
        }

        if ($PSCmdlet.ShouldProcess("User '$userName'", "Create API Key '$Name'")) {
            try {
                Write-Verbose "Creating API key '$Name' for user '$userName'"
                $response = Invoke-VergeAPI -Method POST -Endpoint 'user_api_keys' -Body $body -Connection $Server

                # Get the created key
                $apiKeyId = $response.'$key'

                # The secret is returned in the response
                $secret = $response.key

                Write-Verbose "API key '$Name' created with Key: $apiKeyId"

                # Output the secret - this is the only time it's visible
                $output = [PSCustomObject]@{
                    PSTypeName = 'Verge.APIKeyCreated'
                    Key        = [int]$apiKeyId
                    Name       = $Name
                    UserName   = $userName
                    Secret     = $secret
                    Message    = 'IMPORTANT: Store this secret securely. It cannot be retrieved again.'
                }

                Write-Output $output

                if ($PassThru) {
                    # Also return the API key object (without secret)
                    Start-Sleep -Milliseconds 500
                    Get-VergeAPIKey -Key $apiKeyId -Server $Server
                }
            }
            catch {
                throw "Failed to create API key '$Name': $($_.Exception.Message)"
            }
        }
    }
}