Initial commit with Autotask/DRMM

This commit is contained in:
Chris Payne
2025-07-08 19:32:11 -04:00
parent 5856d09e8b
commit 9657fe7133
5 changed files with 415 additions and 0 deletions

129
InstaProvision.xaml Normal file
View File

@@ -0,0 +1,129 @@
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="InstaProvision - SVS Tools"
Height="550" Width="480"
WindowStartupLocation="CenterScreen"
Background="#1e1e2f"
ResizeMode="NoResize"
FontFamily="Segoe UI">
<Window.Resources>
<!-- Rounded TextBox style -->
<Style x:Key="RoundedTextBox" TargetType="TextBox">
<Setter Property="Background" Value="#2d2d3a"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Padding" Value="10,6"/>
<Setter Property="BorderBrush" Value="#3f3f3f"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Height" Value="34"/>
<Setter Property="Margin" Value="0,4,0,4"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border CornerRadius="10" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer x:Name="PART_ContentHost"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Fancy animated button -->
<Style x:Key="FancyButton" TargetType="Button">
<Setter Property="Background" Value="#007bff"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Padding" Value="8,4"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="buttonBorder" CornerRadius="8" Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetName="buttonBorder" Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)" To="#0056b3" Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetName="buttonBorder" Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)" To="#007bff" Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="buttonBorder" Property="Background" Value="#003f88"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Modern toggle-style CheckBox -->
<Style x:Key="ModernCheckBox" TargetType="CheckBox">
<Setter Property="Foreground" Value="White"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="CheckBox">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Grid Width="40" Height="22" Margin="0,0,8,0">
<Border x:Name="SwitchBorder" Background="#555" CornerRadius="11"/>
<Ellipse x:Name="SwitchKnob" Fill="White" Width="18" Height="18" Margin="2" HorizontalAlignment="Left"/>
</Grid>
<ContentPresenter VerticalAlignment="Center" RecognizesAccessKey="True"/>
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="SwitchBorder" Property="Background" Value="#28a745"/>
<Setter TargetName="SwitchKnob" Property="HorizontalAlignment" Value="Right"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid Margin="20">
<StackPanel Orientation="Vertical">
<!-- Logo at top -->
<Image Source="https://i.ibb.co/vCTJYn2j/svs-logo-nav-wh.png" Height="60" HorizontalAlignment="Center" Margin="0,0,0,10"/>
<!-- Title -->
<TextBlock Text="Provision Company" FontSize="20" FontWeight="SemiBold" Foreground="White" HorizontalAlignment="Center" Margin="0,0,0,10"/>
<!-- Main Content -->
<StackPanel Orientation="Vertical">
<TextBlock Text="Company Name:" Margin="0,5,0,0" Foreground="#cccccc"/>
<TextBox Name="CompanyNameBox" Style="{StaticResource RoundedTextBox}"/>
<TextBlock Name="PhoneLabel" Text="Phone Number:" Margin="0,10,0,0" Foreground="#cccccc"/>
<TextBox Name="PhoneBox" Style="{StaticResource RoundedTextBox}"/>
<CheckBox Name="SelectAllBox" Content="Select All Tools" Margin="0,15,0,0" FontWeight="Bold" Foreground="White" Style="{StaticResource ModernCheckBox}"/>
<StackPanel Margin="10,5,0,0">
<CheckBox Name="AutotaskBox" Content="Autotask" Margin="0,4" Style="{StaticResource ModernCheckBox}"/>
<CheckBox Name="DattoBox" Content="Datto RMM" Margin="0,4" Style="{StaticResource ModernCheckBox}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,25,0,0">
<Button Name="LoginBtn" Content="Login with Microsoft" Width="180" Height="38" Margin="5" Style="{StaticResource FancyButton}"/>
<Button Name="SubmitBtn" Content="Provision" Width="120" Height="38" Margin="5" Style="{StaticResource FancyButton}"/>
</StackPanel>
<TextBlock Name="StatusBlock" Foreground="LightGreen" TextAlignment="Center" Margin="0,15,0,0"/>
</StackPanel>
</StackPanel>
</Grid>
</Window>

