Public/Pages.ps1

function Set-PodeWebLoginPage {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [string]
        $Authentication,

        [Parameter()]
        [hashtable[]]
        $Content,

        [Parameter()]
        [string]
        $Logo,

        [Parameter()]
        [string]
        $LogoUrl,

        [Parameter()]
        [string]
        $Copyright,

        [Parameter()]
        [string]
        $UsernameProperty,

        [Parameter()]
        [string]
        $GroupProperty,

        [Parameter()]
        [string]
        $AvatarProperty,

        [Parameter()]
        [string]
        $ThemeProperty,

        [Parameter()]
        [string]
        $BackgroundImage,

        [Parameter()]
        [string]
        $SignInMessage,

        [Parameter()]
        [string]
        $LoginPath,

        [Parameter()]
        [string]
        $LogoutPath,

        [switch]
        $PassThru
    )

    # check content
    if (!(Test-PodeWebContent -Content $Content -ComponentType Element)) {
        throw 'The Login page can only contain other elements'
    }

    # retrieve the auth from pode
    $auth = Get-PodeAuth -Name $Authentication

    # if no content, add default
    if (Test-PodeIsEmpty -Value $Content) {
        $Content = @(
            New-PodeWebTextbox -Type Text -Name 'username' -Id 'username' -Placeholder 'Username' -Required -AutoFocus -DynamicLabel
            New-PodeWebTextbox -Type Password -Name 'password' -Id 'password' -Placeholder 'Password' -Required -DynamicLabel
        )
    }

    # set auth to be used on other pages
    Set-PodeWebState -Name 'auth' -Value $Authentication
    Set-PodeWebState -Name 'auth-props' -Value @{
        Username = $UsernameProperty
        Group    = $GroupProperty
        Avatar   = $AvatarProperty
        Theme    = $ThemeProperty
        Logout   = $true
    }

    # get home url
    $sysUrls = Get-PodeWebState -Name 'system-urls'

    # set a default logo/url
    if ([string]::IsNullOrWhiteSpace($Logo)) {
        $Logo = '/pode.web-static/images/icon.png'
    }
    $Logo = (Add-PodeWebAppPath -Url $Logo)

    if (![string]::IsNullOrWhiteSpace($LogoUrl)) {
        $LogoUrl = (Add-PodeWebAppPath -Url $LogoUrl)
    }

    # background image
    $BackgroundImage = (Add-PodeWebAppPath -Url $BackgroundImage)

    # is this auto-redirect oauth2?
    $isOAuth2 = ($auth.Scheme.Scheme -ieq 'oauth2')

    $grantType = 'authorization_code'
    if ($isOAuth2 -and !(Test-PodeIsEmpty $auth.Scheme.InnerScheme)) {
        $grantType = 'password'
    }

    # generate page ID
    $Id = Get-PodeWebPageId -Id $Id -Name 'login' -System

    # login / logout paths
    if ([string]::IsNullOrEmpty($LoginPath)) {
        $LoginPath = '/login'
    }

    if ([string]::IsNullOrEmpty($LogoutPath)) {
        $LogoutPath = '/logout'
    }

    # setup page meta
    $pageMeta = @{
        Operation       = 'New'
        ComponentType   = 'Page'
        ObjectType      = 'Page'
        ID              = $Id
        Route           = @{
            Login  = @{
                Path = (Get-PodeWebPagePath -Name 'login' -Path $LoginPath -NoAppPath)
                Url  = (Get-PodeWebPagePath -Name 'login' -Path $LoginPath)
            }
            Logout = @{
                Path = (Get-PodeWebPagePath -Name 'logout' -Path $LogoutPath -NoAppPath)
                Url  = (Get-PodeWebPagePath -Name 'logout' -Path $LogoutPath)
            }
        }
        Name            = 'Login'
        Content         = $Content
        SignInMessage   = (Protect-PodeWebValue -Value $SignInMessage -Default 'Please sign in' -Encode)
        Logo            = @{
            IconUrl = $Logo
            Url     = $LogoUrl
        }
        BackgroundImage = $BackgroundImage
        CopyRight       = $Copyright
        Authentication  = $Authentication
        IsOAuth2        = $isOAuth2
        GrantType       = $grantType
        IsSystem        = $true
        ResponseType    = (Get-PodeWebResponseType)
    }

    # set auth system urls
    $sysUrls.Login = $pageMeta.Route.Login
    $sysUrls.Logout = $pageMeta.Route.Logout

    # set default failure/success urls
    if ([string]::IsNullOrWhiteSpace($auth.Failure.Url)) {
        $auth.Failure.Url = $pageMeta.Route.Login.Url
    }

    if ([string]::IsNullOrWhiteSpace($auth.Success.Url)) {
        $auth.Success.Url = $sysUrls.Home.Url
    }

    # add page meta to state
    $pages = Get-PodeWebState -Name 'pages'
    $pages[$Id] = $pageMeta

    # get the endpoints to bind
    $endpointNames = Get-PodeWebState -Name 'endpoint-name'

    # add the login route
    Add-PodeRoute -Method Get -Path $pageMeta.Route.Login.Path -Authentication $Authentication -ArgumentList @{ ID = $Id } -EndpointName $endpointNames -Login -ScriptBlock {
        param($Data)
        $global:PageData = (Get-PodeWebState -Name 'pages')[$Data.ID]

        Write-PodeWebViewResponse -Path 'login' -Data @{
            Page          = $global:PageData
            Theme         = Get-PodeWebTheme
            Sessions      = @{
                Enabled = (Test-PodeSessionsEnabled)
                Tabs    = !(Test-PodeSessionScopeIsBrowser)
            }
            Logo          = $PageData.Logo.IconUrl
            LogoUrl       = $PageData.Logo.Url
            Background    = @{
                Image = $PageData.BackgroundImage
            }
            SignInMessage = $PageData.SignInMessage
            Copyright     = $PageData.Copyright
            Auth          = @{
                Name      = $PageData.Authentication
                IsOAuth2  = $PageData.IsOAuth2
                GrantType = $PageData.GrantType
            }
        }

        $global:PageData = $null
    }

    Add-PodeRoute -Method Post -Path $pageMeta.Route.Login.Path -Authentication $Authentication -EndpointName $endpointNames -Login

    # add the logout route
    Add-PodeRoute -Method Post -Path $pageMeta.Route.Logout.Path -Authentication $Authentication -EndpointName $endpointNames -Logout

    # login content
    Add-PodeRoute -Method Post -Path "/pode.web-dynamic/pages/$($pageMeta.ID)/content" -ArgumentList @{ ID = $Id } -EndpointName $endpointNames -ScriptBlock {
        param($Data)
        $global:PageData = (Get-PodeWebState -Name 'pages')[$Data.ID]
        Write-PodeJsonResponse -Value $global:PageData.Content
        $global:PageData = $null
    }

    # add sse open route
    if (Test-PodeWebResponseType -Type Sse) {
        Add-PodeRoute -Method Get -Path "/pode.web-dynamic/pages/$($pageMeta.ID)/sse-open" -ArgumentList @{ ID = $Id } -EndpointName $EndpointName -ScriptBlock {
            param($Data)
            ConvertTo-PodeSseConnection -Name 'Pode.Web.Actions' -Group $Data.ID
        }

        # add sse close route
        Add-PodeRoute -Method Post -Path "/pode.web-dynamic/pages/$($pageMeta.ID)/sse-close" -EndpointName $EndpointName -ScriptBlock {
            $clientId = Get-PodeHeader -Name 'X-PODE-SSE-CLIENT-ID'
            if (![string]::IsNullOrEmpty($clientId)) {
                Close-PodeSseConnection -Name 'Pode.Web.Actions' -ClientId $clientId
            }
        }
    }

    if ($PassThru) {
        return $pageMeta
    }
}

