src/Public/SharePoint/Connect-SP365.ps1

function Connect-SP365 {
    <#
    .SYNOPSIS
        Connects to SharePoint Online using a saved tenant configuration.
 
    .DESCRIPTION
        Uses PnP.PowerShell interactive authentication to connect to SharePoint
        Online. Tenant settings are stored in Apps\M365.Toolkit\tenants.json
        under the current user's OneDrive folder.
 
        When Url is omitted, the command displays the saved tenants and prompts
        the user to select, add, update, or remove a connection.
 
    .PARAMETER Url
        The absolute URL of the SharePoint Online site. The URL must correspond
        to a saved tenant configuration.
 
    .EXAMPLE
        Connect-SP365
 
        Displays saved SharePoint tenants and prompts for the tenant to connect to.
 
    .EXAMPLE
        Connect-SP365 -Url 'https://contoso.sharepoint.com/sites/operations'
 
        Connects to the site using the matching saved tenant configuration.
 
    .INPUTS
        None.
 
    .OUTPUTS
        None. The command establishes or retains the current PnP.PowerShell
        connection.
 
    .NOTES
        Interactive authentication may open a browser or authentication dialog.
    #>

    [CmdletBinding()]
    param (
        $Url = $null
    )

    $isVerbose = $VerbosePreference -ne 'SilentlyContinue'
    Write-Verbose "Starting Connect-SP365 function"
    $privatePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath "private"
    Write-Verbose "Private path set to $privatePath"
    @(Get-ChildItem -Path $privatePath -Recurse -Filter "*.ps1") | ForEach-Object {
        try {
            . $_.FullName
        }
        catch {
            Write-Verbose "Failed to source $($_.FullName): $($_.Exception.Message)"
            
        }
    }
    Test-ForModuleUpdate

    $onedriveAppsPath = "$($env:OneDrive)\Apps\M365.Toolkit"
    $settingsFilePath = "$($onedriveAppsPath)\tenants.json"
    if (-not (Test-Path $onedriveAppsPath)) {
        New-Item -Path $onedriveAppsPath -ItemType Directory | Out-Null
    }
    if (-not (Test-Path $settingsFilePath)) {
        $settingsFilePath = "$(Split-Path -Path ($(Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent)) -Parent)\tenants.json"
        Move-Item -Path $settingsFilePath -Destination "$($onedriveAppsPath)\tenants.json" -Force
        $settingsFilePath = "$($onedriveAppsPath)\tenants.json"
    }  

    if (-not (Test-Path $settingsFilePath)) {
        New-Item -Path $settingsFilePath -ItemType File | Out-Null
        $jsonContent = '{"SharePoint": []}'
        Set-Content -Path  $settingsFilePath -Value $jsonContent | ConvertTo-Json
    }

    $settings = Get-Content $settingsFilePath | ConvertFrom-Json
    if ($settings.SharePoint.Count -eq 0) {
        Add-NewTenant -settingsFilePath $settingsFilePath
    }
    $settings = Get-Content $settingsFilePath | ConvertFrom-Json
    if ($null -eq $Url) {
        $settings.SharePoint | ForEach-Object {
            Write-Host -NoNewline "Tenant "
            Write-Host -NoNewline -ForegroundColor Yellow $_.id
            Write-Host -NoNewline ": "
            Write-Host "$($_.name) ($($_.url[0]))"
        }

        Write-Host ""
        Write-Host "Please enter the desired tenant number."

        Write-Host -NoNewline "Enter "
        Write-Host -NoNewline -ForegroundColor Yellow "n"
        Write-Host " to login with a new account"
        
        Write-Host -NoNewline "Enter "
        Write-Host -NoNewline -ForegroundColor Yellow "u"
        Write-Host " to update the above saved connection settings."

        Write-Host -NoNewline "Enter "
        Write-Host -NoNewline -ForegroundColor Yellow "r"
        Write-Host " to remove the above saved connection settings."

        Write-Host -NoNewline "Enter "
        Write-Host -NoNewline -ForegroundColor Yellow "x"
        Write-Host " to exit process"

        $inputVal = Read-Host

        if ($inputVal.ToLower() -eq "n") {
            Add-NewTenant -settingsFilePath $settingsFilePath
            return
        }
        elseif ($inputVal.ToLower() -eq "r") {
            Remove-Tenant -settingsFilePath $settingsFilePath
            return
        }
        elseif ($inputVal.ToLower() -eq "u") {
            Update-Tenant -settingsFilePath $settingsFilePath
            return
        }
        elseif ($inputVal.ToLower() -eq "x") {
            return
        }
        else {
            $tenant = $settings.SharePoint | Where-Object { $_.id -eq $inputVal }
            $Url = $tenant.url[0]
        }
    }
    else {
        $uri = [System.Uri]::new($Url)
        $tenant = $settings.SharePoint | Where-Object { $_.url -eq $uri.Host }
    }
    
    if ($null -eq $tenant) {
        Write-Host "Tenant not found"
        return
    }
    $tenantName = $tenant.name
    $tenantId = $tenant.tenantId
    $tenantClientId = $tenant.clientId
    $tenantEnvironment = $tenant.environment

    Write-Verbose "Preparing to connect to tenant $tenantName with URL $Url"
    Write-Verbose "Connecting with ClientId $tenantClientId"
    Write-Verbose "Connecting to environment $tenantEnvironment"
    Write-Verbose "Attempting to connect to SharePoint Online"
    Write-Verbose "Connecting to URL $Url"
    
    try {
        Write-Host -NoNewline "Connecting to $($tenantName) ($($Url))"

        try {
            $connection = Get-PnPConnection -ErrorAction SilentlyContinue
        }
        catch {
        }
        if ($connection -and $connection.Url.TrimEnd('/') -eq $Url.TrimEnd('/')) { 
            Write-Host -ForegroundColor Yellow " .....Already Connected"
        }
        else { 
            $connectParams = @{
                Url         = $Url
                ClientId    = $tenantClientId
                Interactive = $true
                Verbose     = $isVerbose
            }

            if ($tenantEnvironment) {
                $connectParams.AzureEnvironment = $tenantEnvironment
                $connectParams.Tenant = $tenantId
            }

            Connect-PnPOnline @connectParams
            Write-Host -ForegroundColor Green " .....Success"
        }
    }
    catch {
        Write-Host "An error occurred: $_"
        throw $_
    }
}

