Private/NC-Hlp.Connections.ps1

#Requires -Version 5.0
using namespace System.Management.Automation

# Nebula.Core: (Private) Connections ================================================================================================================

function Test-EOLConnection {
    <#
    .SYNOPSIS
        Makes sure the Exchange Online session is ready to use.
    .DESCRIPTION
        Verifies that the ExchangeOnlineManagement module is present (installing it if asked),
        checks the current session and, if necessary, reconnects with the detected user.
    .PARAMETER UserPrincipalName
        Optional explicit UPN to use when connecting. Defaults to Find-UserConnected.
    .PARAMETER AutoInstall
        Install the module automatically instead of prompting.
    .PARAMETER ForceReconnect
        Skip the quick connectivity probe and always reconnect.
    #>

    [CmdletBinding()]
    param(
        [string]$UserPrincipalName,
        [switch]$AutoInstall,
        [switch]$ForceReconnect
    )

    $moduleAvailable = @(Get-Module -Name ExchangeOnlineManagement -ListAvailable).Count -gt 0
    if (-not $moduleAvailable) {
        Write-Warning "Microsoft Exchange Online Management module is not available."

        $installModule = $AutoInstall.IsPresent
        if (-not $installModule) {
            $confirmation = Read-Host "Install Microsoft Exchange Online Management module now? [Y] Yes [N] No"
            $installModule = $confirmation -match '^[yY]'
        }

        if (-not $installModule) {
            Write-NCMessage "`nMicrosoft Exchange Online Management module is required. Install it with Install-Module ExchangeOnlineManagement." -Level ERROR
            return $false
        }

        try {
            Write-NCMessage "Installing Microsoft Exchange Online Management PowerShell module ..." -Level INFO
            Install-Module ExchangeOnlineManagement -Scope CurrentUser -AllowClobber -Force -ErrorAction Stop
        }
        catch {
            Write-NCMessage "`nCan't install Exchange Online Management module. $($_.Exception.Message)" -Level ERROR
            return $false
        }
    }

    if (-not $ForceReconnect.IsPresent) {
        try {
            Get-EXOMailbox -ResultSize 1 -ErrorAction Stop | Out-Null
            return $true
        }
        catch {
            Write-NCMessage "Existing Exchange Online session not detected. Reconnecting ..." -Level WARNING
        }
    }

    if (-not $PSBoundParameters.ContainsKey('UserPrincipalName') -or [string]::IsNullOrWhiteSpace($UserPrincipalName)) {
        $resolvedUpn = Find-UserConnected
    }
    else {
        $resolvedUpn = $UserPrincipalName
    }

    $resolvedUpn = if ([string]::IsNullOrWhiteSpace($resolvedUpn)) { $null } else { $resolvedUpn }
    $message = if ($resolvedUpn) {
        "Connecting to Microsoft Exchange Online Management.`nUsing $resolvedUpn ..."
    }
    else {
        "Connecting to Microsoft Exchange Online Management ..."
    }
    Write-NCMessage $message -Level INFO

    try {
        if ($resolvedUpn) {
            Connect-EOL -UserPrincipalName $resolvedUpn -ErrorAction Stop
        }
        else {
            Connect-EOL -ErrorAction Stop
        }

        # Run a lightweight cmdlet to be sure the session is alive.
        Get-EXOMailbox -ResultSize 1 -ErrorAction Stop | Out-Null
        return $true
    }
    catch {
        Write-NCMessage "`nUnable to establish Exchange Online session. $($_.Exception.Message)" -Level ERROR
        return $false
    }
}