function Add-PodeWebPage {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Path,

        [Parameter()]
        [object[]]
        $Middleware,

        [Parameter()]
        [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
        $IfExists = 'Default',

        [Parameter()]
        [int]
        $Index = [int]::MaxValue,

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter()]
        [string]
        $Title,

        [Parameter()]
        [ValidateNotNull()]
        [string]
        $Group = [string]::Empty,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]
        $Icon = 'file',

        [Parameter()]
        [hashtable[]]
        $Content,

        [Parameter()]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [object[]]
        $ArgumentList,

        [Parameter()]
        [string[]]
        $AccessGroups = @(),

        [Parameter()]
        [string[]]
        $AccessUsers = @(),

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

        [Parameter()]
        [hashtable[]]
        $Navigation,

        [Parameter()]
        [scriptblock]
        $HelpScriptBlock,

        [Parameter()]
        [Alias('NoAuth')]
        [switch]
        $NoAuthentication,

        [switch]
        $NoTitle,

        [switch]
        $NoBackArrow,

        [switch]
        $NoBreadcrumb,

        [switch]
        $NewTab,

        [switch]
        $Hide,

        [switch]
        $NoSidebar,

        [switch]
        $NoNavigation,

        [switch]
        $HomePage,

        [switch]
        $PassThru
    )

    # ensure elements are correct
    if (!(Test-PodeWebContent -Content $Content -ComponentType Element)) {
        throw 'A Page can only contain elements'
    }

    # test if group exists - otherwise create a basic group entry
    if (!(Test-PodeWebPageGroup -Name $Group)) {
        New-PodeWebPageGroup -Name $Group
    }

    # generate page ID
    $Id = Get-PodeWebPageId -Id $Id -Name $Name -Group $Group

    # test if page/page-link exists
    if (Test-PodeWebPage -Id $Id) {
        throw "Web page/link already exists: $($Name) [Group: $($Group)]"
    }

    # set page title
    if ([string]::IsNullOrWhiteSpace($DisplayName)) {
        $DisplayName = $Name
    }

    if ([string]::IsNullOrWhiteSpace($Title)) {
        $Title = $DisplayName
    }

    # check for scoped vars
    $ScriptBlock, $mainUsingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
    $HelpScriptBlock, $helpUsingVars = Convert-PodeScopedVariables -ScriptBlock $HelpScriptBlock -PSSession $PSCmdlet.SessionState

    # setup page meta
    $pageMeta = @{
        Operation        = 'New'
        ComponentType    = 'Page'
        ObjectType       = 'Page'
        ID               = $Id
        Index            = $Index
        Group            = $Group
        Name             = $Name
        Title            = [System.Net.WebUtility]::HtmlEncode($Title)
        DisplayName      = [System.Net.WebUtility]::HtmlEncode($DisplayName)
        NoTitle          = $NoTitle.IsPresent
        NoBackArrow      = $NoBackArrow.IsPresent
        NoBreadcrumb     = $NoBreadcrumb.IsPresent
        NewTab           = $NewTab.IsPresent
        IsDynamic        = $false
        ShowHelp         = ($null -ne $HelpScriptBlock)
        Icon             = $Icon
        Path             = (Get-PodeWebPagePath -Name $Name -Group $Group -Path $Path -NoAppPath)
        Url              = (Get-PodeWebPagePath -Name $Name -Group $Group -Path $Path)
        Hide             = $Hide.IsPresent
        NoSidebar        = $NoSidebar.IsPresent
        NoNavigation     = $NoNavigation.IsPresent
        Navigation       = $Navigation
        Logic            = @{
            ScriptBlock    = $ScriptBlock
            UsingVariables = $mainUsingVars
        }
        Help             = @{
            ScriptBlock    = $HelpScriptBlock
            UsingVariables = $helpUsingVars
        }
        Content          = $Content
        Authentication   = $null
        NoAuthentication = $NoAuthentication.IsPresent
        IsHomePage       = $HomePage.IsPresent
        Access           = @{
            Groups = @($AccessGroups)
            Users  = @($AccessUsers)
        }
        ResponseType     = (Get-PodeWebResponseType)
    }

    # does the page need auth?
    $auth = $null
    if (!$pageMeta.NoAuthentication) {
        $auth = Get-PodeWebState -Name 'auth'
    }
    $pageMeta.Authentication = $auth

    # add page meta to state
    Register-PodeWebPage -Metadata $pageMeta

    # get the endpoints to bind
    if (Test-PodeIsEmpty $EndpointName) {
        $EndpointName = Get-PodeWebState -Name 'endpoint-name'
    }

    # remove the "root" page, if "root-redirect" was originally flagged and this page is for the root path
    if (($pageMeta.Path -eq '/') -and (Get-PodeWebState -Name 'root-redirect')) {
        Remove-PodeRoute -Method Get -Path '/'
    }

    # add the page route
    Add-PodeRoute -Method Get -Path $pageMeta.Path -Authentication $pageMeta.Authentication -ArgumentList @{ Data = $ArgumentList; ID = $Id } -Middleware $Middleware -IfExists $IfExists -EndpointName $EndpointName -ScriptBlock {
        param($Data)
        $global:PageData = (Get-PodeWebState -Name 'pages')[$Data.ID]

        # get auth details of a user
        $authEnabled = ![string]::IsNullOrEmpty((Get-PodeWebState -Name 'auth'))
        $authMeta = $null

        if ($authEnabled) {
            $authData = Get-PodeAuthUser
            $authMeta = @{
                Enabled       = $true
                Logout        = (Get-PodeWebState -Name 'auth-props').Logout
                Authenticated = ($null -ne $authData)
            }

            if ($authMeta.Authenticated) {
                $authMeta['Username'] = Get-PodeWebAuthUsername -User $authData
                $authMeta['Groups'] = Get-PodeWebAuthGroups -User $authData
                $authMeta['Avatar'] = Get-PodeWebAuthAvatarUrl -User $authData
            }
        }

        # check access - 403 if denied
        if (!(Test-PodeWebPageAccess -PageAccess $global:PageData.Access -Auth $authMeta)) {
            Set-PodeResponseStatus -Code 403
        }

        else {
            # show a back arrow?
            if (!$global:PageData.NoBackArrow) {
                $global:PageData.ShowBack = (($null -ne $WebEvent.Query) -and ($WebEvent.Query.Count -gt 0))
                if ($global:PageData.ShowBack -and ($WebEvent.Query.Count -eq 1) -and ($WebEvent.Query.ContainsKey(''))) {
                    $global:PageData.ShowBack = $false
                }
            }
            else {
                $global:PageData.ShowBack = $false
            }

            # render the page
            Write-PodeWebViewResponse -Path 'index' -Data @{
                Page        = $global:PageData
                Title       = $global:PageData.Title
                DisplayName = $global:PageData.DisplayName
                Theme       = (Get-PodeWebTheme)
                Sessions    = @{
                    Enabled = (Test-PodeSessionsEnabled)
                    Tabs    = !(Test-PodeSessionScopeIsBrowser)
                }
                Auth        = $authMeta
            }
        }

        $global:PageData = $null
    }

    Add-PodeRoute -Method Post -Path "/pode.web-dynamic/pages/$($pageMeta.ID)/content" -Authentication $pageMeta.Authentication -ArgumentList @{ Data = $ArgumentList; ID = $Id } -IfExists $IfExists -EndpointName $EndpointName -ScriptBlock {
        param($Data)
        $global:PageData = (Get-PodeWebState -Name 'pages')[$Data.ID]
        Set-PodeWebMetadata

        # get auth details of a user
        $authEnabled = ![string]::IsNullOrEmpty((Get-PodeWebState -Name 'auth'))
        $authMeta = $null

        if ($authEnabled) {
            $authData = Get-PodeAuthUser
            if ($null -ne $authData) {
                $authMeta = @{
                    Username = (Get-PodeWebAuthUsername -User $authData)
                    Groups   = (Get-PodeWebAuthGroups -User $authData)
                }
            }
        }

        # check access - 403 if denied
        if (!(Test-PodeWebPageAccess -PageAccess $global:PageData.Access -Auth $authMeta)) {
            Set-PodeResponseStatus -Code 403
        }
        else {
            # if we have a scriptblock, invoke that to get dynamic elements
            $content = $null
            if ($null -ne $global:PageData.Logic.ScriptBlock) {
                $content = Invoke-PodeWebScriptBlock -Logic $global:PageData.Logic -Arguments $Data.Data
            }

            if (($null -eq $content) -or ($content.Length -eq 0)) {
                $content = $global:PageData.Content
            }

            $navigation = Get-PodeWebNavDefault -Items $global:PageData.Navigation
            Write-PodeJsonResponse -Value (@($navigation) + @($content))
        }

        $global:PageData = $null
    }

    # add sse open route
    if (Test-PodeWebResponseType -Type Sse) {
        Add-PodeRoute -Method Get -Path "/pode.web-dynamic/pages/$($pageMeta.ID)/sse-open" -Authentication $pageMeta.Authentication -ArgumentList @{ Data = $ArgumentList; ID = $Id } -IfExists $IfExists -EndpointName $EndpointName -ScriptBlock {
            param($Data)
            $global:PageData = (Get-PodeWebState -Name 'pages')[$Data.ID]

            # get auth details of a user
            $authEnabled = ![string]::IsNullOrEmpty((Get-PodeWebState -Name 'auth'))
            $authMeta = $null

            if ($authEnabled) {
                $authData = Get-PodeAuthUser
                if ($null -ne $authData) {
                    $authMeta = @{
                        Username = (Get-PodeWebAuthUsername -User $authData)
                        Groups   = (Get-PodeWebAuthGroups -User $authData)
                    }
                }
            }

            # check access - 403 if denied
            if (!(Test-PodeWebPageAccess -PageAccess $global:PageData.Access -Auth $authMeta)) {
                Set-PodeResponseStatus -Code 403
            }
            else {
                # open new sse connection
                ConvertTo-PodeSseConnection -Name 'Pode.Web.Actions' -Group $Data.ID
            }

            $global:PageData = $null
        }

        # add sse close route
        Add-PodeRoute -Method Post -Path "/pode.web-dynamic/pages/$($pageMeta.ID)/sse-close" -Authentication $pageMeta.Authentication -ArgumentList @{ Data = $ArgumentList; ID = $Id } -IfExists $IfExists -EndpointName $EndpointName -ScriptBlock {
            param($Data)
            $global:PageData = (Get-PodeWebState -Name 'pages')[$Data.ID]

            # get auth details of a user
            $authEnabled = ![string]::IsNullOrEmpty((Get-PodeWebState -Name 'auth'))
            $authMeta = $null

            if ($authEnabled) {
                $authData = Get-PodeAuthUser
                if ($null -ne $authData) {
                    $authMeta = @{
                        Username = (Get-PodeWebAuthUsername -User $authData)
                        Groups   = (Get-PodeWebAuthGroups -User $authData)
                    }
                }
            }

            # check access - 403 if denied
            if (!(Test-PodeWebPageAccess -PageAccess $global:PageData.Access -Auth $authMeta)) {
                Set-PodeResponseStatus -Code 403
            }
            else {
                # if a connection in header, close connection
                $clientId = Get-PodeHeader -Name 'X-PODE-SSE-CLIENT-ID'
                if (![string]::IsNullOrEmpty($clientId)) {
                    Close-PodeSseConnection -Name 'Pode.Web.Actions' -ClientId $clientId
                }
            }

            $global:PageData = $null
        }
    }

    # add the page help route
    $helpPath = "/pode.web-dynamic/pages/$($pageMeta.ID)/help"
    if (($null -ne $HelpScriptBlock) -and !(Test-PodeWebRoute -Path $helpPath)) {
        Add-PodeRoute -Method Post -Path $helpPath -Authentication $pageMeta.Authentication -ArgumentList @{ Data = $ArgumentList; ID = $Id } -IfExists $IfExists -EndpointName $EndpointName -ScriptBlock {
            param($Data)
            $global:PageData = (Get-PodeWebState -Name 'pages')[$Data.ID]
            Set-PodeWebMetadata

            # get auth details of a user
            $authEnabled = ![string]::IsNullOrEmpty((Get-PodeWebState -Name 'auth'))
            $authMeta = $null

            if ($authEnabled) {
                $authData = Get-PodeAuthUser
                if ($null -ne $authData) {
                    $authMeta = @{
                        Username = (Get-PodeWebAuthUsername -User $authData)
                        Groups   = (Get-PodeWebAuthGroups -User $authData)
                    }
                }
            }

            # check access - 403 if denied
            if (!(Test-PodeWebPageAccess -PageAccess $global:PageData.Access -Auth $authMeta)) {
                Set-PodeResponseStatus -Code 403
            }
            else {
                $result = Invoke-PodeWebScriptBlock -Logic $global:PageData.Help -Arguments $Data.Data

                if (($null -ne $result) -and !$WebEvent.Response.Headers.ContainsKey('Content-Disposition')) {
                    Write-PodeJsonResponse -Value $result
                }
            }

            $global:PageData = $null
        }
    }

    if ($PassThru) {
        return $pageMeta
    }
}