5
config.ps1 Normal file
View File

@@ -0,0 +1,5 @@
$Global:CLIENT_ID = "8c3edf4c-e5f4-4889-8528-785896bef75d"
$Global:TENANT_ID = "common"
$Global:AUTHORITY = "https://login.microsoftonline.com/$TENANT_ID"
$Global:SCOPES = "User.Read"
$Global:WEBHOOK_URL = "https://automate.svstools.ca/webhook/cff4797d-8d38-4005-b4a2-bf2743a5e6e8"

185
main.ps1 Normal file
View File

@@ -0,0 +1,185 @@
# main.ps1 (clean, gated execution)
# Ensure MSAL.PS is installed
if (-not (Get-Module -ListAvailable -Name MSAL.PS)) {
try {
Write-Host "[INFO] Installing MSAL.PS..."
Install-Module -Name MSAL.PS -Scope CurrentUser -Force -AllowClobber
} catch {
[System.Windows.MessageBox]::Show("MSAL.PS install failed: $($_.Exception.Message)", "Error")
exit 1
}
}
Import-Module MSAL.PS
Write-Host "[INFO] MSAL.PS module loaded."
# Load config and tool modules
. "$PSScriptRoot\config.ps1"
. "$PSScriptRoot\tools\autotask.ps1"
. "$PSScriptRoot\tools\dattormm.ps1"
Write-Host "[INFO] Config and tool modules loaded."
# Load WPF assemblies
Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName PresentationCore
Add-Type -AssemblyName WindowsBase
Add-Type -AssemblyName System.Xaml
# Load WPF XAML UI
[xml]$xaml = Get-Content "$PSScriptRoot\InstaProvision.xaml"
$reader = (New-Object System.Xml.XmlNodeReader $xaml)
$window = [Windows.Markup.XamlReader]::Load($reader)
if (-not $window) {
Write-Host "[ERROR] Failed to load XAML window."
exit 1
}
Write-Host "[INFO] WPF window loaded."
# Map named UI controls
$CompanyNameBox = $window.FindName("CompanyNameBox")
$PhoneBox = $window.FindName("PhoneBox")
$SelectAllBox = $window.FindName("SelectAllBox")
$AutotaskBox = $window.FindName("AutotaskBox")
$DattoBox = $window.FindName("DattoBox")
$LoginBtn = $window.FindName("LoginBtn")
$SubmitBtn = $window.FindName("SubmitBtn")
$StatusBlock = $window.FindName("StatusBlock")
$phoneLabel = $window.FindName("PhoneLabel")
$PhoneBox.Visibility = 'Collapsed'
$phoneLabel.Visibility = 'Collapsed'
$SubmitBtn.Visibility = 'Collapsed'
$AutotaskBox.Add_Checked({
$PhoneBox.Visibility = 'Visible'
$phoneLabel.Visibility = 'Visible'
})
$AutotaskBox.Add_Unchecked({
$PhoneBox.Visibility = 'Collapsed'
$phoneLabel.Visibility = 'Collapsed'
})
Write-Host "[INFO] UI control references assigned."
# Global tool credentials
$script:toolCredentials = $null
# Select All logic
$SelectAllBox.Add_Checked({
$AutotaskBox.IsChecked = $true
$DattoBox.IsChecked = $true
Write-Host "[UI] Select All: Checked"
})
$SelectAllBox.Add_Unchecked({
$AutotaskBox.IsChecked = $false
$DattoBox.IsChecked = $false
Write-Host "[UI] Select All: Unchecked"
})
# Login logic
$LoginBtn.Add_Click({
Write-Host "[LOGIN] Initiating login..."
try {
$auth = Get-MSALToken -ClientId $CLIENT_ID -TenantId $TENANT_ID -Scopes $SCOPES -Interactive
Write-Host "[LOGIN] Token acquired."
if (-not $auth.AccessToken) {
Write-Host "[ERROR] No access token returned."
[System.Windows.MessageBox]::Show("Login failed.", "Auth")
return
}
$tenantId = $auth.TenantId
Write-Host "[DEBUG] Tenant ID: $tenantId"
if (-not $tenantId) {
[System.Windows.MessageBox]::Show("Tenant ID missing from login result.", "Auth Error")
return
}
$headers = @{ "Content-Type" = "application/json" }
$body = @{ tenant_id = $tenantId } | ConvertTo-Json -Depth 2
Write-Host "[DEBUG] Webhook payload: $body"
$response = Invoke-RestMethod -Uri $WEBHOOK_URL -Method POST -Headers $headers -Body $body
if (-not $response) {
Write-Host "[ERROR] Webhook returned no data."
[System.Windows.MessageBox]::Show("Unauthorized tenant or empty webhook response.", "Auth")
return
}
if ($response -is [pscustomobject]) {
$script:toolCredentials = @{}
foreach ($property in $response.PSObject.Properties) {
$script:toolCredentials[$property.Name] = $property.Value
}
} else {
Write-Host "[ERROR] Webhook returned non-object data: $response"
[System.Windows.MessageBox]::Show("Invalid webhook response.", "Auth")
return
}
$SubmitBtn.Visibility = 'Visible'
$LoginBtn.Visibility = 'Collapsed'
Write-Host "[INFO] Login and webhook successful."
[System.Windows.MessageBox]::Show("Login successful.", "Auth")
}
catch {
Write-Host "[ERROR] Login/Webhook exception: $($_.Exception.Message)"
[System.Windows.MessageBox]::Show("Login/Webhook error: $($_.Exception.Message)", "Error")
}
})
# Submit logic
$SubmitBtn.Add_Click({
$company = $CompanyNameBox.Text.Trim()
$phone = $PhoneBox.Text.Trim()
$StatusBlock.Text = ""
Write-Host "[SUBMIT] Provisioning start"
Write-Host "[DEBUG] Company: $company"
Write-Host "[DEBUG] Phone: $phone"
Write-Host "[DEBUG] Autotask selected: $($AutotaskBox.IsChecked)"
Write-Host "[DEBUG] Datto selected: $($DattoBox.IsChecked)"
Write-Host "[DEBUG] Tool Credentials: $($script:toolCredentials | ConvertTo-Json -Depth 4)"
if (-not $company) {
[System.Windows.MessageBox]::Show("Please enter a company name.", "Missing Info")
return
}
if ($AutotaskBox.IsChecked -and ($phone -notmatch "^[\d\s\-\+\(\)]{10,}$")) {
[System.Windows.MessageBox]::Show("Valid phone number required for Autotask.", "Invalid Input")
return
}
$StatusBlock.Text = "Provisioning in progress..."
$window.Dispatcher.Invoke("Background", [action]{ $window.UpdateLayout() })
try {
if ($AutotaskBox.IsChecked) {
Write-Host "[INFO] Provisioning Autotask..."
Invoke-AutotaskProvision -CompanyName $company -PhoneNumber $phone -Credentials $script:toolCredentials
}
if ($DattoBox.IsChecked) {
Write-Host "[INFO] Provisioning Datto RMM..."
Invoke-DattoProvision -CompanyName $company -Credentials $script:toolCredentials
}
$StatusBlock.Text = "Provisioning completed successfully."
Write-Host "[SUCCESS] Provisioning complete."
$CompanyNameBox.Text = ""
$PhoneBox.Text = ""
$AutotaskBox.IsChecked = $false
$DattoBox.IsChecked = $false
$SelectAllBox.IsChecked = $false
}
catch {
Write-Host "[ERROR] Provisioning failed: $($_.Exception.Message)"
$StatusBlock.Text = "Provisioning failed: $($_.Exception.Message)"
}
})
# Show the window
$window.ShowDialog() | Out-Null

