Zivver.psm1

# Zivver.psm1
# Author: Martijn van de Pol
# Description: Provides cmdlets for managing Zivver users and groups via the SCIM API. https://github.com/polcomp/Zivver
# Version: 1.0.3
# Copyright © Martijn van de Pol, 2025

# Load all public functions
Get-ChildItem -Path "$PSScriptRoot/*.ps1" | ForEach-Object {
    . $_.FullName
}

# Session variable to store token and base URI
$Script:ZivverSession = @{}

function Connect-ZivverApi {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$Token,

        [string]$BaseUri = "https://app.zivver.com/api/scim/v2"
    )

    $headers = @{
        "Authorization" = "Bearer $Token"
        "Accept"        = "application/scim+json"
    }

    try {
        $testUri = "$BaseUri/Users?count=1"
        $response = Invoke-RestMethod -Uri $testUri -Method GET -Headers $headers -ErrorAction Stop

        $Script:ZivverSession = @{
            Token   = $Token
            BaseUri = $BaseUri
        }

        Write-Host "✅ Successfully connected to Zivver SCIM API." -ForegroundColor Green
    } catch {
        Write-Error "❌ Failed to connect to Zivver SCIM API: $_"
    }
}

function CreateZivverUserTable {
    [PSCustomObject]@{
        Id        = $_.id
        UserName  = $_.userName
        Name      = $_.name.formatted
        phoneNumbers      = $_.phoneNumbers
        Active    = $_.active
        Created   = $_.meta.created
        ResourceType  = $_.meta.resourceType
        Division  = $_.'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'.division
        Aliases       = $_."urn:ietf:params:scim:schemas:zivver:0.1:User".aliases
        Delegates     = $_."urn:ietf:params:scim:schemas:zivver:0.1:User".delegates
        ExternalAccountId     = $_."urn:ietf:params:scim:schemas:zivver:0.1:User".ExternalAccountId
    }
}

function CreateZivverGroupTable {
    [PSCustomObject]@{
        Id        = $_.id
        externalId  = $_.externalId
        displayName      = $_.displayName
        members      = $_.members
        Created   = $_.meta.created
        ResourceType  = $_.meta.resourceType
        Division  = $_.'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'.division
        Aliases       = $_."urn:ietf:params:scim:schemas:zivver:0.1:Group".aliases
        ExternalAccountId     = $_."urn:ietf:params:scim:schemas:zivver:0.1:Group".ExternalAccountId
    }
}

function Get-ZivverOrganization {
    $baseUri = "$($Script:ZivverSession.BaseUri)/Organization"
    $headers = @{
        "Authorization" = "Bearer $($Script:ZivverSession.Token)"
        "Accept"        = "application/scim+json"
    }

    try {
        Invoke-RestMethod -Uri $baseUri -Method GET -Headers $headers -ErrorAction Stop
    } catch {
        Write-Error "❌ Failed to retrieve Zivver Organization: $_"
    }
}

