Private/Invoke-FloRecruitRequest.ps1
|
function Invoke-FloRecruitRequest { <# .SYNOPSIS Central HTTP request handler for FloRecruit API calls. .DESCRIPTION Handles all HTTP requests to the FloRecruit API including session verification, automatic token refresh, rate limiting, error handling, and response parsing. .PARAMETER Endpoint The API endpoint to call (e.g., '/v1/job-applications'). .PARAMETER Method The HTTP method to use (default: GET). .PARAMETER Body The request body as a hashtable (will be converted to JSON). .PARAMETER QueryParameters Query parameters to append to the URL. .PARAMETER ReturnHeaders Return both data and headers for pagination handling. .PARAMETER RawResponse Return the raw response object instead of parsed JSON. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Endpoint, [Parameter()] [ValidateSet('GET', 'POST', 'PUT', 'PATCH', 'DELETE')] [string]$Method = 'GET', [Parameter()] [hashtable]$Body, [Parameter()] [hashtable]$QueryParameters, [Parameter()] [switch]$ReturnHeaders, [Parameter()] [switch]$RawResponse ) # Verify session exists if (-not $Script:FloRecruitSession) { throw "Not connected to FloRecruit. Use Connect-FloRecruit first." } # Check token expiry and refresh if needed if ($Script:FloRecruitSession.TokenExpiry -lt (Get-Date)) { Write-Verbose "Token expired. Refreshing authentication..." Update-FloRecruitToken } # Rate limiting check $windowElapsed = ((Get-Date) - $Script:FloRecruitSession.RateLimitWindowStart).TotalSeconds if ($windowElapsed -ge $Script:RateLimitWindowSeconds) { # New window - reset counter $Script:FloRecruitSession.RequestCount = 0 $Script:FloRecruitSession.RateLimitWindowStart = Get-Date Write-Verbose "Rate limit window reset" } # Check if approaching limit if ($Script:FloRecruitSession.RequestCount -ge ($Script:RateLimitMaxRequests - 10)) { Write-Warning "Approaching rate limit ($($Script:FloRecruitSession.RequestCount)/$Script:RateLimitMaxRequests requests in current window)" Start-Sleep -Milliseconds 500 } # Check if at limit if ($Script:FloRecruitSession.RequestCount -ge $Script:RateLimitMaxRequests) { $sleepSeconds = [math]::Ceiling($Script:RateLimitWindowSeconds - $windowElapsed + 5) Write-Warning "Rate limit reached ($Script:RateLimitMaxRequests requests). Waiting $sleepSeconds seconds for new window..." Start-Sleep -Seconds $sleepSeconds $Script:FloRecruitSession.RequestCount = 0 $Script:FloRecruitSession.RateLimitWindowStart = Get-Date } # Build URL $baseUrl = $Script:FloRecruitSession.BaseUrl $url = "$baseUrl$Endpoint" # Add query parameters if ($QueryParameters -and $QueryParameters.Count -gt 0) { $queryString = ($QueryParameters.GetEnumerator() | ForEach-Object { "$([System.Web.HttpUtility]::UrlEncode($_.Key))=$([System.Web.HttpUtility]::UrlEncode($_.Value))" }) -join '&' $url = "$url`?$queryString" } # Build headers $headers = @{ 'Accept' = 'application/json' 'Cookie' = $Script:FloRecruitSession.AuthToken } # Build request parameters $requestParams = @{ Uri = $url Method = $Method Headers = $headers } # Add body if provided if ($Body) { $requestParams['Body'] = ($Body | ConvertTo-Json -Depth 10) $requestParams['ContentType'] = 'application/json; charset=utf-8' } try { Write-Verbose "$Method $url" # Make the request $response = Invoke-WebRequest @requestParams -ErrorAction Stop # Increment request counter $Script:FloRecruitSession.RequestCount++ # Return raw response if requested if ($RawResponse) { return $response } # Parse JSON response $data = $null if ($response.Content) { try { $data = $response.Content | ConvertFrom-Json } catch { Write-Warning "Failed to parse JSON response: $_" $data = $response.Content } } # Return with headers if requested if ($ReturnHeaders) { return [PSCustomObject]@{ Data = $data Headers = $response.Headers } } return $data } catch { # Handle HTTP errors $statusCode = $null $errorDetails = $null if ($_.Exception.Response) { $statusCode = [int]$_.Exception.Response.StatusCode # Try to read error response body try { $streamReader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream()) $errorDetails = $streamReader.ReadToEnd() $streamReader.Close() } catch { $errorDetails = $_.Exception.Message } } # Convert to standardized error $errorRecord = ConvertTo-FloRecruitError -Exception $_.Exception -ErrorDetails $errorDetails -StatusCode $statusCode # Special handling for 429 (rate limit) if ($statusCode -eq 429) { Write-Warning "Rate limit exceeded (HTTP 429). Consider reducing request frequency." # Check for Retry-After header if ($_.Exception.Response.Headers -and $_.Exception.Response.Headers['Retry-After']) { $retryAfter = $_.Exception.Response.Headers['Retry-After'] Write-Warning "API requests Retry-After: $retryAfter seconds" } } # Special handling for 401 (unauthorized) if ($statusCode -eq 401) { Write-Warning "Authentication failed (HTTP 401). Token may be invalid. Try reconnecting with Connect-FloRecruit." } throw $errorRecord } } function Update-FloRecruitToken { <# .SYNOPSIS Re-authenticates using stored credentials to refresh the session token. .DESCRIPTION Called automatically by Invoke-FloRecruitRequest when the token has expired. Uses stored credentials from the session to re-authenticate. #> [CmdletBinding()] param() if (-not $Script:FloRecruitSession) { throw "No session to refresh" } Write-Verbose "Re-authenticating to refresh token..." try { # Build auth URL $authUrl = $Script:FloRecruitSession.AuthEndpoint # Convert SecureString password back to plain text for authentication $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Script:FloRecruitSession.Password) $plainPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR) # Build auth body $authBody = @{ email = $Script:FloRecruitSession.Email password = $plainPassword } # Add MFA if present if ($Script:FloRecruitSession.MFASecret) { $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Script:FloRecruitSession.MFASecret) $plainMFA = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR) $authBody['mfa_secret'] = $plainMFA } # Make auth request $response = Invoke-WebRequest -Uri $authUrl -Method POST -Body ($authBody | ConvertTo-Json) -ContentType 'application/json' -SessionVariable webSession # Extract token from cookie $authCookie = $null $cookieString = $null # Try to get cookie from web session first if ($webSession -and $webSession.Cookies) { $authCookie = $webSession.Cookies.GetCookies($authUrl) | Where-Object { $_.Name -match 'auth|token|session' } | Select-Object -First 1 } if ($authCookie) { $cookieString = "$($authCookie.Name)=$($authCookie.Value)" } else { # Fallback: Try to extract from Set-Cookie header if ($response.Headers['Set-Cookie']) { $setCookieHeader = $response.Headers['Set-Cookie'] if ($setCookieHeader -match '([^=]+)=([^;]+)') { $cookieName = $Matches[1] $cookieValue = $Matches[2] $cookieString = "$cookieName=$cookieValue" } } } if ($cookieString) { $Script:FloRecruitSession.AuthToken = $cookieString $Script:FloRecruitSession.TokenExpiry = (Get-Date).AddMinutes(30) Write-Verbose "Token refreshed successfully" } else { throw "Failed to extract authentication token from response" } } catch { throw "Failed to refresh authentication token: $_" } finally { # Clear sensitive data if ($plainPassword) { Clear-Variable -Name plainPassword -ErrorAction SilentlyContinue } if ($plainMFA) { Clear-Variable -Name plainMFA -ErrorAction SilentlyContinue } } } |