41
tools/autotask.ps1 Normal file
View File

@@ -0,0 +1,41 @@
function Invoke-AutotaskProvision {
param (
[Parameter(Mandatory)]
[string]$CompanyName,
[Parameter(Mandatory)]
[string]$PhoneNumber,
[Parameter(Mandatory)]
[hashtable]$Credentials
)
if (-not $Credentials["AutotaskURL"] -or -not $Credentials["AutotaskIntCode"] -or -not $Credentials["AutotaskUsername"] -or -not $Credentials["AutotaskSecret"]) {
throw "Missing Autotask credentials in hashtable."
}
$BaseURL = $Credentials["AutotaskURL"]
$Url = "https://$BaseURL/atservicesrest/v1.0/companies"
$Headers = @{
"ApiIntegrationcode" = $Credentials["AutotaskIntCode"]
"UserName" = $Credentials["AutotaskUsername"]
"Secret" = $Credentials["AutotaskSecret"]
"Content-Type" = "application/json"
}
$Body = @{
companyName = $CompanyName
companyType = 1
phone = $PhoneNumber
ownerResourceID = 4
} | ConvertTo-Json -Depth 3
try {
$Response = Invoke-RestMethod -Uri $Url -Method Post -Headers $Headers -Body $Body
$CompanyID = $Response.itemId, $Response.id, $Response.companyID, $Response.item.id | Where-Object { $_ } | Select-Object -First 1
[System.Windows.MessageBox]::Show("Autotask company created with ID: $CompanyID", "Autotask")
} catch {
throw "[Autotask] Provisioning failed: $($_.Exception.Message)"
}
}