function Get-ZivverUser {
    [CmdletBinding(DefaultParameterSetName = 'All')]
    param (
        [Parameter(ParameterSetName = 'ById')][string]$Id,
        [Parameter(ParameterSetName = 'ByUsername')][string]$UserName
    )

    $baseUri = "$($Script:ZivverSession.BaseUri)/Users"
    $headers = @{
        "Authorization" = "Bearer $($Script:ZivverSession.Token)"
        "Accept"        = "application/scim+json"
    }

    try {
        switch ($PSCmdlet.ParameterSetName) {
            'ById' {
                $uri = "$baseUri/$Id"
                $response = Invoke-RestMethod -Uri $uri -Method GET -Headers $headers -ErrorAction Stop
                if (-not $response.Resources) {Write-Error "❌ Cannot find user with Id $Id"}
                return $response.Resources | ForEach-Object {CreateZivverUserTable}
            }
            'ByUsername' {
                $uri = "$baseUri" + "?filter=userName eq `"$UserName`""
                $response = Invoke-RestMethod -Uri $uri -Method GET -Headers $headers -ErrorAction Stop
                if (-not $response.Resources) {Write-Error "❌ Cannot find user with UserName $UserName"}
                return $response.Resources | ForEach-Object {CreateZivverUserTable}
            }
            default {
                $response = Invoke-RestMethod -Uri $baseUri -Method GET -Headers $headers -ErrorAction Stop
                return $response.Resources | ForEach-Object {CreateZivverUserTable}
            }
        }
    } catch {
        Write-Error "❌ Failed to retrieve Zivver users: $_"
    }
}


function Set-ZivverUser {
    [CmdletBinding(DefaultParameterSetName = 'ById')]
    param (
        [Parameter(ParameterSetName = 'ById', Mandatory)][string]$Id,
        [Parameter(ParameterSetName = 'ByUsername', Mandatory)][string]$UserName,

        [string[]]$Aliases,
        [string[]]$Delegates,
        [bool]$Active
    )

    $baseUri = "$($Script:ZivverSession.BaseUri)/Users"
    $headers = @{
        "Authorization" = "Bearer $($Script:ZivverSession.Token)"
        "Content-Type"  = "application/scim+json"
    }

    try {
        if ($UserName) {
            $uri = "$baseUri" + "?filter=userName eq `"$UserName`""
            $lookup = Invoke-RestMethod -Uri $uri -Headers $headers -ErrorAction Stop
            if ($lookup.Resources.Count -eq 0) {
                throw "User not found with username: $UserName"
            }
            $Id = $lookup.Resources[0].id
        }

        $operations = @{}

        # Always retrieve current user details
        $currentUser = Invoke-RestMethod -Uri "$baseUri/$Id" -Headers $headers -ErrorAction Stop
        $currentExtension = $currentUser.'urn:ietf:params:scim:schemas:zivver:0.1:User'

        $aliases   = if ($PSBoundParameters.ContainsKey('Aliases')) { $Aliases } else { $currentExtension.aliases }
        $delegates = if ($PSBoundParameters.ContainsKey('Delegates')) { $Delegates } else { $currentExtension.delegates }
        $externalAccountId = $currentExtension.ExternalAccountId  # Always preserve!

        $operations += @{
            'urn:ietf:params:scim:schemas:zivver:0.1:User' = @{
                aliases   = $aliases
                delegates = $delegates
                ExternalAccountId = $externalAccountId
            }
        }

        if ($PSBoundParameters.ContainsKey('Active')) {
            $operations += @{ active = $Active }
        }

        if ($operations.Count -eq 0) {
            Write-Warning "No update parameters provided."
            return
        }

        $body = $operations | ConvertTo-Json -Depth 5

        $patchUri = "$baseUri/$Id"
        $response = Invoke-RestMethod -Uri $patchUri -Method PUT -Headers $headers -Body $body -ContentType "application/json; charset=utf-8" -ErrorAction Stop
        Write-Host "✅ User $Id successfully updated."
        
        return $response | ForEach-Object {CreateZivverUserTable}

    } catch {
        Write-Error "❌ Failed to update Zivver user: $_"
    }
}

function Add-ZivverUser {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)][string]$UserName,
        [Parameter(Mandatory)][string]$Name,
        [Parameter(Mandatory)][string]$Active
    )

    $uri = "$($Script:ZivverSession.BaseUri)/Users"
    $headers = @{
        "Authorization" = "Bearer $($Script:ZivverSession.Token)"
        "Content-Type"  = "application/scim+json"
    }

    $operations = @{}

    $operations += @{ userName = $UserName }
    $operations += @{ active = $Active }
    $operations += @{ name = @{ formatted = $UserName } }

    $body = $operations | ConvertTo-Json -Depth 5

    try {
        $response = Invoke-RestMethod -Uri $uri -Method POST -Headers $headers -Body $body
        Write-Host "✅ Zivver user '$UserName' successfully created."
        return $response | ForEach-Object {CreateZivverUserTable}
    } catch {
        Write-Error "❌ Failed to create Zivver user: $_"
    }
}