Export-ModuleMember -Function *
# SIG # Begin signature block
# MIIHMAYJKoZIhvcNAQcCoIIHITCCBx0CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBlvZQChthdfgmD
# Cm4WXxmvqvWO9RfnclbAGBns3DWz2qCCBBwwggQYMIICgKADAgECAhBH6tQY1Crt
# r02dueiKibmNMA0GCSqGSIb3DQEBCwUAMCQxIjAgBgNVBAMMGU0zNjUuVG9vbGtp
# dCBDb2RlIFNpZ25pbmcwHhcNMjYwNzE2MTY1NjE0WhcNMjcwNzE2MTcwNjEzWjAk
# MSIwIAYDVQQDDBlNMzY1LlRvb2xraXQgQ29kZSBTaWduaW5nMIIBojANBgkqhkiG
# 9w0BAQEFAAOCAY8AMIIBigKCAYEAxQCBRWJ923yIy73AwGWtFvfBL/dZHvwCBO05
# dJopbkHGcCqifl9R21PXr5OIjcEFv16ObYL23cPwZaNKCiejuiU32MHwidYHU1k5
# AzFpjwW2uwjeA7Hip3cURwoeOH/LzNa/xNTBRiO70M7gunZCm+FixDcPKf66Tvb1
# iM6pbOFmJvzbGjFqKM6mQRofaCPU89SHW5y43HyZNdiWsJTN4TsWj96Rps4fKhh2
# gToexqi0GhLHhLtVQ8ZxMic6w0C9sCWhYBl3kw1OJdaa/MVBdVAXDOIL0HIG8jOa
# l1x67U1LihXEw1hdc3YabhvSYZ5ZXPV1AZ2z6+Qa0EL9XlJWqBkn0rO6sZB1IkOo
# gfeTxVuI6SjhqpW6QrAFFJzb81qd0dDWmmuVka1yoWEic7naZpDfuh3JZPOUilE1
# Wv3qhBDYN5qg1qXeh0RMosR8rMfKjE8CFvsB7LTibom+22in1x16vmIiv9ep5HNc
# eS0hRa/84+vmXQ8R6hN8x+oWbG8NAgMBAAGjRjBEMA4GA1UdDwEB/wQEAwIHgDAT
# BgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUu9RWiOCtUYtlrLgwzVDsor7P
# p88wDQYJKoZIhvcNAQELBQADggGBAKJksJX6tgjdng//9wRM2ztQn8breDnY/Zki
# 8iXrC94sqCYoYg1lT0pVHZq1ZHpdUNh/xQnjP9asbIqgUt0YiCIASCONV2nM2wyK
# VjrvxaC08xefuHiO/R6Uc9sDW38mID+gDoAmswmB9JV5qm+j8b4hw3vcwuUe3nla
# 5bn3YoKXLY4/YJjHNA62TmhYNfs7iwdk2vyH/dCvD35LdBhP/y2ChmOxzI1aSp4L
# c65zCFVDPzw1IED/5q9NM48QUVDgVAX6VNuocjXZpWd7fHf7KFLRJSExXtaYQCdk
# hBSITzd5WBT2opKF02TrTyc5h1zCTr6HU+1Eqr92fvEGfqk4yYSt4plyqbm6Unnq
# gEgwDqlgm9ZNA8hzorcoxSetVVZ8z+50KB55JRTENZ/5kOJWzPtoIA+PdmFvg3wU
# jQHhO6C32rdVUk+j1hI1zL6sR8gYn5VmLPVjfs49yKf+2t3iDsaYVRNawOqHrMZj
# g59c2M2hT5D6krLsI8r3F/t3YfHbKjGCAmowggJmAgEBMDgwJDEiMCAGA1UEAwwZ
# TTM2NS5Ub29sa2l0IENvZGUgU2lnbmluZwIQR+rUGNQq7a9Nnbnoiom5jTANBglg
# hkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3
# DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEV
# MC8GCSqGSIb3DQEJBDEiBCCk/MZhx5GMlVQ4TLL1jf1VstiDPc/xrTd1EvHcjXER
# XTANBgkqhkiG9w0BAQEFAASCAYCO/dfDMvqOpl80V/ZFTnmE8b/y/6xHSRmyYOyk
# ZzfBCK/ud9YsxvZccdcr0ilbDijS9029Ru3Deuh8zAvG7wV/oeNbyNDRjlte780z
# BlRMxgqTP3+V31BO/Cw++SHImdyDFdRkB7AzTIc99cZpmKsV2ErIFyxrK5VTsWO4
# YqRJo2xdUjQdPdNWzZwUrRLFOeXgnBiogWgmaEie2zzO2nHC4V63ytj9z5cDY70f
# huNy91/UpkODT2jmSpxVeooz4qX3dG0ucAYwDpxr0YtKnkEWfH/P+LVkNu0JDR38
# +MK12B0U8+eOVDHJzGsKZFit9rG8wWCN2aLRC6Ylysw989kkugRks/F+LVpZIMdk
# Z3NHfAsU2VEmQ9WGsrNiDbPnJoTXP+gAdPD1+wUepygDOrb7KXh+eGcfool8cv33
# lCkOWsZfez55G34IzE9h/3JePnO4RV/ii1uVD6vmF1IbjKFeHM3gdSUpAu4uoS6W
# Q3PNi2c2qcAapzXE1VbwtjjcV34=
# SIG # End signature block