Public/Core/New-IDBridgeConfig.ps1

<#
.SYNOPSIS
First-run scaffold: create the IDBridge folder tree and a default IDBridgeConfig.psd1.

.DESCRIPTION
One-time setup helper for a fresh install. It:
  - creates the runtime directory tree under RootPath (Config/Logs/Exports/Plugins/Data/Vault);
  - writes a default <RootPath>\Config\IDBridgeConfig.psd1 with every feature disabled and
    placeholder values for all site-specific settings.

An existing config file is NEVER overwritten — the function throws instead (there is
deliberately no -Force). The generated config is safe to load immediately: ReadOnly stays
$true, AD/Google processing and every plugin are disabled, and all site-specific values are
obvious placeholders to fill in before enabling anything.

This runs before any state exists, so it does not use Write-Log or require
Initialize-IDBridge — run it first, edit the config, then run Initialize-IDBridge.

.PARAMETER RootPath
Base directory for Config/Logs/Exports/Plugins/Data/Vault. Defaults to C:\IDBridge.
Missing directories are created.

.OUTPUTS
None. Writes the default config file and prints its path.

.EXAMPLE
New-IDBridgeConfig

.EXAMPLE
New-IDBridgeConfig -RootPath 'D:\IDBridge'

.NOTES
   Created by: Sam Cattanach
   Modified: 2026-07-09
#>

function New-IDBridgeConfig {
    [CmdletBinding()]
    param (
        [string]$RootPath = "C:\IDBridge"
    )

    $configFilePath = Join-Path $RootPath "Config\IDBridgeConfig.psd1"
    if (Test-Path $configFilePath) {
        Throw "Config file already exists and will not be overwritten: $configFilePath"
    }

    #region Create Directories
    $directories = @(
        $RootPath
        "$RootPath\Config"
        "$RootPath\Logs"
        "$RootPath\Exports"
        "$RootPath\Plugins"
        "$RootPath\Data"
        "$RootPath\Vault"
    )
    foreach ($directory in $directories) {
        if (-not (Test-Path $directory)) {
            New-Item -ItemType Directory -Path $directory -Force | Out-Null
            Write-Host "Created missing directory: $directory" -ForegroundColor Yellow
        }
    }
    #endregion Create Directories

    #region Write Default Config
    $defaultConfig = @'
# IDBridgeConfig.psd1 — generated by New-IDBridgeConfig
# Every feature is disabled. Replace the placeholder values, then enable features one at a time.

@{
    # ── Debug / Run Modes ─────────────────────────────────────────────────────
    Debug = @{
        ReadOnly = $true # Set $false to actually write changes
        TestRun = $false # Set $true to only process a subset of users for faster testing
        SkipADCheck = $false # Set $true to skip the AD module check at startup
        TraceLogging = $true # Set $false to disable verbose/trace logging
    }

    # ── Change Safety ─────────────────────────────────────────────────────────
    ChangeThreshold = @{
        Enabled = $true # Set $false (or run -SkipChangeThreshold) to bypass the guard
        Percentage = 25 # Abort the run if proposed lifecycle changes exceed this % of the managed population
    }

    # ── Google Token ──────────────────────────────────────────────────────────
    GoogleToken = @{
        Enabled = $false # Set $true once the GoogleAuth-ServiceAccount secret is seeded
    }

    # ── Google Workspace ──────────────────────────────────────────────────────
    Google = @{
        enabled = $false
        customerID = 'C00000000' # Workspace customer ID
        userRootOU = '/YourDistrict' # Managed root OU path; anchors the ChangeThreshold safety guard's population count
        enableGroupProcessing = $false
        enableGroupProcessingWhatIf = $true # Log-only while $true; set $false to actually apply group changes
        enableGroupProcessingRemove = $false # Set $true to allow group membership removals
        enableGroupProcessingTrash = $false # Set $true to strip group memberships on deactivate
        enableLicenseRemoval = $false # Set $true to remove paid license assignments on deactivate (the base license self-releases when the user is archived)
        # licenseProductIds = @('101031', '101037') # Paid products searched for license assignments
    }

    # ── Active Directory ──────────────────────────────────────────────────────
    AD = @{
        enabled = $false
        userRootOU = 'OU=YourDistrict,DC=yourdomain,DC=local' # Managed root OU DN; anchors the ChangeThreshold safety guard's population count
        enableGroupProcessing = $false
        enableGroupProcessingWhatIf = $true # Log-only while $true; set $false to actually apply group changes
        enableGroupProcessingRemove = $false # Set $true to allow group membership removals
        enableGroupProcessingTrash = $false # Set $true to strip group memberships on deactivate
    }

    # ── Google Log ────────────────────────────────────────────────────────────
    Logging = @{
        GoogleSheetLoggingEnabled = $false
        SheetID = 'your-log-spreadsheet-id' # Target spreadsheet ID for logs
    }

    # ── Telemetry (see PRIVACY.md) ────────────────────────────────────────────
    Telemetry = @{
        Tier = 'Basic' # 'Basic' (default - anonymous counts) | 'Enhanced' (adds install SiteID + error class on failures) | 'Off'
    }

    # ── Secrets ───────────────────────────────────────────────────────────────
    Secrets = @{
        Provider = 'Cms' # 'Cms' (certificate, default) | 'DpapiNG' (gMSA) | 'AzKeyVault' (Azure Key Vault, remote)

        Cms = @{
            Thumbprint = '' # From New-IDBridgeSecretCertificate; empty = find the single 'CN=IDBridge Secrets' cert
        }

        # DpapiNG provider: uncomment and set Provider = 'DpapiNG' to use it.
        # DpapiNG = @{
        # ProtectionDescriptor = 'SID=<gMSA SID> OR SID=<admins group SID>' # AD principal(s) secrets are protected to
        # }

        # AzKeyVault provider: uncomment and set Provider = 'AzKeyVault' to use it.
        # AzKeyVault = @{
        # VaultUri = 'https://your-vault.vault.azure.net/' # Key Vault URI
        # TenantId = 'yourtenant.onmicrosoft.com' # Entra tenant ID or domain
        # ClientId = '00000000-0000-0000-0000-000000000000' # App registration (client) ID
        # CertThumbprint = '' # Auth certificate thumbprint
        # }
    }

    # ── Plugins ───────────────────────────────────────────────────────────────
    Plugins = @(
        @{ Enabled = $false; Type = "Source"; Function = 'Invoke-PluginGSheetStaff' }
        @{ Enabled = $false; Type = "Source"; Function = 'Invoke-PluginSkywardSMSStudents' }
        @{ Enabled = $false; Type = "Override"; Function = 'Invoke-PluginStaffOverride' }
    )
}
'@


    try {
        Set-Content -Path $configFilePath -Value $defaultConfig -ErrorAction Stop
    }
    catch {
        Throw "Error writing default config file: $_"
    }
    #endregion Write Default Config

    Write-Host "Created default config: $configFilePath" -ForegroundColor Green
    Write-Host "Edit the placeholder values, then run Initialize-IDBridge to load it."
}