1527 lines
54 KiB
PowerShell
1527 lines
54 KiB
PowerShell
# region changes to be done
|
||
# if (Get-Command msedge.exe -ErrorAction SilentlyContinue) {
|
||
# Start-Process msedge.exe -ArgumentList "--app=http://localhost:$Port"
|
||
#} else {
|
||
# Start-Process "microsoft-edge:http://localhost:$Port"
|
||
#}
|
||
#endregion
|
||
|
||
<#
|
||
.SYNOPSIS
|
||
ScriptMonkey - MSP client onboarding/offboarding toolkit with a user interface,
|
||
and optional silent install of the SVSMSP toolkit and headless DattoRMM deployment.
|
||
|
||
.DESCRIPTION
|
||
Provides an HTTP-hosted GUI for selecting and running tasks (module installs, tweaks, offboarding, etc.).
|
||
When invoked with the correct parameters, it can silently install the SVSMSP toolkit and perform a headless
|
||
DattoRMM deployment without ever launching the browser or UI.
|
||
|
||
.PARAMETER SilentInstall
|
||
Runs only the SVSMSP module install (Install-Toolkit) and skips launching the browser/UI.
|
||
|
||
.PARAMETER DattoApiUrl
|
||
The Datto Automate API base URL for headless deployment.
|
||
|
||
.PARAMETER DattoApiKey
|
||
Your Datto Automate API username.
|
||
|
||
.PARAMETER DattoApiSecretKey
|
||
Your Datto Automate API password/secret.
|
||
|
||
.PARAMETER SiteUID
|
||
The target Datto site UID for headless installation.
|
||
|
||
.PARAMETER SiteName
|
||
The target Datto site name for headless installation.
|
||
|
||
.PARAMETER PushSiteVars
|
||
Switch to include site variables in the headless DattoRMM install.
|
||
|
||
.PARAMETER InstallRMM
|
||
Switch to install the RMM agent in the headless DattoRMM install.
|
||
|
||
.PARAMETER SaveCopy
|
||
Switch to download the RMM installer executable during the headless DattoRMM install.
|
||
|
||
.EXAMPLE
|
||
& ([ScriptBlock]::Create(
|
||
(iwr 'https://sm.svstools.com' -UseBasicParsing).Content
|
||
)) `
|
||
-N8nPassword 'pwd' `
|
||
-SiteUID 'site-123' `
|
||
-SiteName 'Acme Corp' `
|
||
-InstallRMM `
|
||
-PushSiteVars `
|
||
-SaveCopy `
|
||
-WhatIf
|
||
|
||
.EXAMPLE
|
||
& ([ScriptBlock]::Create(
|
||
(iwr 'https://sm.svstools.com/ScriptMonkey.ps1' -UseBasicParsing).Content
|
||
)) `
|
||
-N8nPassword 's3cr3t' `
|
||
-FetchSitesOnly `
|
||
-OutputFile 'sites.json'
|
||
|
||
.EXAMPLE
|
||
& ([ScriptBlock]::Create(
|
||
(iwr 'https://sm.svstools.com/ScriptMonkey.ps1' -UseBasicParsing).Content
|
||
)) `
|
||
-N8nPassword 's3cr3t' `
|
||
-FetchSitesOnly
|
||
# → writes datto_sites.csv
|
||
|
||
.EXAMPLE
|
||
Before i built the Invoke-scriptmonkey
|
||
& ([ScriptBlock]::Create( (iwr 'sm.svstools.ca').Content )) -SilentInstall
|
||
|
||
.EXAMPLE
|
||
not tested but i thin this is how would call it
|
||
iex (iwr 'https://your.server/ScriptMonkey.ps1' -UseBasicParsing).Content; Invoke-ScriptMonkey -DattoApiUrl
|
||
'https://…' -DattoApiKey '…' -DattoApiSecretKey '…' -SiteUID '…' -SiteName '…' -InstallRMM -PushSiteVars"
|
||
|
||
#>
|
||
|
||
#region ScriptMonkey run silently Entrypoint
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
# 1) ENTRYPOINT + PARAMETER DECLARATION
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
|
||
[CmdletBinding(
|
||
DefaultParameterSetName='UI',
|
||
SupportsShouldProcess=$true,
|
||
ConfirmImpact= 'Medium'
|
||
)]
|
||
param(
|
||
# ─────────────────────────────────────────────────────────
|
||
# Toolkit-only mode
|
||
[Parameter(Mandatory,ParameterSetName='Toolkit')][switch]$SilentInstall,
|
||
# ─────────────────────────────────────────────────────────
|
||
# Datto headless mode
|
||
|
||
# Both Datto sets share the webhook password
|
||
[Parameter(Mandatory,ParameterSetName='DattoFetch'),
|
||
Parameter(Mandatory,ParameterSetName='DattoInstall')][string]$N8nPassword,
|
||
|
||
# ─────────────────────────────────────────────────────────
|
||
# Fetch‐only set: write sites and exit
|
||
[Parameter(ParameterSetName='DattoFetch')][switch] $FetchSitesOnly,
|
||
[Parameter(ParameterSetName='DattoFetch')][string] $OutputFile = 'datto_sites.csv',
|
||
|
||
[Parameter(Mandatory,ParameterSetName='Datto')][string] $N8nPassword,
|
||
|
||
# ─────────────────────────────────────────────────────────
|
||
# Install set: target site must be provided
|
||
[Parameter(Mandatory,ParameterSetName='DattoInstall')][string] $SiteUID,
|
||
[Parameter(Mandatory,ParameterSetName='DattoInstall')][string] $SiteName,
|
||
[Parameter(ParameterSetName='DattoInstall')][switch] $PushSiteVars,
|
||
[Parameter(ParameterSetName='DattoInstall')][switch] $InstallRMM,
|
||
[Parameter(ParameterSetName='DattoInstall')][switch] $SaveCopy
|
||
)
|
||
|
||
#region — guarantee NuGet provider is present without prompting
|
||
|
||
# ─── Top of script ───
|
||
Import-Module PackageManagement -Force -ErrorAction SilentlyContinue | Out-Null
|
||
Import-Module PowerShellGet -Force -ErrorAction SilentlyContinue | Out-Null
|
||
|
||
# ─── ensure TLS 1.2 + no prompts ───
|
||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||
$ProgressPreference = 'SilentlyContinue'
|
||
$ConfirmPreference = 'None'
|
||
|
||
# check if NuGet exists (no output—assigned to $nuget)
|
||
$nuget = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue
|
||
|
||
if (-not $nuget) {
|
||
# install it (again, assignment suppresses the table)
|
||
Install-PackageProvider `
|
||
-Name NuGet `
|
||
-MinimumVersion 2.8.5.201 `
|
||
-Force `
|
||
-Confirm:$false
|
||
|
||
|
||
# re-query just for version info
|
||
$found = Get-PackageProvider -Name NuGet -ListAvailable
|
||
Write-Host "Installed NuGet provider v$($found.Version)" -ForegroundColor Green
|
||
}
|
||
else {
|
||
Write-Host "NuGet provider already present (v$($found.Version))" -ForegroundColor DarkGray
|
||
}
|
||
|
||
# now import it silently
|
||
Import-PackageProvider -Name NuGet -Force -ErrorAction SilentlyContinue | Out-Null
|
||
|
||
# ensure trust PSGallery without its own output (so you don't get “untrusted repository” prompt
|
||
$gallery = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue
|
||
if ($gallery.InstallationPolicy -ne 'Trusted') {
|
||
Set-PSRepository `
|
||
-Name PSGallery `
|
||
-InstallationPolicy Trusted `
|
||
-ErrorAction SilentlyContinue | Out-Null
|
||
|
||
Write-Host "PSGallery marked as Trusted" -ForegroundColor Green
|
||
}
|
||
|
||
# …and only *after* this completes* do you Start-Server…
|
||
Write-Host "Starting ScriptMonkey UI on http://localhost:$Port/" -ForegroundColor Cyan
|
||
|
||
#endregion
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
# 2) GLOBAL SETTINGS & HELPERS
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
|
||
# Listening port for HTTP UI
|
||
$Port = 8082
|
||
|
||
# Configurable endpoints
|
||
$Global:DattoWebhookUrl = 'https://automate.svstools.ca/webhook/svsmspkit'
|
||
|
||
#region Get-DattoApiCredentials
|
||
function Get-DattoApiCredentials {
|
||
[CmdletBinding()]
|
||
param (
|
||
[Parameter(Mandatory)][string]$Password
|
||
)
|
||
$headers = @{ "SVSMSPKit" = $Password }
|
||
try {
|
||
$resp = Invoke-RestMethod -Uri $Global:DattoWebhookUrl `
|
||
-Headers $headers `
|
||
-Method GET
|
||
return @{
|
||
ApiUrl = $resp.ApiUrl
|
||
ApiKey = $resp.ApiKey
|
||
ApiSecretKey = $resp.ApiSecretKey
|
||
}
|
||
}
|
||
catch {
|
||
Write-LogHybrid "Failed to fetch API credentials: $($_.Exception.Message)" Error DattoAuth
|
||
return $null
|
||
}
|
||
}
|
||
|
||
#region Logging Helpers
|
||
|
||
# Initialize a global in-memory log cache
|
||
if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) {
|
||
$Global:LogCache = [System.Collections.ArrayList]::new()
|
||
}
|
||
|
||
# Core Write-Log function (advanced with event-log support)
|
||
function Write-LogHelper {
|
||
[CmdletBinding()]
|
||
param(
|
||
[Parameter(Mandatory)][string]$Message,
|
||
[ValidateSet("Info","Warning","Error","Success","General")][string]$Level = "Info",
|
||
[string]$TaskCategory = "GeneralTask",
|
||
[switch]$LogToEvent, [string]$EventSource="SVSMSP_Module", [string]$EventLog="Application",
|
||
[int]$CustomEventID, [string]$LogFile, [switch]$PassThru
|
||
)
|
||
# IDs & colors
|
||
$idMap = @{ Info=1000; Warning=2000; Error=3000; Success=4000; General=1000 }
|
||
$colMap= @{ Info="Cyan"; Warning="Yellow"; Error="Red"; Success="Green"; General="White" }
|
||
$EventID = if ($PSBoundParameters.CustomEventID) { $CustomEventID } else { $idMap[$Level] }
|
||
$color = $colMap[$Level]
|
||
$fmt = "[$Level] [$TaskCategory] $Message (Event ID: $EventID)"
|
||
Write-Host $fmt -ForegroundColor $color
|
||
|
||
# cache
|
||
if (-not $Global:LogCache) { $Global:LogCache = @() }
|
||
$entry = [pscustomobject]@{ Timestamp=(Get-Date -Format "yyyy-MM-dd HH:mm:ss"); Level=$Level; Message=$fmt }
|
||
$Global:LogCache += $entry
|
||
|
||
# file
|
||
if ($PSBoundParameters.LogFile) {
|
||
try { "$($entry.Timestamp) $fmt" | Out-File $LogFile -Append -Encoding UTF8 }
|
||
catch { Write-Host "[Warning] File log failed: $_" -ForegroundColor Yellow }
|
||
}
|
||
|
||
# event log
|
||
if ($LogToEvent) {
|
||
$etype = if ($Level -in 'Warning','Error') { $Level } else { 'Information' }
|
||
try {
|
||
if (-not (Get-EventLog -LogName $EventLog -Source $EventSource -ErrorAction SilentlyContinue)) {
|
||
New-EventLog -LogName $EventLog -Source $EventSource
|
||
}
|
||
$msg = "TaskCategory:$TaskCategory | Message:$Message"
|
||
Write-EventLog -LogName $EventLog -Source $EventSource -EntryType $etype -EventID $EventID -Message $msg
|
||
} catch { Write-Host "[Warning] EventLog failed: $_" -ForegroundColor Yellow }
|
||
}
|
||
|
||
if ($PassThru) { return $entry }
|
||
}
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
# WRITE-LOG HYBRID (single definition, chooses at runtime)
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
function Write-LogHybrid {
|
||
[CmdletBinding()]
|
||
param(
|
||
[Parameter(Mandatory=$true)][string]$Message,
|
||
[ValidateSet("Info","Warning","Error","Success","General")]
|
||
[string]$Level = "Info",
|
||
[string]$TaskCategory = "GeneralTask",
|
||
[switch]$LogToEvent
|
||
)
|
||
|
||
if ( Get-Command -Name Write-Log -ErrorAction SilentlyContinue ) {
|
||
# SVSMSP module's Write-Log is available
|
||
Write-Log -Message $Message -Level $Level -TaskCategory $TaskCategory -LogToEvent:$LogToEvent
|
||
}
|
||
else {
|
||
# fall back to your helper
|
||
Write-LogHelper -Message $Message -Level $Level -TaskCategory $TaskCategory -LogToEvent:$LogToEvent
|
||
}
|
||
}
|
||
|
||
|
||
#endregion
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
# 3) MAIN LOGIC (Toolkit vs Datto vs UI)
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
|
||
switch ($PSCmdlet.ParameterSetName) {
|
||
'Toolkit' {
|
||
Write-LogHybrid "Toolkit-only mode" Info Startup
|
||
Install-SVSMSP -InstallToolkit
|
||
return
|
||
}
|
||
|
||
'Datto' {
|
||
Write-LogHybrid "Headless DattoRMM deploy (via n8n)" Info Startup
|
||
try{
|
||
# ────────────────────────────────────────────
|
||
# 1) Fetch URL, Key & Secret from n8n webhook
|
||
# ────────────────────────────────────────────
|
||
|
||
# 1) Fetch creds
|
||
$creds = Get-DattoApiCredentials -Password $N8nPassword
|
||
if (-not $creds) { throw "Could not fetch creds" }
|
||
|
||
# 2) Store in globals for everyone else
|
||
$Global:ApiUrl = $creds.ApiUrl
|
||
$Global:ApiKey = $creds.ApiKey
|
||
$Global:ApiSecretKey = $creds.ApiSecretKey
|
||
|
||
Write-LogHybrid "Fetched Datto API credentials from n8n" Success DattoAuth
|
||
}
|
||
catch {
|
||
Write-LogHybrid "N8N credential fetch error: $($_.Exception.Message)" Error DattoAuth
|
||
throw
|
||
}
|
||
|
||
# ───────────────────────────────────────────────────────────
|
||
# 2) If user only wants the site list, do that and exit
|
||
# ───────────────────────────────────────────────────────────
|
||
|
||
'DattoFetch' {
|
||
Write-LogHybrid "Fetching site list only…" Info DattoAuth
|
||
$sites = Get-DattoRmmSites -Password $N8nPassword
|
||
|
||
$ext = [IO.Path]::GetExtension($OutputFile).ToLower()
|
||
if ($ext -eq '.json') {
|
||
$sites | ConvertTo-Json -Depth 3 | Out-File -FilePath $OutputFile -Encoding UTF8
|
||
} else {
|
||
$sites | Export-Csv -Path $OutputFile -NoTypeInformation -Encoding UTF8
|
||
}
|
||
|
||
Write-LogHybrid "Wrote $($sites.Count) sites to $OutputFile" Success DattoAuth
|
||
return
|
||
}
|
||
|
||
|
||
|
||
# ────────────────────────────────────────────
|
||
# 3) Invoke the existing Install-DattoRMM cmdlet
|
||
# ────────────────────────────────────────────
|
||
|
||
'DattoInstall' {
|
||
Write-LogHybrid "Headless DattoRMM deploy" Info DattoAuth
|
||
if ($PSCmdlet.ShouldProcess("Datto site '$SiteName'", "Headless install")) {
|
||
Install-DattoRMM `
|
||
-ApiUrl $Global:ApiUrl `
|
||
-ApiKey $Global:ApiKey `
|
||
-ApiSecretKey $Global:ApiSecretKey `
|
||
-SiteUID $SiteUID `
|
||
-SiteName $SiteName `
|
||
-PushSiteVars:$PushSiteVars `
|
||
-InstallRMM:$InstallRMM `
|
||
-SaveCopy:$SaveCopy
|
||
}
|
||
|
||
return
|
||
|
||
}
|
||
}
|
||
#endregion ScriptMonkey run silently Entrypoint
|
||
|
||
|
||
# STACK = Scripted Tooling for Automated Client Kickoff
|
||
# MONKEY = Module-based Onboarding & Next-step Kickoff Engine Yoke
|
||
# Conveys the idea of coupling tasks together and keeping them under control.
|
||
|
||
#region Config & Task Definitions
|
||
|
||
# Define every task once here:
|
||
# Id → checkbox HTML `id`
|
||
# Name → URL path (`/Name`)
|
||
# Label → user-visible text
|
||
# HandlerFn → the PowerShell function to invoke
|
||
# Page → which tab/page it appears on
|
||
|
||
$Global:Tasks = @(
|
||
# On-Boarding, left column
|
||
@{ Id='setSVSPowerplan'; Name='setSVSPowerplan'; Label='Set SVS Powerplan'; HandlerFn='Handle-setSVSPowerPlan'; Page='onboard'; Column='left' },
|
||
@{ Id='installSVSMSPModule'; Name='installSVSMSPModule'; Label='Install SVSMSP Module'; HandlerFn='Handle-InstallSVSMSP'; Page='onboard'; Column='left' },
|
||
@{ Id='installCyberQP'; Name='installCyberQP'; Label='Install CyberQP'; HandlerFn='Handle-InstallCyberQP'; Page='onboard'; Column='left' },
|
||
@{ Id='installSVSHelpDesk'; Name='installSVSHelpDesk'; Label='Install SVS HelpDesk'; HandlerFn='Handle-InstallSVSHelpDesk'; Page='onboard'; Column='left' },
|
||
@{ Id='installThreatLocker'; Name='installThreatLocker'; Label='Install ThreatLocker'; HandlerFn='Handle-InstallThreatLocker'; Page='onboard'; Column='left' },
|
||
@{ Id='installRocketCyber'; Name='installRocketCyber'; Label='Install RocketCyber'; HandlerFn='Handle-InstallRocketCyber'; Page='onboard'; Column='left' },
|
||
@{ Id='installDattoRMM'; Name='installDattoRMM'; Label='Install DattoRMM'; HandlerFn='Handle-InstallDattoRMM'; Page='onboard'; Column='left';
|
||
SubOptions= @(
|
||
@{ Value='inputVar'; Label='Copy Site Variables' },
|
||
@{ Value='rmm'; Label='Install RMM Agent' },
|
||
@{ Value='exe'; Label='Download Executable' }
|
||
)
|
||
},
|
||
|
||
|
||
# On-Boarding, right column (optional bits)
|
||
@{ Id='enableBitLocker'; Name='EnableBitLocker'; Label='Enable BitLocker'; HandlerFn='Set-SVSBitLocker'; Page='onboard'; Column='right' },
|
||
@{ Id='setEdgeDefaultSearch';Name='setedgedefaultsearch';Label='Set Edge Default Search'; Tooltip='Will configure Edge to use Google as default search provider'; HandlerFn='set-EdgeDefaultSearchProvider';Page='onboard'; Column='right' },
|
||
|
||
# Off-Boarding
|
||
@{ Id='uninstallCyberQP'; Name='uninstallCyberQP'; Label='Uninstall CyberQP'; HandlerFn='Uninstall-CyberQP'; Page='offboard' },
|
||
@{ Id='uninstallSVSMSPModule'; Name='uninstallSVSMSPModule'; Label='Uninstall SVSMSP Module'; HandlerFn='Cleanup-SVSMSP'; Page='offboard' },
|
||
|
||
# Tweaks
|
||
@{ Id='disableAnimations'; Name='disableAnimations'; Label='Disable Animations'; HandlerFn='Disable-Animations'; Page='tweaks' },
|
||
|
||
# SVS Apps
|
||
@{ Id='wingetLastpass'; Name='wingetLastpass'; Label='LastPass Desktop App'; HandlerFn='Install-WingetLastPass'; Page='SVSApps' }
|
||
)
|
||
|
||
#endregion
|
||
|
||
|
||
# If we got here, it's the UI set—launch browser + listener:
|
||
# ——— UI fallback starts here ———
|
||
Write-LogHybrid "Launching UI" Info Startup
|
||
|
||
|
||
#region Handler Stubs
|
||
|
||
function Respond-Text {
|
||
param($Context, $Text)
|
||
$bytes = [Text.Encoding]::UTF8.GetBytes($Text)
|
||
$Context.Response.ContentType = 'text/plain'
|
||
$Context.Response.ContentLength64 = $bytes.Length
|
||
$Context.Response.OutputStream.Write($bytes,0,$bytes.Length)
|
||
$Context.Response.OutputStream.Close()
|
||
}
|
||
|
||
function Respond-HTML {
|
||
[CmdletBinding()]
|
||
param(
|
||
[Parameter(Mandatory = $true)][object] $Context,
|
||
[Parameter(Mandatory = $true)][string] $Html
|
||
)
|
||
$bytes = [Text.Encoding]::UTF8.GetBytes($Html)
|
||
$Context.Response.ContentType = 'text/html'
|
||
$Context.Response.ContentLength64 = $bytes.Length
|
||
$Context.Response.OutputStream.Write($bytes, 0, $bytes.Length)
|
||
$Context.Response.OutputStream.Close()
|
||
}
|
||
|
||
# new helper to return JSON
|
||
function Respond-JSON {
|
||
param($Context, $Object)
|
||
$json = $Object | ConvertTo-Json -Depth 5
|
||
$bytes = [Text.Encoding]::UTF8.GetBytes($json)
|
||
$Context.Response.ContentType = 'application/json'
|
||
$Context.Response.ContentLength64 = $bytes.Length
|
||
$Context.Response.OutputStream.Write($bytes,0,$bytes.Length)
|
||
$Context.Response.OutputStream.Close()
|
||
}
|
||
|
||
|
||
#region Install-DattoRMM-Helper
|
||
function Install-DattoRMM-Helper {
|
||
param (
|
||
[string]$ApiUrl,
|
||
[string]$ApiKey,
|
||
[string]$ApiSecretKey,
|
||
[switch]$FetchSitesOnly,
|
||
[string]$SiteName,
|
||
[string]$SiteUID
|
||
)
|
||
if (-not $ApiUrl -or -not $ApiKey -or -not $ApiSecretKey) {
|
||
Write-LogHybrid -Message "Missing required parameters. Please provide ApiUrl, ApiKey, and ApiSecretKey." -Level "Error" -LogToEvent
|
||
return
|
||
}
|
||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||
Write-LogHybrid -Message "Fetching OAuth token..." -Level "Info"
|
||
try {
|
||
$securePassword = ConvertTo-SecureString -String 'public' -AsPlainText -Force
|
||
$apiGenToken = Invoke-WebRequest -Credential (New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ('public-client', $securePassword)) `
|
||
-Uri ('{0}/auth/oauth/token' -f $ApiUrl) `
|
||
-Method 'POST' `
|
||
-ContentType 'application/x-www-form-urlencoded' `
|
||
-Body ('grant_type=password&username={0}&password={1}' -f $ApiKey, $ApiSecretKey) `
|
||
| ConvertFrom-Json
|
||
$requestToken = $apiGenToken.access_token
|
||
Write-LogHybrid -Message "OAuth token fetched successfully." -Level "Success" -LogToEvent
|
||
} catch {
|
||
Write-LogHybrid -Message "Failed to fetch OAuth token. Details: $($_.Exception.Message)" -Level "Error" -LogToEvent
|
||
return
|
||
}
|
||
$getHeaders = @{"Authorization" = "Bearer $requestToken"}
|
||
if ($FetchSitesOnly) {
|
||
Write-Host "Fetching list of sites from the Datto RMM API..." -ForegroundColor Cyan
|
||
try {
|
||
$getHeaders = @{"Authorization" = "Bearer $requestToken" }
|
||
$getSites = Invoke-WebRequest -Uri "$ApiUrl/api/v2/account/sites" -Method Get -Headers $getHeaders -ContentType "application/json"
|
||
$sitesJson = $getSites.Content | ConvertFrom-Json
|
||
$siteList = $sitesJson.sites | ForEach-Object {
|
||
[PSCustomObject]@{
|
||
Name = $_.name
|
||
UID = $_.uid
|
||
}
|
||
}
|
||
Write-Host "Successfully fetched list of sites." -ForegroundColor Green
|
||
return $siteList
|
||
}
|
||
catch {
|
||
Write-Host "Failed to fetch sites from the API. Details: $($_.Exception.Message)" -ForegroundColor Red
|
||
return
|
||
}
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
#region SVS Module
|
||
|
||
function Install-SVSMSP {
|
||
param (
|
||
[switch] $Cleanup,
|
||
[switch] $InstallToolkit,
|
||
[Parameter(Mandatory = $false)][array] $AllModules = @(@{ ModuleName = "SVS_Toolkit" }, @{ ModuleName = "SVSMSP" }),
|
||
[Parameter(Mandatory = $false)][array] $AllRepositories = @(@{ RepoName = "SVS_Repo" }, @{ RepoName = "SVS_Toolkit" }),
|
||
[Parameter(Mandatory = $false)][string] $NewModuleName = "SVSMSP",
|
||
[Parameter(Mandatory = $false)][string] $NewRepositoryName = "SVS_Repo",
|
||
[Parameter(Mandatory = $false)][string] $NewRepositoryURL = "http://proget.svstools.ca:8083/nuget/SVS_Repo/"
|
||
)
|
||
|
||
function Perform-Cleanup {
|
||
Write-LogHybrid "Cleanup mode enabled. Starting cleanup..." "Info" "SVSModule"
|
||
# …your old cleanup logic here…
|
||
}
|
||
|
||
function Perform-ToolkitInstallation {
|
||
Perform-Cleanup
|
||
Write-LogHybrid "Registering repo $NewRepositoryName…" "Info" "SVSModule"
|
||
if (-not (Get-PSRepository -Name $NewRepositoryName -ErrorAction SilentlyContinue)) {
|
||
Register-PSRepository -Name $NewRepositoryName -SourceLocation $NewRepositoryURL -InstallationPolicy Trusted
|
||
}
|
||
Write-LogHybrid "Installing module $NewModuleName…" "Info" "SVSModule"
|
||
Install-Module -Name $NewModuleName -Repository $NewRepositoryName -Scope AllUsers -Force
|
||
Write-LogHybrid "Toolkit installation complete." "Success" "SVSModule"
|
||
}
|
||
|
||
Write-LogHybrid "Install-SVSMSP called" "Info" "SVSModule"
|
||
if ($Cleanup) {
|
||
Perform-Cleanup; return
|
||
}
|
||
if ($InstallToolkit) {
|
||
Perform-ToolkitInstallation; return
|
||
}
|
||
# default if no switch passed:
|
||
Perform-ToolkitInstallation
|
||
}
|
||
|
||
#endregion
|
||
|
||
# POST /getpw → read JSON body, call helper, return JSON
|
||
|
||
|
||
function Handle-FetchSites {
|
||
param($Context)
|
||
|
||
# 1) Read incoming JSON (using block auto-disposes the reader)
|
||
|
||
$reader = [IO.StreamReader]::new($Context.Request.InputStream)
|
||
try {
|
||
$raw = $reader.ReadToEnd()
|
||
} finally {
|
||
$reader.Close()
|
||
}
|
||
|
||
try {
|
||
$pw = (ConvertFrom-Json $raw).password
|
||
} catch {
|
||
Write-LogHybrid "Invalid JSON in /getpw payload: $($_.Exception.Message)" "Error" "FetchSites"
|
||
returnRespondEmpty $Context
|
||
return
|
||
}
|
||
|
||
# 2) Fetch your Datto API creds from the webhook
|
||
Write-LogHybrid "Calling webhook for Datto credentials…" "Info" "FetchSites"
|
||
|
||
try {
|
||
$creds = Get-DattoApiCredentials -Password $pw
|
||
if (-not $creds) {
|
||
Write-LogHybrid "Webhook returned no credentials" Error FetchSites
|
||
returnRespondEmpty $Context 403
|
||
return
|
||
}
|
||
|
||
# reuse the same globals from the entrypoint
|
||
$Global:ApiUrl = $creds.ApiUrl
|
||
$Global:ApiKey = $creds.ApiKey
|
||
$Global:ApiSecretKey = $creds.ApiSecretKey
|
||
|
||
Write-LogHybrid "Fetched and stored API credentials." Success FetchSites
|
||
} catch {
|
||
Write-LogHybrid "Credential-fetch error: $($_.Exception.Message)" Error FetchSites -LogToEvent
|
||
returnRespondEmpty $Context 500
|
||
return
|
||
}
|
||
|
||
|
||
# 3) Exchange for a bearer token
|
||
Write-LogHybrid "Requesting OAuth token" "Info" "FetchSites"
|
||
try {
|
||
$securePublic = ConvertTo-SecureString 'public' -AsPlainText -Force
|
||
$creds = New-Object System.Management.Automation.PSCredential('public-client',$securePublic)
|
||
$tokenResp = Invoke-RestMethod `
|
||
-Uri "$Global:ApiUrl/auth/oauth/token" `
|
||
-Credential $creds `
|
||
-Method Post `
|
||
-ContentType 'application/x-www-form-urlencoded' `
|
||
-Body "grant_type=password&username=$Global:ApiKey&password=$Global:ApiSecretKey"
|
||
$token = $tokenResp.access_token
|
||
Write-LogHybrid "OAuth token acquired." "Success" "FetchSites"
|
||
} catch {
|
||
Write-LogHybrid "OAuth request failed: $($_.Exception.Message)" "Error" "FetchSites"
|
||
returnRespondEmpty $Context 500
|
||
return
|
||
}
|
||
|
||
# 4) Pull the site list
|
||
Write-LogHybrid "Fetching Datto RMM site list" "Info" "FetchSites"
|
||
try {
|
||
$hdr = @{ Authorization = "Bearer $token" }
|
||
$sitesResp = Invoke-RestMethod -Uri "$Global:ApiUrl/api/v2/account/sites" `
|
||
-Method Get `
|
||
-Headers $hdr `
|
||
-ContentType 'application/json'
|
||
|
||
$siteList = $sitesResp.sites | ForEach-Object {
|
||
[PSCustomObject]@{ Name = $_.name; UID = $_.uid }
|
||
}
|
||
Write-LogHybrid "Site list retrieved ($($siteList.Count) sites)." "Success" "FetchSites"
|
||
} catch {
|
||
Write-LogHybrid "Failed to fetch site list: $($_.Exception.Message)" "Error" "FetchSites"
|
||
returnRespondEmpty $Context 500
|
||
return
|
||
}
|
||
|
||
# 5) Return JSON array
|
||
$json = $siteList | ConvertTo-Json -Depth 2
|
||
$bytes = [Text.Encoding]::UTF8.GetBytes($json)
|
||
$Context.Response.ContentType = 'application/json'
|
||
$Context.Response.ContentLength64 = $bytes.Length
|
||
$Context.Response.OutputStream.Write($bytes, 0, $bytes.Length)
|
||
$Context.Response.OutputStream.Close()
|
||
}
|
||
|
||
|
||
|
||
# Helper function to consistently return an empty JSON array
|
||
function returnRespondEmpty {
|
||
param(
|
||
[Parameter(Mandatory)][object]$Context,
|
||
[Parameter(Mandatory)][ValidateRange(100,599)][int]$StatusCode = 500
|
||
)
|
||
# Always return an empty JSON array body
|
||
$empty = [Text.Encoding]::UTF8.GetBytes("[]")
|
||
|
||
# Set the desired status code and headers
|
||
$Context.Response.StatusCode = $StatusCode
|
||
$Context.Response.ContentType = 'application/json'
|
||
$Context.Response.ContentLength64 = $empty.Length
|
||
|
||
# Write and close
|
||
$Context.Response.OutputStream.Write($empty, 0, $empty.Length)
|
||
$Context.Response.OutputStream.Close()
|
||
}
|
||
|
||
|
||
|
||
# On-boarding handlers
|
||
function Handle-SetSVSPowerPlan {
|
||
param($Context)
|
||
|
||
# 1) call into your module
|
||
Set-SVSPowerPlan
|
||
|
||
# 2) log & write back a simple text response
|
||
Write-LogHybrid "PowerPlan set" "Success" "OnBoard"
|
||
Respond-Text $Context "PowerPlan applied"
|
||
}
|
||
|
||
function Handle-InstallSVSMSP {
|
||
param($Context)
|
||
Write-LogHybrid "HTTP trigger: Handle-InstallSVSMSP" "Info" "OnBoard"
|
||
try {
|
||
Install-SVSMSP -InstallToolkit
|
||
Respond-Text $Context "SVSMSP Module installed/updated."
|
||
} catch {
|
||
Write-LogHybrid "Error in Install-SVSMSP: $_" "Error" "OnBoard"
|
||
Respond-Text $Context "ERROR: $_"
|
||
}
|
||
}
|
||
|
||
function Handle-InstallCyberQP {
|
||
param($Context)
|
||
|
||
# 1) call into your module
|
||
Install-CyberQP
|
||
|
||
# 2) log & write back a simple text response
|
||
Write-LogHybrid "CyberQP installed" "Success" "OnBoard"
|
||
Respond-Text $Context "CyberQP installed"
|
||
}
|
||
|
||
function Handle-InstallThreatLocker {
|
||
param($Context)
|
||
|
||
# 1) call into your module
|
||
Install-ThreatLocker
|
||
|
||
# 2) log & write back a simple text response
|
||
Write-LogHybrid "ThreatLocker installed" "Success" "OnBoard"
|
||
Respond-Text $Context "ThreatLocker installed"
|
||
}
|
||
|
||
function Handle-InstallRocketCyber {
|
||
param($Context)
|
||
|
||
# 1) call into your module
|
||
Install-RocketCyber
|
||
|
||
# 2) log & write back a simple text response
|
||
Write-LogHybrid "RocketCyber installed" "Success" "OnBoard"
|
||
Respond-Text $Context "RocketCyber installed"
|
||
}
|
||
|
||
function Handle-InstallSVSHelpDesk {
|
||
param($Context)
|
||
|
||
# 1) call into your module
|
||
Install-SVSHelpDesk
|
||
|
||
# 2) log & write back a simple text response
|
||
Write-LogHybrid "SVS HelpDesk installed" "Success" "OnBoard"
|
||
Respond-Text $Context "SVS HelpDesk installed"
|
||
}
|
||
|
||
function Handle-InstallDattoRMM {
|
||
param($Context)
|
||
$req = $Context.Request
|
||
$resp = $Context.Response
|
||
|
||
if ($req.HttpMethod -ne 'POST') {
|
||
$resp.StatusCode = 405; $resp.ContentType = 'text/plain'
|
||
$resp.OutputStream.Write([Text.Encoding]::UTF8.GetBytes('Use POST'),0,7)
|
||
$resp.OutputStream.Close(); return
|
||
}
|
||
|
||
# parse JSON body
|
||
$body = (New-Object IO.StreamReader $req.InputStream).ReadToEnd()
|
||
$data = $body | ConvertFrom-Json
|
||
$checked = $data.checkedValues
|
||
$uid = $data.UID
|
||
$name = $data.Name
|
||
|
||
try {
|
||
Install-DattoRMM `
|
||
-ApiUrl $Global:ApiUrl `
|
||
-ApiKey $Global:ApiKey `
|
||
-ApiSecretKey $Global:ApiSecretKey `
|
||
-SiteUID $uid `
|
||
-SiteName $name `
|
||
-PushSiteVars:($checked -contains 'inputVar') `
|
||
-InstallRMM: ($checked -contains 'rmm') `
|
||
-SaveCopy: ($checked -contains 'exe')
|
||
|
||
Write-LogHybrid "RMM install triggered for $name" "Success" "DattoRMM"
|
||
$resp.StatusCode = 200
|
||
$responseString = "Triggered DattoRMM for $name"
|
||
}
|
||
catch {
|
||
Write-LogHybrid "Error in Install-DattoRMM: $_" "Error" "DattoRMM"
|
||
$resp.StatusCode = 500
|
||
$responseString = "ERROR: $($_.Exception.Message)"
|
||
}
|
||
|
||
$b = [Text.Encoding]::UTF8.GetBytes($responseString)
|
||
$resp.ContentType = 'text/plain'
|
||
$resp.ContentLength64 = $b.Length
|
||
$resp.OutputStream.Write($b,0,$b.Length)
|
||
$resp.OutputStream.Close()
|
||
}
|
||
|
||
|
||
# Off-boarding handlers
|
||
function Handle-UninstallCyberQP {
|
||
param($Context)
|
||
|
||
# 1) call into your module
|
||
Uninstall-CyberQP
|
||
|
||
Write-LogHybrid "CyberQP uninstalled" "Success" "OffBoard"
|
||
Respond-Text $Context "CyberQP uninstalled"
|
||
}
|
||
|
||
function Cleanup-SVSMSP {
|
||
param($Context)
|
||
Write-LogHybrid "SVSMSP cleaned up" "Success" "OffBoard"
|
||
Respond-Text $Context "SVSMSP cleaned up"
|
||
}
|
||
|
||
# Tweaks handler
|
||
function Disable-Animations {
|
||
param($Context)
|
||
Write-LogHybrid "Animations disabled" "Success" "Tweaks"
|
||
Respond-Text $Context "Animations disabled"
|
||
}
|
||
|
||
# SVSApps handler
|
||
function Install-WingetLastPass {
|
||
param($Context)
|
||
Write-LogHybrid "Winget LastPass installed" "Success" "SVSApps"
|
||
Respond-Text $Context "Winget LastPass installed"
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region UI Generation
|
||
|
||
function Build-Checkboxes {
|
||
param($Page, $Column)
|
||
|
||
(
|
||
$Global:Tasks |
|
||
Where-Object Page -EQ $Page |
|
||
Where-Object Column -EQ $Column |
|
||
ForEach-Object {
|
||
$taskId = $_.Id
|
||
$tooltip = if ($_.PSObject.Properties.Name -contains 'Tooltip' -and $_.Tooltip) {
|
||
" title='$($_.Tooltip)'"
|
||
} else { '' }
|
||
|
||
$html = "<label$tooltip><input type='checkbox' id='$taskId' name='$($_.Name)' data-column='$Column'> $($_.Label)</label>"
|
||
|
||
if ($_.SubOptions) {
|
||
# join inside the code block is fine
|
||
$subHtml = (
|
||
$_.SubOptions |
|
||
ForEach-Object {
|
||
"<label style='margin-left:20px; display:block;'>
|
||
<input type='checkbox' class='sub-option-$taskId' name='$($_.Value)' value='$($_.Value)'> $($_.Label)
|
||
</label>"
|
||
}
|
||
) -join "`n"
|
||
|
||
$html += @"
|
||
<div id='${taskId}OptionsContainer' style='display:none; margin-top:4px;'>
|
||
$subHtml
|
||
</div>
|
||
"@
|
||
}
|
||
|
||
$html
|
||
}
|
||
) -join "`n"
|
||
}
|
||
|
||
|
||
|
||
### Get SVSMSP module version to display in the UI
|
||
function Get-ModuleVersionHtml {
|
||
$mod = Get-Module -ListAvailable -Name SVSMSP | Sort-Object Version -Descending | Select-Object -First 1
|
||
if ($mod) {
|
||
return "<div style='color:#bbb; font-size:0.9em; margin-top:1em;'>Module Version: $($mod.Version)</div>"
|
||
}
|
||
return "<div style='color:#f66;'>SVSMSP_Module not found</div>"
|
||
}
|
||
|
||
|
||
function Get-UIHtml {
|
||
param([string]$Page = 'onboard')
|
||
|
||
#
|
||
# 1) Inline your full original CSS here
|
||
#
|
||
$style = @'
|
||
<style>
|
||
:root {
|
||
/* Cool Palette */
|
||
--background-color: rgba(18, 18, 18, 1);
|
||
--border-color: rgba(255,127,0,0.25);
|
||
/* Neutral Colors */
|
||
--white-color: rgba(255,255,255);
|
||
--gray-color: rgba(102,102,102);
|
||
--dark-gray-color: rgba(51,51,51);
|
||
--light-gray-color: rgba(187,187,187);
|
||
/* Sidebar Button Colors */
|
||
--btn-sidebar-light-gray: rgba(68,68,68);
|
||
--btn-sidebar-blue: rgba(30,144,255,1);
|
||
--btn-hover: rgba(0,86,179,1);
|
||
--btn-hover-scale: 1.05;
|
||
/* Button Colors */
|
||
--btn-success: rgba(40,167,69);
|
||
--btn-success-disabled: rgba(108,117,125);
|
||
--btn-danger: rgba(220,53,69);
|
||
}
|
||
|
||
body {
|
||
font-family: Arial, sans-serif;
|
||
margin: 0; padding: 0;
|
||
background-color: var(--background-color);
|
||
color: var(--white-color);
|
||
height: 100%; overflow: hidden;
|
||
}
|
||
|
||
|
||
.logo-container { display: grid; grid-template-columns: auto 1fr; align-items: center; padding: 20px; }
|
||
.logo-container img { max-width:300px; height:auto; }
|
||
.subtitle { font-size: 1.2rem; color: var(--gray-color); margin-top: 0.5em; }
|
||
|
||
.container { display:flex; height:100vh; overflow:hidden; }
|
||
.sidebar { width:200px; background:var(--background-color); padding:10px; }
|
||
.sidebar button {
|
||
display:block; width:100%; margin-bottom:10px; padding:10px;
|
||
color:var(--white-color); background:var(--btn-sidebar-light-gray);
|
||
border:none; border-radius:5px; cursor:pointer; text-align:left;
|
||
transition:background-color 0.3s, transform 0.2s;
|
||
}
|
||
.sidebar button.active { background:var(--btn-sidebar-blue); }
|
||
.sidebar button:hover {
|
||
background:var(--btn-hover); transform:scale(var(--btn-hover-scale));
|
||
}
|
||
|
||
.content {
|
||
position: relative;
|
||
flex:1;
|
||
padding:20px;
|
||
overflow-y:auto;
|
||
max-height:calc(100vh - 50px);
|
||
}
|
||
|
||
.fixed-buttons {
|
||
position: fixed;
|
||
bottom: 20px;
|
||
right: 20px;
|
||
display: flex;
|
||
gap: 10px; /* space between Exit and Run */
|
||
z-index: 1000;
|
||
}
|
||
|
||
.exit-button,
|
||
.run-button {
|
||
border: none;
|
||
border-radius: 5px;
|
||
padding: 10px 20px;
|
||
cursor: pointer;
|
||
color: var(--white-color);
|
||
}
|
||
|
||
/* Specific overrides */
|
||
.exit-button {
|
||
background-color: var(--btn-danger);
|
||
}
|
||
|
||
/* Specific overrides */
|
||
.run-button {
|
||
background-color: var(--btn-success);
|
||
}
|
||
|
||
|
||
|
||
.tab-content { display:none; }
|
||
.tab-content.active { display:block; }
|
||
|
||
.columns-container {
|
||
display:flex; gap:20px; flex-wrap:wrap; align-items:flex-start;
|
||
}
|
||
|
||
/* column styling, same as old script */
|
||
.column {
|
||
flex: 1; /* fill available space */
|
||
max-width: 45%; /* or whatever width you like */
|
||
border: 2px solid var(--border-color);
|
||
border-radius: 8px;
|
||
padding: 10px;
|
||
background-color: var(--dark-gray-color);
|
||
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
|
||
}
|
||
.checkbox-group label {
|
||
display:flex; align-items:center; margin-bottom:8px;
|
||
}
|
||
|
||
.button-group { text-align:right; margin-top:20px; }
|
||
.exit-button {
|
||
background:var(--btn-danger); color:var(--white-color);
|
||
padding:10px 20px; border:none; border-radius:5px; cursor:pointer;
|
||
}
|
||
|
||
#PasswordContainer, #dattoRmmContainer {
|
||
margin-top: 1em;
|
||
}
|
||
|
||
/* Common styles for inputs, buttons, and selects */
|
||
#PasswordContainer input,
|
||
#PasswordContainer button,
|
||
#dattoRmmContainer select {
|
||
background-color: var(--dark-gray-color);
|
||
color: var(--white-color);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 4px;
|
||
padding: 8px;
|
||
font-size: 14px;
|
||
display: block;
|
||
width: 40%;
|
||
max-width: 200px;
|
||
}
|
||
|
||
/* Style specifically for the fetch button */
|
||
#PasswordContainer button {
|
||
background-color: var(--btn-sidebar-blue);
|
||
cursor: pointer;
|
||
transition: background-color 0.3s ease;
|
||
}
|
||
|
||
/* Hover effect for the fetch button */
|
||
#PasswordContainer button:hover {
|
||
background-color: var(--btn-hover);
|
||
}
|
||
|
||
/* Tag line */
|
||
#tagline {
|
||
font-size: 1.2rem;
|
||
color: var(--light-gray-color);
|
||
font-weight: bold;
|
||
justify-self: center;
|
||
}
|
||
|
||
@media (max-width:768px) {
|
||
.container { flex-direction:column; }
|
||
.sidebar { width:100%; }
|
||
}
|
||
</style>
|
||
'@
|
||
|
||
$script = @'
|
||
<script>
|
||
// =======================================================================
|
||
// Tab Navigation
|
||
// =======================================================================
|
||
const tabButtons = document.querySelectorAll(".tab-button");
|
||
const tabContents = document.querySelectorAll(".tab-content");
|
||
tabButtons.forEach(btn => {
|
||
btn.addEventListener("click", () => {
|
||
// clear active state
|
||
tabButtons.forEach(b => b.classList.remove("active"));
|
||
tabContents.forEach(c => c.classList.remove("active"));
|
||
// set new active
|
||
btn.classList.add("active");
|
||
document.getElementById(btn.dataset.tab).classList.add("active");
|
||
});
|
||
});
|
||
// initialize default tab on load
|
||
document.querySelector(".tab-button[data-tab='{{defaultPage}}Tab']").classList.add("active");
|
||
document.getElementById("{{defaultPage}}Tab").classList.add("active");
|
||
|
||
// =======================================================================
|
||
// Task Trigger
|
||
// =======================================================================
|
||
const tasks = [
|
||
{{tasksJsAll}}
|
||
];
|
||
|
||
// =======================================================================
|
||
// Column “Select All” toggling for On-Boarding
|
||
// =======================================================================
|
||
function toggleColumn(col) {
|
||
const master = document.getElementById(`selectAll${col[0].toUpperCase() + col.slice(1)}Checkbox`);
|
||
const children = document.querySelectorAll(`#onboardTab input[type=checkbox][data-column=${col}]`);
|
||
|
||
children.forEach(cb => {
|
||
cb.checked = master.checked;
|
||
});
|
||
|
||
// Now simulate change events after setting all checkboxes
|
||
setTimeout(() => {
|
||
children.forEach(cb => {
|
||
cb.dispatchEvent(new Event('change'));
|
||
});
|
||
}, 0);
|
||
}
|
||
|
||
|
||
// =======================================================================
|
||
// Un-check “Select All” if any child is unchecked (& re-check if all are checked)
|
||
// =======================================================================
|
||
function updateSelectAll(col) {
|
||
const master = document.getElementById(
|
||
`selectAll${col[0].toUpperCase() + col.slice(1)}Checkbox`
|
||
);
|
||
const children = document.querySelectorAll(
|
||
`#onboardTab input[type=checkbox][data-column=${col}]`
|
||
);
|
||
master.checked = Array.from(children).every(cb => cb.checked);
|
||
}
|
||
|
||
// Attach listeners on load
|
||
['left','right'].forEach(col => {
|
||
document
|
||
.querySelectorAll(`#onboardTab input[type=checkbox][data-column=${col}]`)
|
||
.forEach(cb => cb.addEventListener('change', () => updateSelectAll(col)));
|
||
});
|
||
|
||
// =======================================================================
|
||
// DattoRMM Options
|
||
// =======================================================================
|
||
function toggleDattoRMMOptions() {
|
||
const master = document.getElementById('installDattoRMM');
|
||
const container = document.getElementById('installDattoRMMOptionsContainer');
|
||
if (!container) return;
|
||
container.style.display = master.checked ? 'block' : 'none';
|
||
container.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = master.checked);
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const master = document.getElementById('installDattoRMM');
|
||
if (master) master.addEventListener('change', toggleDattoRMMOptions);
|
||
});
|
||
|
||
|
||
// =======================================================================
|
||
// Fetch Sites Handler
|
||
// =======================================================================
|
||
async function fetchSites() {
|
||
const pwd = document.getElementById("Password").value;
|
||
if (!pwd) {
|
||
alert("Please enter the password.");
|
||
return;
|
||
}
|
||
|
||
const dropdown = document.getElementById("dattoDropdown");
|
||
dropdown.innerHTML = '<option disabled selected>Loading sites...</option>';
|
||
|
||
try {
|
||
const resp = await fetch("/getpw", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ password: pwd })
|
||
});
|
||
|
||
if (!resp.ok) throw("HTTP " + resp.status);
|
||
|
||
const sites = await resp.json();
|
||
dropdown.innerHTML = ''; // clear the loading message
|
||
|
||
sites.forEach(site => {
|
||
const option = document.createElement("option");
|
||
option.value = site.UID;
|
||
option.textContent = site.Name;
|
||
dropdown.appendChild(option);
|
||
});
|
||
|
||
document.getElementById("dattoRmmContainer").style.display = "block";
|
||
}
|
||
catch (e) {
|
||
console.error(e);
|
||
dropdown.innerHTML = '<option disabled selected>Error loading sites</option>';
|
||
alert("Failed to fetch sites. Check password and try again.");
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
async function triggerInstall() {
|
||
// disable the button so you can't double-click while work is in flight
|
||
const runBtn = document.querySelector('.run-button');
|
||
runBtn.disabled = true;
|
||
|
||
try {
|
||
for (const t of tasks) {
|
||
const cb = document.getElementById(t.id);
|
||
if (!cb || !cb.checked) continue;
|
||
|
||
if (t.id === 'installDattoRMM') {
|
||
const sub = Array.from(
|
||
document.querySelectorAll('.sub-option-installDattoRMM:checked')
|
||
).map(x => x.value);
|
||
const dropdown = document.getElementById('dattoDropdown');
|
||
const uid = dropdown.value;
|
||
const name = dropdown.selectedOptions[0].text;
|
||
|
||
await fetch('/installDattoRMM', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
checkedValues: sub,
|
||
UID: uid,
|
||
Name: name
|
||
})
|
||
});
|
||
} else {
|
||
await fetch(t.handler, { method: 'GET' });
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('Error during triggerInstall:', e);
|
||
} finally {
|
||
// always re-enable, even if an error occurred
|
||
runBtn.disabled = false;
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
// =======================================================================
|
||
// Shutdown Handler
|
||
// =======================================================================
|
||
function endSession() {
|
||
fetch("/quit", { method: "GET" })
|
||
.finally(() => window.close());
|
||
}
|
||
|
||
// =======================================================================
|
||
// Sub-Options Auto-Toggle for Tasks
|
||
// =======================================================================
|
||
|
||
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
// Auto-handle visibility and checking for tasks with sub-options
|
||
const tasksWithSubOptions = document.querySelectorAll('[id$="OptionsContainer"]');
|
||
|
||
tasksWithSubOptions.forEach(container => {
|
||
const taskId = container.id.replace('OptionsContainer', '');
|
||
const masterCheckbox = document.getElementById(taskId);
|
||
|
||
if (!masterCheckbox) return;
|
||
|
||
function updateVisibility() {
|
||
const checked = masterCheckbox.checked;
|
||
container.style.display = checked ? 'block' : 'none';
|
||
container.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = checked);
|
||
|
||
// Show/hide Password and RMM only if it's installDattoRMM
|
||
if (taskId === 'installDattoRMM') {
|
||
const pwdBox = document.getElementById('PasswordContainer');
|
||
const rmmBox = document.getElementById('dattoRmmContainer');
|
||
if (pwdBox) pwdBox.style.display = checked ? 'block' : 'none';
|
||
if (rmmBox) rmmBox.style.display = checked ? 'block' : 'none';
|
||
}
|
||
}
|
||
|
||
masterCheckbox.addEventListener('change', updateVisibility);
|
||
updateVisibility(); // call once on load
|
||
});
|
||
});
|
||
|
||
// ===========================================
|
||
// ─ rotating tagline ───────────────────────────────
|
||
// ===========================================
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const taglines = [
|
||
"Fast deployments, no monkey business.",
|
||
"Bananas for better builds.",
|
||
"Deploy without flinging code.",
|
||
"Tame your stack. Unleash the monkey.",
|
||
"Monkey see, monkey deploy.",
|
||
"Deploy smarter -- with a monkey on your team.",
|
||
"Don't pass the monkey -- let it deploy.",
|
||
"No more monkeying around. Stack handled.",
|
||
"Own your stack. But let the monkey do the work.",
|
||
"Why throw code when the monkey's got it?",
|
||
"Deployments so easy, a monkey could do it. Ours does.",
|
||
"Monkey in the stack, not on your back."
|
||
];
|
||
|
||
const el = document.getElementById("tagline");
|
||
let idx = Math.floor(Math.random() * taglines.length);
|
||
el.textContent = taglines[idx];
|
||
|
||
setInterval(() => {
|
||
idx = (idx + 1) % taglines.length;
|
||
el.textContent = taglines[idx];
|
||
}, 10_000);
|
||
});
|
||
|
||
// when the browser window is closed (X), notify the server to quit
|
||
window.addEventListener('beforeunload', () => {
|
||
// keepalive: true ensures the request is sent even as the page unloads
|
||
fetch('/quit', { method: 'GET', keepalive: true });
|
||
});
|
||
|
||
</script>
|
||
|
||
'@
|
||
|
||
#
|
||
# 3) The HTML skeleton with placeholders
|
||
#
|
||
$htmlTemplate = @"
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||
<title>Script Monkey</title>
|
||
<link rel="icon" href="https://git.svstools.com/syelle/Logo/raw/branch/main/SVS_Favicon.ico">
|
||
$style
|
||
</head>
|
||
<body>
|
||
<div class="logo-container">
|
||
<div class="logo-left">
|
||
<img src="https://git.svstools.com/syelle/Logo/raw/branch/main/SVS_logo.svg" alt="SVS Logo">
|
||
{{moduleVersion}}
|
||
</div>
|
||
<div id="tagline"></div>
|
||
</div>
|
||
|
||
<div class="container">
|
||
<div class="sidebar">
|
||
<button class="tab-button" data-tab="onboardTab">On-Boarding</button>
|
||
<button class="tab-button" data-tab="offboardTab">Off-Boarding</button>
|
||
<button class="tab-button" data-tab="tweaksTab">Tweaks</button>
|
||
<button class="tab-button" data-tab="SVSAppsTab">SVS APPs</button>
|
||
</div>
|
||
<div class="content">
|
||
<div id="onboardTab" class="tab-content">
|
||
<h2>On-Boarding</h2>
|
||
<h3 class="subtitle">This new deployment method ensures everything is successfully deployed with greater ease!</h3>
|
||
|
||
<!-- 1) Dynamic task checkboxes -->
|
||
<div class="columns-container">
|
||
<div class="checkbox-group column">
|
||
<h3>SVSMSP Stack</h3>
|
||
<label><input type="checkbox" id="selectAllLeftCheckbox" onclick="toggleColumn('left')"> Select All</label>
|
||
{{onboardLeftColumn}}
|
||
</div>
|
||
<div class="checkbox-group column">
|
||
<h3>Optional</h3>
|
||
<label><input type="checkbox" id="selectAllRightCheckbox" onclick="toggleColumn('right')"> Select All</label>
|
||
{{onboardRightColumn}}
|
||
</div>
|
||
</div>
|
||
|
||
|
||
|
||
<!-- 2) Password and Datto Site dropdown shown conditionally -->
|
||
<div id="PasswordContainer" style="display:none; margin-bottom:1em;">
|
||
<label for="Password">Enter Password:</label>
|
||
<div style="display:flex; gap:5px;">
|
||
<input type="password" id="Password" placeholder="Enter Password" style="flex:1;" />
|
||
|
||
<button onclick="fetchSites()" style="padding:4px 10px; background-color: var(--btn-sidebar-blue); color: var(--white-color); border: none; border-radius: 4px;">GO!</button>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div id="dattoRmmContainer" style="display:none; margin-bottom:1em;">
|
||
<label for="dattoDropdown">Select a Datto RMM site:</label>
|
||
<select id="dattoDropdown" style="width:100%;">
|
||
<option disabled selected>Fetching sites...</option>
|
||
</select>
|
||
</div>
|
||
|
||
|
||
<div id="offboardTab" class="tab-content">
|
||
<h2>Off-Boarding</h2>
|
||
<div class="columns-container">
|
||
{{offboardCheckboxes}}
|
||
</div>
|
||
|
||
</div>
|
||
<div id="tweaksTab" class="tab-content">
|
||
<h2>Tweaks</h2>
|
||
<div class="columns-container">
|
||
{{tweaksCheckboxes}}
|
||
</div>
|
||
|
||
</div>
|
||
<div id="SVSAppsTab" class="tab-content">
|
||
<h2>SVS APPs</h2>
|
||
<div class="columns-container">
|
||
{{appsCheckboxes}}
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
$script
|
||
<!-- floating button group -->
|
||
<div class="fixed-buttons">
|
||
<button class="exit-button" onclick="endSession()">Exit</button>
|
||
<button class="run-button" onclick="triggerInstall()">Run Selected</button>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
|
||
"@
|
||
|
||
#
|
||
# 4) Build the checkbox HTML and tasks JS from $Global:Tasks
|
||
#
|
||
|
||
# On-boarding now has two columns:
|
||
$onboardLeft = Build-Checkboxes -Page 'onboard' -Column 'left'
|
||
$onboardRight = Build-Checkboxes -Page 'onboard' -Column 'right'
|
||
|
||
# Off-boarding, Tweaks, SVSApps stay one-column:
|
||
$offboard = Build-Checkboxes -Page 'offboard' -Column ''
|
||
$tweaks = Build-Checkboxes -Page 'tweaks' -Column ''
|
||
$apps = Build-Checkboxes -Page 'SVSApps' -Column ''
|
||
|
||
# Tasks JS array (fixed)
|
||
$tasksJsAll = (
|
||
$Global:Tasks | ForEach-Object {
|
||
" { id: '$($_.Id)', handler: '/$($_.Name)', label: '$($_.Label)' }"
|
||
}
|
||
) -join ",`n"
|
||
|
||
|
||
#
|
||
# 5) Inject into template
|
||
#
|
||
$html = $htmlTemplate
|
||
$html = $html.Replace('{{moduleVersion}}', (Get-ModuleVersionHtml))
|
||
$html = $html.Replace('{{onboardLeftColumn}}', $onboardLeft)
|
||
$html = $html.Replace('{{onboardRightColumn}}', $onboardRight)
|
||
$html = $html.Replace('{{offboardCheckboxes}}', $offboard)
|
||
$html = $html.Replace('{{tweaksCheckboxes}}', $tweaks)
|
||
$html = $html.Replace('{{appsCheckboxes}}', $apps)
|
||
$html = $html.Replace('{{tasksJsAll}}', $tasksJsAll)
|
||
$html = $html.Replace('{{defaultPage}}', $Page)
|
||
|
||
|
||
return $html
|
||
}
|
||
|
||
#endregion
|
||
|
||
|
||
|
||
|
||
#region HTTP Listener & Routing
|
||
|
||
# Handle shutdown command
|
||
if ($path -eq 'quit') {
|
||
Write-LogHybrid "Shutdown requested" "Info" "Server"
|
||
Respond-Text $Context "Server shutting down."
|
||
# This will break out of the while loop in Start-Server
|
||
$Global:Listener.Stop()
|
||
return
|
||
}
|
||
|
||
# Sends the HTML for a given page or invokes a task handler
|
||
function Dispatch-Request {
|
||
param($Context)
|
||
|
||
# figure out the path
|
||
$path = $Context.Request.Url.AbsolutePath.TrimStart('/')
|
||
|
||
# ---- Shutdown handler ----
|
||
if ($path -eq 'quit') {
|
||
Write-LogHybrid "Shutdown requested" "Info" "Server"
|
||
Respond-Text $Context "Server shutting down."
|
||
# stop the listener loop
|
||
$Global:Listener.Stop()
|
||
return
|
||
}
|
||
|
||
# ---- Fetch Sites endpoint ----
|
||
if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'getpw') {
|
||
Handle-FetchSites $Context
|
||
return
|
||
}
|
||
|
||
# ---- Serve UI pages ----
|
||
if ($path -in @('', 'onboard', 'offboard', 'tweaks', 'SVSApps')) {
|
||
$page = if ($path -eq '') { 'onboard' } else { $path }
|
||
$html = Get-UIHtml -Page $page
|
||
Respond-HTML $Context $html
|
||
return
|
||
}
|
||
|
||
# ---- Task invocation ----
|
||
$task = $Global:Tasks | Where-Object Name -EQ $path
|
||
if ($task) {
|
||
& $task.HandlerFn $Context
|
||
return
|
||
}
|
||
|
||
# ---- 404 ----
|
||
$Context.Response.StatusCode = 404
|
||
Respond-Text $Context '404 - Not Found'
|
||
}
|
||
|
||
|
||
# Starts the HTTP listener loop
|
||
function Start-Server {
|
||
# make it accessible to Dispatch-Request
|
||
$Global:Listener = [System.Net.HttpListener]::new()
|
||
$Global:Listener.Prefixes.Add("http://localhost:$Port/")
|
||
$Global:Listener.Start()
|
||
Write-Host "Listening on http://localhost:$Port/ ..."
|
||
|
||
try {
|
||
while ($Global:Listener.IsListening) {
|
||
$ctx = $Global:Listener.GetContext()
|
||
try {
|
||
Dispatch-Request $ctx
|
||
} catch {
|
||
Write-LogHybrid "Dispatch error: $_" "Error" "Server"
|
||
}
|
||
}
|
||
} finally {
|
||
# once the loop exits, clean up
|
||
$Global:Listener.Close()
|
||
Write-LogHybrid "Listener closed." "Info" "Server"
|
||
}
|
||
}
|
||
|
||
|
||
#endregion
|
||
|
||
|
||
# Only launch UI if not in silent mode
|
||
if (-not $SilentInstall) {
|
||
# open browser on whatever port you've set
|
||
# Start-Process "microsoft-edge:http://localhost:$Port"
|
||
Start-Process "msedge.exe" -ArgumentList "--app=http://localhost:$Port"
|
||
# now start your server (this will block until you hit Exit in the UI)
|
||
Start-Server
|
||
}
|
||
|
||
# At the end of ScriptMonkey.ps1
|
||
if ($MyInvocation.ExpectingInput) {
|
||
Invoke-ScriptMonkey @PSBoundParameters
|
||
}
|