55
tools/dattormm.ps1 Normal file
View File

@@ -0,0 +1,55 @@
function Invoke-DattoProvision {
param (
[Parameter(Mandatory)]
[string]$CompanyName,
[Parameter(Mandatory)]
[hashtable]$Credentials
)
if (-not $Credentials["DattoURL"] -or -not $Credentials["DattoKey"] -or -not $Credentials["DattoSecret"]) {
throw "Missing Datto RMM credentials in hashtable."
}
$TokenUrl = "$($Credentials["DattoURL"].TrimEnd('/'))/auth/oauth/token"
$SiteUrl = "$($Credentials["DattoURL"].TrimEnd('/'))/api/v2/site"
$TokenBody = @{
grant_type = "password"
username = $Credentials["DattoKey"]
password = $Credentials["DattoSecret"]
}
$TokenHeaders = @{
"Content-Type" = "application/x-www-form-urlencoded"
"Accept" = "application/json"
}
try {
# Construct Basic Auth manually
$pair = "public-client:public"
$bytes = [System.Text.Encoding]::UTF8.GetBytes($pair)
$base64 = [System.Convert]::ToBase64String($bytes)
$TokenHeaders["Authorization"] = "Basic $base64"
$TokenResponse = Invoke-RestMethod -Uri $TokenUrl -Method Post -Headers $TokenHeaders -Body $TokenBody
$AccessToken = $TokenResponse.access_token
} catch {
throw "[Datto] Token request failed: $($_.Exception.Message)"
}
$SiteHeaders = @{
"Authorization" = "Bearer $AccessToken"
"Content-Type" = "application/json"
"Accept" = "application/json"
}
$SiteBody = @{ name = $CompanyName } | ConvertTo-Json -Depth 3
try {
$SiteResponse = Invoke-RestMethod -Uri $SiteUrl -Method Put -Headers $SiteHeaders -Body $SiteBody
[System.Windows.MessageBox]::Show("Datto RMM site created: $CompanyName", "Datto RMM")
} catch {
throw "[Datto] Site creation failed: $($_.Exception.Message)"
}
}