function Test-MgGraphConnection {
    <#
    .SYNOPSIS
        Ensures a Microsoft Graph session is available with the requested scopes.
    .DESCRIPTION
        Verifies that the Microsoft Graph module is present (installs it if requested),
        checks the current MgGraph context for the required scopes, and reconnects if needed.
        Optionally runs the Exchange Online prerequisite first to preserve legacy behaviour.
    .PARAMETER Scopes
        Delegated scopes to request during Connect-MgGraph. Defaults to User.Read.All.
    .PARAMETER TenantId
        Optional tenant to target explicitly.
    .PARAMETER UseDeviceCode
        Use device code authentication instead of opening a browser window.
    .PARAMETER AutoInstall
        Install Microsoft.Graph automatically if missing.
    .PARAMETER ForceReconnect
        Skip context validation and force a new Connect-MgGraph call.
    .PARAMETER EnsureExchangeOnline
        Run Test-EOLConnection before attempting Graph (defaults to true for backward compatibility).
    #>

    [CmdletBinding()]
    param(
        [string[]]$Scopes = @('User.Read.All'),
        [string]$TenantId,
        [switch]$UseDeviceCode,
        [switch]$AutoInstall,
        [switch]$ForceReconnect,
        [bool]$EnsureExchangeOnline = $true
    )

    if ($EnsureExchangeOnline -and -not (Test-EOLConnection -AutoInstall:$AutoInstall.IsPresent)) {
        Write-NCMessage "Exchange Online prerequisite check failed. Skipping Microsoft Graph connection." -Level ERROR
        return $false
    }

    $requestedScopes = @($Scopes | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
    if (-not $requestedScopes) {
        $requestedScopes = @('User.Read.All')
    }

    $graphModuleAvailable = @(Get-Module -Name Microsoft.Graph -ListAvailable).Count -gt 0
    if (-not $graphModuleAvailable) {
        Write-Warning "Microsoft Graph PowerShell module is not available."

        $installModule = $AutoInstall.IsPresent
        if (-not $installModule) {
            $confirmation = Read-Host "Install Microsoft Graph PowerShell module now? [Y] Yes [N] No"
            $installModule = $confirmation -match '^[yY]'
        }

        if (-not $installModule) {
            Write-NCMessage "`nMicrosoft Graph PowerShell module is required. Install it with Install-Module Microsoft.Graph." -Level ERROR
            return $false
        }

        try {
            Write-NCMessage "Installing Microsoft Graph PowerShell module ..." -Level INFO
            Install-Module Microsoft.Graph -Scope CurrentUser -AllowClobber -Force -ErrorAction Stop
        }
        catch {
            Write-NCMessage "`nCan't install Microsoft Graph module. $($_.Exception.Message)" -Level ERROR
            return $false
        }
    }

    try {
        Import-Module Microsoft.Graph.Authentication -ErrorAction Stop | Out-Null
    }
    catch {
        Write-NCMessage "`nUnable to import Microsoft.Graph.Authentication. $($_.Exception.Message)" -Level ERROR
        return $false
    }

    $missingScopeMessage = {
        param($currentScopes, $requiredScopes)
        if (-not $requiredScopes -or $requiredScopes.Count -eq 0) { return $null }
        if (-not $currentScopes -or $currentScopes.Count -eq 0) { return $requiredScopes }
        $requiredScopes | Where-Object { $currentScopes -notcontains $_ }
    }

    if (-not $ForceReconnect.IsPresent) {
        try {
            $ctx = Get-MgContext -ErrorAction Stop
            if ($ctx -and $ctx.Account) {
                $missingScopes = &$missingScopeMessage $ctx.Scopes $requestedScopes
                if (-not $missingScopes -or $missingScopes.Count -eq 0) {
                    return $true
                }

                Write-NCMessage "Existing Microsoft Graph session missing required scopes ($($missingScopes -join ', ')). Reconnecting ..." -Level WARNING
            }
            else {
                Write-NCMessage "Microsoft Graph context not found. Establishing connection ..." -Level INFO
            }
        }
        catch {
            Write-NCMessage "Unable to retrieve current Microsoft Graph context. Reconnecting ..." -Level WARNING
        }
    }

    $scopeLabel = $requestedScopes -join ', '
    Write-NCMessage "Connecting to Microsoft Graph requesting scopes: $scopeLabel" -Level INFO

    $connectParams = @{
        Scopes    = $requestedScopes
        NoWelcome = $true
    }

    if ($TenantId) {
        $connectParams.TenantId = $TenantId
    }

    if ($UseDeviceCode.IsPresent) {
        $connectParams.UseDeviceCode = $true
    }

    try {
        Connect-MgGraph @connectParams | Out-Null

        $ctx = Get-MgContext -ErrorAction Stop
        if (-not $ctx -or -not $ctx.Account) {
            throw "Connect-MgGraph did not return an authenticated context."
        }

        $missingScopes = &$missingScopeMessage $ctx.Scopes $requestedScopes
        if ($missingScopes -and $missingScopes.Count -gt 0) {
            throw "Connected session is missing scopes: $($missingScopes -join ', ')"
        }

        return $true
    }
    catch {
        Write-NCMessage "`nUnable to establish Microsoft Graph session. $($_.Exception.Message)" -Level ERROR
        return $false
    }
}