function Add-PodeWebPageLink {
    [CmdletBinding(DefaultParameterSetName = 'ScriptBlock')]
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(ParameterSetName = 'ScriptBlock')]
        [object[]]
        $Middleware,

        [Parameter()]
        [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
        $IfExists = 'Default',

        [Parameter()]
        [int]
        $Index = [int]::MaxValue,

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter()]
        [ValidateNotNull()]
        [string]
        $Group = [string]::Empty,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]
        $Icon = 'file',

        [Parameter(Mandatory = $true, ParameterSetName = 'ScriptBlock')]
        [scriptblock]
        $ScriptBlock,

        [Parameter(ParameterSetName = 'ScriptBlock')]
        [object[]]
        $ArgumentList,

        [Parameter(Mandatory = $true, ParameterSetName = 'Url')]
        [string]
        $Url,

        [Parameter()]
        [string[]]
        $AccessGroups = @(),

        [Parameter()]
        [string[]]
        $AccessUsers = @(),

        [Parameter(ParameterSetName = 'ScriptBlock')]
        [string[]]
        $EndpointName,

        [Parameter(ParameterSetName = 'ScriptBlock')]
        [Alias('NoAuth')]
        [switch]
        $NoAuthentication,

        [Parameter(ParameterSetName = 'Url')]
        [switch]
        $NewTab,

        [switch]
        $Hide
    )

    # test if group exists - otherwise create a basic group entry
    if (!(Test-PodeWebPageGroup -Name $Group)) {
        New-PodeWebPageGroup -Name $Group
    }

    # generate page ID
    $Id = Get-PodeWebPageId -Id $Id -Name $Name -Group $Group

    # test if page/page-link exists
    if (Test-PodeWebPage -Id $Id) {
        throw "Web page/link already exists: $($Name) [Group: $($Group)]"
    }

    # set page title
    if ([string]::IsNullOrWhiteSpace($DisplayName)) {
        $DisplayName = $Name
    }

    # check for scoped vars
    $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

    # setup page meta
    $pageMeta = @{
        Operation        = 'New'
        ComponentType    = 'Page'
        ObjectType       = 'Link'
        ID               = $Id
        Index            = $Index
        Name             = $Name
        Group            = $Group
        DisplayName      = [System.Net.WebUtility]::HtmlEncode($DisplayName)
        NewTab           = $NewTab.IsPresent
        Icon             = $Icon
        Path             = (Get-PodeWebPagePath -Name $Name -Group $Group -NoAppPath)
        Url              = (Add-PodeWebAppPath -Url $Url)
        Hide             = $Hide.IsPresent
        IsDynamic        = ($null -ne $ScriptBlock)
        Logic            = @{
            ScriptBlock    = $ScriptBlock
            UsingVariables = $usingVars
        }
        Authentication   = $null
        NoAuthentication = $NoAuthentication.IsPresent
        Access           = @{
            Groups = @($AccessGroups)
            Users  = @($AccessUsers)
        }
        NoEvents         = $true
    }

    # does the page need auth?
    $auth = [string]::Empty
    if (!$pageMeta.NoAuthentication) {
        $auth = Get-PodeWebState -Name 'auth'
    }
    $pageMeta.Authentication = $auth

    # add page meta to state
    Register-PodeWebPage -Metadata $pageMeta

    # add page link
    if (($null -ne $ScriptBlock) -and !(Test-PodeWebRoute -Path $pageMeta.Path)) {
        if (Test-PodeIsEmpty $EndpointName) {
            $EndpointName = Get-PodeWebState -Name 'endpoint-name'
        }

        # remove the "root" page, if "root-redirect" was originally flagged and this page is for the root path
        if (($pageMeta.Path -eq '/') -and (Get-PodeWebState -Name 'root-redirect')) {
            Remove-PodeRoute -Method Get -Path '/'
        }

        # add the route
        Add-PodeRoute -Method Post -Path $pageMeta.Path -Authentication $pageMeta.Authentication -ArgumentList @{ Data = $ArgumentList; ID = $Id } -Middleware $Middleware -IfExists $IfExists -EndpointName $EndpointName -ScriptBlock {
            param($Data)
            $pageData = (Get-PodeWebState -Name 'pages')[$Data.ID]
            Set-PodeWebMetadata

            $result = Invoke-PodeWebScriptBlock -Logic $pageData.Logic -Arguments $Data.Data

            if (($null -ne $result) -and !$WebEvent.Response.Headers.ContainsKey('Content-Disposition')) {
                Write-PodeJsonResponse -Value $result
            }
        }
    }
}

