Public/Install-MakeMeAdminService.ps1

#Requires -Version 5.1
<#
.SYNOPSIS
    Installs the MakeMeAdminCLI background service.
 
.DESCRIPTION
    The Install-MakeMeAdminService cmdlet configures the background service
    components required for MakeMeAdminCLI to function. This is a one-time
    setup step that must be run from an elevated PowerShell session.
 
.NOTES
    Author: MakeMeAdminCLI
    Version: 1.1.0
#>


function Install-MakeMeAdminService {
    <#
    .SYNOPSIS
        Installs the MakeMeAdminCLI background service.
 
    .DESCRIPTION
        Configures all service components required for MakeMeAdminCLI to function:
        - Creates the state directory at $env:ProgramData\MakeMeAdminCLI\
        - Copies the default config.json to the state directory
        - Initializes the state.json tracking file
        - Registers the Windows Event Log source
        - Creates a scheduled task folder under Task Scheduler
        - Registers and starts the MakeMeAdminCLI-Service scheduled task
 
        The scheduled task runs Private\Service-Main.ps1 as NT AUTHORITY\SYSTEM
        at startup. It listens on a named pipe for elevation requests from
        standard users.
 
        This cmdlet requires administrator privileges. It must be run once after
        installing the module via Install-Module.
 
    .PARAMETER Force
        Reinstalls the service even if it is already configured. Existing
        configuration is preserved unless the config.json is missing.
 
    .OUTPUTS
        PSCustomObject with installation status for each component:
        - StateDirectory: Boolean
        - ConfigFile: Boolean
        - StateFile: Boolean
        - EventLogSource: Boolean
        - ScheduledTaskFolder: Boolean
        - ScheduledTask: Boolean
        - ServiceStarted: Boolean
        - OverallSuccess: Boolean
 
    .EXAMPLE
        Install-MakeMeAdminService
 
        Performs a standard installation of the service components.
 
    .EXAMPLE
        Install-MakeMeAdminService -Force
 
        Reinstalls the service, recreating the scheduled task even if it
        already exists.
 
    .EXAMPLE
        Install-MakeMeAdminService -Verbose
 
        Installs with detailed progress output.
 
    .LINK
        Uninstall-MakeMeAdminService
        Test-MakeMeAdminService
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([PSCustomObject])]
    param(
        [switch]$Force
    )

    $ErrorActionPreference = 'Stop'

    # --- Constants ---
    $ModuleName       = 'MakeMeAdminCLI'
    $TaskName         = 'MakeMeAdminCLI-Service'
    $TaskPath         = '\Microsoft\Windows\MakeMeAdminCLI\'
    $EventLogSource   = 'MakeMeAdminCLI'
    $StateDirectory   = Join-Path $env:ProgramData $ModuleName
    $ConfigFilePath   = Join-Path $StateDirectory 'config.json'
    $StateFilePath    = Join-Path $StateDirectory 'state.json'

    # Determine module root from the script location
    $moduleRoot = Split-Path -Parent (Split-Path -Parent $PSCommandPath)

    # --- Result tracker ---
    $result = [ordered]@{
        StateDirectory      = $false
        ConfigFile          = $false
        StateFile           = $false
        EventLogSource      = $false
        ScheduledTaskFolder = $false
        ScheduledTask       = $false
        ServiceStarted      = $false
        OverallSuccess      = $false
    }

    #region Elevation Check

    if (-not (Test-IsElevated)) {
        $PSCmdlet.ThrowTerminatingError(
            [System.Management.Automation.ErrorRecord]::new(
                [System.UnauthorizedAccessException]::new(
                    'Install-MakeMeAdminService must be run from an elevated (Administrator) PowerShell session.'
                ),
                'ElevationRequired',
                [System.Management.Automation.ErrorCategory]::PermissionDenied,
                $null
            )
        )
        return
    }

    #endregion

    #region Pre-flight: check for existing installation

    $existingTask = $null
    try {
        $existingTask = Get-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -ErrorAction SilentlyContinue
    }
    catch {
        # Task doesn't exist, that's fine
    }

    if ($existingTask -and -not $Force) {
        Write-Warning "MakeMeAdminCLI service is already installed. Use -Force to reinstall."
        # Still return current status
        $result.StateDirectory      = Test-Path $StateDirectory
        $result.ConfigFile          = Test-Path $ConfigFilePath
        $result.StateFile           = Test-Path $StateFilePath
        $result.EventLogSource      = $true
        $result.ScheduledTaskFolder = $true
        $result.ScheduledTask       = $true
        $result.ServiceStarted      = $existingTask.State -eq 'Running'
        $result.OverallSuccess      = $true
        return [PSCustomObject]$result
    }

    #endregion

    #region Stop existing service if running

    if ($existingTask -and $existingTask.State -eq 'Running') {
        Write-Verbose 'Stopping existing service scheduled task...'
        try {
            Stop-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -ErrorAction SilentlyContinue
            Start-Sleep -Seconds 2
        }
        catch {
            Write-Verbose "Could not stop existing task: $($_.Exception.Message)"
        }
    }

    #endregion

    #region Step 1: Create state directory

    if ($PSCmdlet.ShouldProcess($StateDirectory, 'Create state directory')) {
        try {
            if (-not (Test-Path $StateDirectory)) {
                New-Item -ItemType Directory -Path $StateDirectory -Force | Out-Null
                Write-Verbose "Created state directory: $StateDirectory"
            }
            else {
                Write-Verbose "State directory already exists: $StateDirectory"
            }

            # Create Scripts subdirectory for removal scripts
            $scriptsDir = Join-Path $StateDirectory 'Scripts'
            if (-not (Test-Path $scriptsDir)) {
                New-Item -ItemType Directory -Path $scriptsDir -Force | Out-Null
                Write-Verbose "Created scripts directory: $scriptsDir"
            }

            $result.StateDirectory = $true
        }
        catch {
            Write-Warning "Failed to create state directory: $($_.Exception.Message)"
        }
    }

    #endregion

    #region Step 2: Copy config.json

    if ($PSCmdlet.ShouldProcess($ConfigFilePath, 'Deploy default configuration')) {
        try {
            $sourceConfig = Join-Path $moduleRoot 'config.json'

            if (-not (Test-Path $ConfigFilePath) -or $Force) {
                if (Test-Path $sourceConfig) {
                    Copy-Item -Path $sourceConfig -Destination $ConfigFilePath -Force
                    Write-Verbose "Deployed config.json to $ConfigFilePath"
                }
                else {
                    Write-Warning "Default config.json not found at $sourceConfig"
                }
            }
            else {
                Write-Verbose "Config.json already exists at $ConfigFilePath (use -Force to overwrite)"
            }

            $result.ConfigFile = Test-Path $ConfigFilePath
        }
        catch {
            Write-Warning "Failed to deploy config.json: $($_.Exception.Message)"
        }
    }

    #endregion

    #region Step 3: Initialize state.json

    if ($PSCmdlet.ShouldProcess($StateFilePath, 'Initialize state file')) {
        try {
            if (-not (Test-Path $StateFilePath)) {
                $initialState = @{
                    ActiveUsers      = @()
                    LastUpdated      = (Get-Date).ToString('o')
                    ServiceStartTime = $null
                }
                $initialState | ConvertTo-Json -Depth 10 |
                    Set-Content -Path $StateFilePath -Encoding UTF8 -Force
                Write-Verbose "Initialized state.json at $StateFilePath"
            }
            else {
                Write-Verbose "state.json already exists at $StateFilePath"
            }

            $result.StateFile = Test-Path $StateFilePath
        }
        catch {
            Write-Warning "Failed to initialize state.json: $($_.Exception.Message)"
        }
    }

    #endregion

    #region Step 4: Register Event Log source

    if ($PSCmdlet.ShouldProcess("Application log source '$EventLogSource'", 'Register Event Log source')) {
        try {
            if (-not [System.Diagnostics.EventLog]::SourceExists($EventLogSource)) {
                [System.Diagnostics.EventLog]::CreateEventSource($EventLogSource, 'Application')
                Start-Sleep -Milliseconds 500
                Write-Verbose "Registered Event Log source '$EventLogSource'"
            }
            else {
                Write-Verbose "Event Log source '$EventLogSource' already registered"
            }

            $result.EventLogSource = $true
        }
        catch {
            Write-Warning "Could not register Event Log source: $($_.Exception.Message)"
            # Non-fatal
        }
    }

    #endregion

    #region Step 5: Create scheduled task folder

    if ($PSCmdlet.ShouldProcess($TaskPath, 'Create Task Scheduler folder')) {
        try {
            $schedule = New-Object -ComObject Schedule.Service
            $schedule.Connect()

            try {
                $null = $schedule.GetFolder($TaskPath)
                Write-Verbose "Task Scheduler folder already exists: $TaskPath"
            }
            catch {
                # Create the folder hierarchy
                $pathParts = $TaskPath.Trim('\').Split('\')
                $currentPath = '\'
                foreach ($part in $pathParts) {
                    $nextPath = "$currentPath$part"
                    try {
                        $null = $schedule.GetFolder($nextPath)
                    }
                    catch {
                        $parentFolder = $schedule.GetFolder($currentPath.TrimEnd('\'))
                        $null = $parentFolder.CreateFolder($part)
                    }
                    $currentPath = "$nextPath\"
                }
                Write-Verbose "Created Task Scheduler folder: $TaskPath"
            }

            $result.ScheduledTaskFolder = $true
        }
        catch {
            Write-Warning "Failed to create Task Scheduler folder: $($_.Exception.Message)"
        }
    }

    #endregion

    #region Step 6: Register the scheduled task

    if ($PSCmdlet.ShouldProcess("$TaskPath$TaskName", 'Register scheduled task')) {
        try {
            # Remove existing task if present (Force path)
            $existingTask = Get-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -ErrorAction SilentlyContinue
            if ($existingTask) {
                Unregister-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -Confirm:$false
                Write-Verbose 'Removed existing scheduled task'
            }

            # Build the path to Service-Main.ps1 from the installed module location
            $servicePath = Join-Path $moduleRoot 'Private\Service-Main.ps1'

            $action = New-ScheduledTaskAction -Execute 'powershell.exe' `
                -Argument "-ExecutionPolicy Bypass -WindowStyle Hidden -NonInteractive -File `"$servicePath`""

            $trigger = New-ScheduledTaskTrigger -AtStartup

            $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' `
                -LogonType ServiceAccount `
                -RunLevel Highest

            $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries `
                -DontStopIfGoingOnBatteries `
                -StartWhenAvailable `
                -RestartCount 3 `
                -RestartInterval (New-TimeSpan -Minutes 1) `
                -ExecutionTimeLimit (New-TimeSpan -Days 365) `
                -Priority 4 `
                -Hidden

            $null = Register-ScheduledTask -TaskName $TaskName `
                -TaskPath $TaskPath `
                -Action $action `
                -Trigger $trigger `
                -Principal $principal `
                -Settings $settings `
                -Description 'MakeMeAdminCLI service - Provides temporary administrator rights to users'

            Write-Verbose 'Registered scheduled task'
            $result.ScheduledTask = $true
        }
        catch {
            Write-Warning "Failed to register scheduled task: $($_.Exception.Message)"
        }
    }

    #endregion

    #region Step 7: Start the service

    if ($result.ScheduledTask -and $PSCmdlet.ShouldProcess($TaskName, 'Start scheduled task')) {
        try {
            Start-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath
            Start-Sleep -Seconds 2

            $taskInfo = Get-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath
            if ($taskInfo.State -eq 'Running') {
                Write-Verbose 'MakeMeAdminCLI service started successfully'
                $result.ServiceStarted = $true
            }
            else {
                Write-Warning "Service task started but current state is: $($taskInfo.State)"
            }
        }
        catch {
            Write-Warning "Could not start service immediately: $($_.Exception.Message). The service will start automatically on next boot."
        }
    }

    #endregion

    # Determine overall success
    $result.OverallSuccess = $result.StateDirectory -and
                             $result.ConfigFile -and
                             $result.StateFile -and
                             $result.ScheduledTask

    return [PSCustomObject]$result
}

# Export the function
Export-ModuleMember -Function 'Install-MakeMeAdminService'