function Remove-ZivverUser {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High', DefaultParameterSetName = 'ById')]
    param (
        [Parameter(ParameterSetName = 'ById', Mandatory)][string]$Id,
        [Parameter(ParameterSetName = 'ByUsername', Mandatory)][string]$UserName,
        [switch]$Force
    )

    $baseUri = "$($Script:ZivverSession.BaseUri)/Users"
    $headers = @{
        "Authorization" = "Bearer $($Script:ZivverSession.Token)"
        "Accept"        = "application/scim+json"
    }

    try {
        # Resolve by username if needed
        if ($PSCmdlet.ParameterSetName -eq 'ByUsername') {
            $uri = "$baseUri" + "?filter=userName eq `"$UserName`""
            $lookup = Invoke-RestMethod -Uri $uri -Headers $headers -ErrorAction Stop
            if (-not $lookup.Resources -or $lookup.Resources.Count -eq 0) {
                throw "Cannot find user with UserName '$UserName'."
            }
            $user = $lookup.Resources[0]
            $Id = $user.id
        } else {
            # Fetch user (for confirmation message)
            $userResp = Invoke-RestMethod -Uri "$baseUri/$Id" -Headers $headers -ErrorAction Stop
            # Some SCIM impls return full object vs envelope; normalize
            $user = if ($userResp.PSObject.Properties.Match('Resources')) { $userResp.Resources[0] } else { $userResp }
            if (-not $user) { throw "Cannot find user with Id '$Id'." }
        }

        $targetLabel = if ($user.userName) { "$($user.userName) (Id: $Id)" } else { "Id: $Id" }

        # Respect -Force/-Confirm and WhatIf
        if ($Force -or $PSCmdlet.ShouldProcess($targetLabel, "REMOVE Zivver user")) {
            Invoke-RestMethod -Uri "$baseUri/$Id" -Method DELETE -Headers $headers -ErrorAction Stop
            Write-Host "🗑️ Removed Zivver user: $targetLabel"
        }
    } catch {
        Write-Error "❌ Failed to delete Zivver user: $_"
    }
}

function Get-ZivverGroup {
    [CmdletBinding(DefaultParameterSetName = 'All')]
    param (
        [Parameter(ParameterSetName = 'ById')][string]$Id,
        [Parameter(ParameterSetName = 'ByExternalId')][string]$ExternalId
    )

    $baseUri = "$($Script:ZivverSession.BaseUri)/Groups"
    $headers = @{
        "Authorization" = "Bearer $($Script:ZivverSession.Token)"
        "Accept"        = "application/scim+json"
    }

    try {
        switch ($PSCmdlet.ParameterSetName) {
            'ById' {
                $uri = "$baseUri/$Id"
                $response = Invoke-RestMethod -Uri $uri -Method GET -Headers $headers -ErrorAction Stop
                if (-not $response) {Write-Error "❌ Cannot find user with Id $Id"}
                return $response | ForEach-Object {CreateZivverGroupTable}
            }
            'ByExternalId' {
                $uri = "$baseUri" + "?filter=externalId eq `"$ExternalId`""
                $response = Invoke-RestMethod -Uri $uri -Method GET -Headers $headers -ErrorAction Stop
                if (-not $response.Resources) {Write-Error "❌ Cannot find Group with ExternalId $ExternalId"}
                return $response.Resources | ForEach-Object {CreateZivverGroupTable}
            }
            default {
                $response = Invoke-RestMethod -Uri $baseUri -Method GET -Headers $headers -ErrorAction Stop
                return $response.Resources | ForEach-Object {CreateZivverGroupTable}
            }
        }
    } catch {
        Write-Error "❌ Failed to retrieve Zivver groups: $_"
    }
}