function ConvertTo-PodeWebPage {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline = $true)]
        [string[]]
        $Commands,

        [Parameter()]
        [string]
        $Module,

        [switch]
        $GroupVerbs,

        [Parameter()]
        [Alias('NoAuth')]
        [switch]
        $NoAuthentication
    )

    # if a module was supplied, import it - then validate the commands
    if (![string]::IsNullOrWhiteSpace($Module)) {
        Import-PodeModule -Name $Module
        Export-PodeModule -Name $Module

        Write-Verbose 'Getting exported commands from module'
        $ModuleCommands = (Get-Module -Name $Module | Sort-Object -Descending | Select-Object -First 1).ExportedCommands.Keys

        # if commands were supplied validate them - otherwise use all exported ones
        if (Test-PodeIsEmpty $Commands) {
            Write-Verbose "Using all commands in $($Module) for converting to Pages"
            $Commands = $ModuleCommands
        }
        else {
            Write-Verbose "Validating supplied commands against module's exported commands"
            foreach ($cmd in $Commands) {
                if ($ModuleCommands -inotcontains $cmd) {
                    throw "Module $($Module) does not contain function $($cmd) to convert to a Page"
                }
            }
        }
    }

    # if there are no commands, fail
    if (Test-PodeIsEmpty $Commands) {
        throw 'No commands supplied to convert to Pages'
    }

    $sysParams = [System.Management.Automation.PSCmdlet]::CommonParameters.GetEnumerator() | Foreach-Object { $_ }

    # create the pages for each of the commands
    foreach ($cmd in $Commands) {
        Write-Verbose "Building page for $($cmd)"
        $cmdInfo = (Get-Command -Name $cmd -ErrorAction Stop)

        $sets = $cmdInfo.ParameterSets
        if (($null -eq $sets) -or ($sets.Length -eq 0)) {
            continue
        }

        # for cmdlets this will be null
        $ast = $cmdInfo.ScriptBlock.Ast
        $paramDefs = $null
        if ($null -ne $ast) {
            $paramDefs = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.ParameterAst] }, $true) | Where-Object {
                $_.Parent.Parent.Parent.Name -ieq $cmd
            }
        }

        $tabs = New-PodeWebTabs -Tabs @(foreach ($set in $sets) {
                # build name
                $name = $set.Name
                if ([string]::IsNullOrWhiteSpace($name) -or ($set.Name -iin @('__AllParameterSets'))) {
                    $name = 'Default'
                }

                # build input controls
                $elements = @(foreach ($param in $set.Parameters) {
                        if ($sysParams -icontains $param.Name) {
                            continue
                        }

                        $type = $param.ParameterType.Name

                        $default = $null
                        if ($null -ne $paramDefs) {
                            $default = ($paramDefs | Where-Object { $_.DefaultValue -and $_.Name.Extent.Text -ieq "`$$($param.Name)" }).DefaultValue.Value
                        }

                        if ($type -iin @('boolean', 'switchparameter')) {
                            New-PodeWebCheckbox -Name "$($name)_$($param.Name)" -DisplayName $param.Name -AsSwitch
                        }
                        else {
                            switch ($type) {
                                'pscredential' {
                                    New-PodeWebCredential -Name "$($name)_$($param.Name)" -DisplayName $param.Name
                                }

                                default {
                                    $multiple = $param.ParameterType.Name.EndsWith('[]')

                                    if ($param.Attributes.TypeId.Name -icontains 'ValidateSetAttribute') {
                                        $values = ($param.Attributes | Where-Object { $_.TypeId.Name -ieq 'ValidateSetAttribute' }).ValidValues
                                        New-PodeWebSelect -Name "$($name)_$($param.Name)" -DisplayName $param.Name -Options $values -SelectedValue $default -Multiple:$multiple
                                    }
                                    elseif ($param.ParameterType.BaseType.Name -ieq 'enum') {
                                        $values = [enum]::GetValues($param.ParameterType)
                                        New-PodeWebSelect -Name "$($name)_$($param.Name)" -DisplayName $param.Name -Options $values -SelectedValue $default -Multiple:$multiple
                                    }
                                    else {
                                        New-PodeWebTextbox -Name "$($name)_$($param.Name)" -DisplayName $param.Name -Value $default
                                    }
                                }
                            }
                        }
                    })

                $elements += (New-PodeWebHidden -Name '_Function_Name_' -Value $cmd)
                $elements += (New-PodeWebHidden -Name '_Parameter_Set_Name_' -Value $name)

                # build form
                $formId = "form_param_$($cmd)_$($name)"
                $form = New-PodeWebForm -Name "$($name)_Parameters_Form" -Id $formId -Content $elements -NoAuthentication:$NoAuthentication -ScriptBlock {
                    $cmd = $WebEvent.Data['_Function_Name_']
                    $WebEvent.Data.Remove('_Function_Name_')

                    $setName = $WebEvent.Data['_Parameter_Set_Name_']
                    $WebEvent.Data.Remove('_Parameter_Set_Name_')

                    $_args = @{}
                    foreach ($key in $WebEvent.Data.Keys) {
                        $argKey = $key -ireplace "$($setName)_", ''

                        if ($argKey -imatch '(?<name>.+)_(Username|Password)$') {
                            $name = $Matches['name']
                            $uKey = "$($argKey)_$($name)_Username"
                            $pKey = "$($argKey)_$($name)_Password"

                            if (![string]::IsNullOrWhiteSpace($WebEvent.Data[$uKey]) -and ![string]::IsNullOrWhiteSpace($WebEvent.Data[$pKey])) {
                                $creds = (New-Object System.Management.Automation.PSCredential -ArgumentList $WebEvent.Data[$uKey], (ConvertTo-SecureString -AsPlainText $WebEvent.Data[$pKey] -Force))
                                $_args[$name] = $creds
                            }
                        }
                        else {
                            if ($WebEvent.Data[$key] -iin @('true', 'false')) {
                                $_args[$argKey] = ($WebEvent.Data[$key] -ieq 'true')
                            }
                            else {
                                if ($WebEvent.Data[$key].Contains(',')) {
                                    $_args[$argKey] = ($WebEvent.Data[$key] -isplit ',' | ForEach-Object { $_.Trim() })
                                }
                                else {
                                    $_args[$argKey] = $WebEvent.Data[$key]
                                }
                            }
                        }
                    }

                    try {
                    (. $cmd @_args) |
                            New-PodeWebTextbox -Name 'Output_Result' -Multiline -Preformat |
                            Out-PodeWebElement
                    }
                    catch {
                        $_.Exception |
                            New-PodeWebTextbox -Name 'Output_Error' -Multiline -Preformat |
                            Out-PodeWebElement
                    }
                }

                $card = New-PodeWebCard -Name "$($name)_Parameters" -DisplayName 'Parameters' -Content $form
                New-PodeWebTab -Name $name -Content $card
            })

        $group = [string]::Empty
        if ($GroupVerbs) {
            $group = $cmdInfo.Verb
            if ([string]::IsNullOrWhiteSpace($group)) {
                $group = '_'
            }
        }

        Add-PodeWebPage `
            -Name $cmd `
            -Icon Settings `
            -Content $tabs `
            -Group $group `
            -NoAuthentication:$NoAuthentication
    }
}

function Use-PodeWebPages {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path
    )

    # use default ./pages, or custom path
    if ([string]::IsNullOrWhiteSpace($Path)) {
        $Path = Join-Path (Get-PodeServerPath) 'pages'
    }
    elseif ($Path.StartsWith('.')) {
        $Path = Join-Path (Get-PodeServerPath) $Path
    }

    # fail if path not found
    if (!(Test-Path -Path $Path)) {
        throw "Path to load pages not found: $($Path)"
    }

    # get .ps1 files and load them
    Get-ChildItem -Path $Path -Filter *.ps1 -Force -Recurse | ForEach-Object {
        Use-PodeScript -Path $_.FullName
    }
}

function Get-PodeWebPage {
    [CmdletBinding(DefaultParameterSetName = 'Name')]
    param(
        [Parameter(ParameterSetName = 'Id')]
        [string]
        $Id,

        [Parameter(ParameterSetName = 'Name')]
        [string]
        $Name,

        [Parameter(ParameterSetName = 'Name')]
        [string]
        $Group,

        [Parameter(ParameterSetName = 'Name')]
        [switch]
        $NoGroup
    )

    # get all pages
    $pages = Get-PodeWebState -Name 'pages'
    if (($null -eq $pages) -or ($pages.Count -eq 0)) {
        return $null
    }

    # if ID, check
    if (![string]::IsNullOrWhiteSpace($Id)) {
        return $pages[$Id]
    }

    # get page values
    $pages = $pages.Values

    # filter by group
    if ($NoGroup -and [string]::IsNullOrWhiteSpace($Group)) {
        $pages = @(foreach ($page in $pages) {
                if ([string]::IsNullOrWhiteSpace($page.Group)) {
                    $page
                }
            })
    }
    elseif (![string]::IsNullOrWhiteSpace($Group)) {
        $pages = @(foreach ($page in $pages) {
                if ($page.Group -ieq $Group) {
                    $page
                }
            })
    }

    # filter by page name
    if (![string]::IsNullOrWhiteSpace($Name)) {
        $pages = @(foreach ($page in $pages) {
                if ($page.Name -ieq $Name) {
                    $page
                }
            })
    }

    # return filtered pages
    return $pages
}

function Test-PodeWebPage {
    [CmdletBinding(DefaultParameterSetName = 'Name')]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'Id')]
        [string]
        $Id,

        [Parameter(Mandatory = $true, ParameterSetName = 'Name')]
        [string]
        $Name,

        [Parameter(ParameterSetName = 'Name')]
        [string]
        $Group,

        [Parameter(ParameterSetName = 'Name')]
        [switch]
        $NoGroup
    )

    # by ID
    if (![string]::IsNullOrWhiteSpace($Id)) {
        return (Get-PodeWebState -Name 'pages').ContainsKey($Id)
    }

    # by Name/Group
    else {
        # get pages
        $pages = Get-PodeWebPage -Name $Name -Group $Group -NoGroup:$NoGroup

        # are there any pages?
        if ($null -eq $pages) {
            return $false
        }

        return (@($pages) | Measure-Object).Count -gt 0
    }
}

function New-PodeWebPageGroup {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [string]
        $DisplayName,

        [Parameter()]
        [string]
        $Icon,

        [switch]
        $NoCounter,

        [switch]
        $Hide,

        [switch]
        $PassThru
    )

    # test if page group exists
    if (Test-PodeWebPageGroup -Name $Name) {
        throw "Page Group already exists: $($Name)"
    }

    # set display name
    if ([string]::IsNullOrEmpty($DisplayName)) {
        $DisplayName = $Name
    }

    # setup group meta
    $groupMeta = @{
        Operation     = 'New'
        ComponentType = 'Group'
        ObjectType    = 'Group'
        ID            = Get-PodeWebRandomName
        Name          = $Name
        DisplayName   = [System.Net.WebUtility]::HtmlEncode($DisplayName)
        Icon          = $Icon
        NoCounter     = $NoCounter.IsPresent
        Hide          = $Hide.IsPresent
        Pages         = @{}
    }

    # add group meta to state
    $groups = Get-PodeWebState -Name 'groups'
    $groups[$Name] = $groupMeta

    if ($PassThru) {
        return $groupMeta
    }
}

function Get-PodeWebPageGroup {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Name = $null
    )

    $groups = Get-PodeWebState -Name 'groups'

    # get all groups on null
    if ($null -eq $Name) {
        return $groups
    }

    # return specific group
    return $groups[$Name]
}

function Test-PodeWebPageGroup {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Name
    )

    return (Get-PodeWebState -Name 'groups').ContainsKey($Name)
}

function Remove-PodeWebPageGroup {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Name
    )

    $null = (Get-PodeWebState -Name 'groups').Remove($Name)
}