function Set-ZivverGroup {
    [CmdletBinding(DefaultParameterSetName = 'ById')]
    param (
        [Parameter(ParameterSetName = 'ById', Mandatory)][string]$Id,
        [Parameter(ParameterSetName = 'ByExternalId', Mandatory)][string]$ExternalId,
        [string[]]$Aliases,
        [string[]]$Members,
        [string]$NewExternalId
    )

    $baseUri = "$($Script:ZivverSession.BaseUri)/Groups"
    $headers = @{
        "Authorization" = "Bearer $($Script:ZivverSession.Token)"
        "Content-Type"  = "application/scim+json"
    }

    try {
        if ($ExternalId) {
            $uri = "$baseUri" + "?filter=externalId eq `"$ExternalId`""
            $lookup = Invoke-RestMethod -Uri $uri -Headers $headers -ErrorAction Stop
            if ($lookup.Resources.Count -eq 0) {
                throw "Group not found with ExternalId: $ExternalId"
            }
            $Id = $lookup.Resources[0].id
        }
        
        $operations = @{}

        # Always retrieve current group details
        $currentGroup = Invoke-RestMethod -Uri "$baseUri/$Id" -Headers $headers -ErrorAction Stop
        $currentExtension = $currentGroup.'urn:ietf:params:scim:schemas:zivver:0.1:Group'

        $aliases   = if ($PSBoundParameters.ContainsKey('Aliases')) { $Aliases } else { $currentExtension.aliases }
        $externalAccountId = $currentExtension.ExternalAccountId  # Always preserve!

        if ($PSBoundParameters.ContainsKey('Aliases')) {
            $operations += @{
                'urn:ietf:params:scim:schemas:zivver:0.1:Group' = @{
                    aliases   = $aliases
                    ExternalAccountId = $externalAccountId
                }
            }
        }

        if ($PSBoundParameters.ContainsKey('Members')) {
            $MembersTable = @()
            foreach ($MemberId in $Members) {
                $MembersTable += @{ value = $memberId }
            }
            $operations += @{
                members = $MembersTable
            }
        }
        

        if ($operations.Count -eq 0) {
            Write-Warning "No update parameters provided."
            return
        }

        $body = $operations | ConvertTo-Json -Depth 5

        $patchUri = "$baseUri/$Id"
        $response = Invoke-RestMethod -Uri $patchUri -Method PUT -Headers $headers -Body $body -ContentType "application/json; charset=utf-8" -ErrorAction Stop
        Write-Host "✅ Group $Id successfully updated."
        
        return $response | ForEach-Object {CreateZivverGroupTable}

    } catch {
        Write-Error "❌ Failed to update Zivver group: $_"
    }
}

function Add-ZivverGroup {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)][string]$ExternalId,
        [Parameter(Mandatory)][string]$DisplayName
    )

    $uri = "$($Script:ZivverSession.BaseUri)/Groups"
    $headers = @{
        "Authorization" = "Bearer $($Script:ZivverSession.Token)"
        "Content-Type"  = "application/scim+json"
    }

    $operations = @{}

    $operations += @{ externalId = $ExternalId }
    $operations += @{ displayName = $DisplayName }

    $body = $operations | ConvertTo-Json -Depth 5

    try {
        $response = Invoke-RestMethod -Uri $uri -Method POST -Headers $headers -Body $body
        Write-Host "✅ Zivver group '$DisplayName' successfully created."
        return $response | ForEach-Object {CreateZivverGroupTable}
    } catch {
        Write-Error "❌ Failed to create Zivver group: $_"
    }
}

function Remove-ZivverGroup {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High', DefaultParameterSetName = 'ById')]
    param (
        [Parameter(ParameterSetName = 'ById', Mandatory)][string]$Id,
        [Parameter(ParameterSetName = 'ByExternalId', Mandatory)][string]$ExternalId,
        [switch]$Force
    )

    $baseUri = "$($Script:ZivverSession.BaseUri)/Groups"
    $headers = @{
        "Authorization" = "Bearer $($Script:ZivverSession.Token)"
        "Accept"        = "application/scim+json"
    }

    try {
        # Resolve by ExternalId if needed
        if ($PSCmdlet.ParameterSetName -eq 'ByExternalId') {
            $uri = "$baseUri" + "?filter=externalId eq `"$ExternalId`""
            $lookup = Invoke-RestMethod -Uri $uri -Headers $headers -ErrorAction Stop
            if (-not $lookup.Resources -or $lookup.Resources.Count -eq 0) {
                throw "Cannot find group with ExternalId '$ExternalId'."
            }
            $group = $lookup.Resources[0]
            $Id = $group.id
        } else {
            # Fetch group (for confirmation message)
            $groupResp = Invoke-RestMethod -Uri "$baseUri/$Id" -Headers $headers -ErrorAction Stop
            $group = if ($groupResp.PSObject.Properties.Match('Resources')) { $groupResp.Resources[0] } else { $groupResp }
            if (-not $group) { throw "Cannot find group with Id '$Id'." }
        }

        $label = if ($group.displayName) { "$($group.displayName) (Id: $Id)" }
                 elseif ($ExternalId)   { "ExternalId: $ExternalId (Id: $Id)" }
                 else { "Id: $Id" }

        if ($Force -or $PSCmdlet.ShouldProcess($label, "REMOVE Zivver group")) {
            Invoke-RestMethod -Uri "$baseUri/$Id" -Method DELETE -Headers $headers -ErrorAction Stop
            Write-Host "🗑️ Remove Zivver group: $label"
        }
    } catch {
        Write-Error "❌ Failed to delete Zivver group: $_"
    }
}