Rewrite web handler for device managedBy comparison dashboard
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
using Disco.Plugins.ADCompare.Features;
|
||||
using Disco.Plugins.ADCompare.Models;
|
||||
using Disco.Services.Plugins;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Web.Mvc;
|
||||
|
||||
@@ -20,8 +20,6 @@ namespace Disco.Plugins.ADCompare.WebHandler
|
||||
return Index();
|
||||
case "compare":
|
||||
return Compare();
|
||||
case "compareuser":
|
||||
return CompareUser();
|
||||
case "export":
|
||||
return ExportCsv();
|
||||
default:
|
||||
@@ -29,28 +27,20 @@ namespace Disco.Plugins.ADCompare.WebHandler
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Landing page with a button to run comparison
|
||||
/// </summary>
|
||||
private ActionResult Index()
|
||||
{
|
||||
var html = BuildIndexPage();
|
||||
return new ContentResult
|
||||
{
|
||||
Content = html,
|
||||
Content = BuildDashboardHtml(),
|
||||
ContentType = "text/html",
|
||||
ContentEncoding = Encoding.UTF8
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run full comparison and return results as JSON
|
||||
/// </summary>
|
||||
private ActionResult Compare()
|
||||
{
|
||||
var service = new ADCompareService(Database);
|
||||
var summary = service.CompareAllUsers();
|
||||
|
||||
var service = new DeviceCompareService(Database);
|
||||
var summary = service.CompareAllDevices();
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(summary, Formatting.Indented),
|
||||
@@ -59,237 +49,88 @@ namespace Disco.Plugins.ADCompare.WebHandler
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compare a single user - expects ?userId=DOMAIN\username
|
||||
/// </summary>
|
||||
private ActionResult CompareUser()
|
||||
{
|
||||
var userId = HostController.Request.QueryString["userId"];
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
{
|
||||
return new HttpStatusCodeResult(400, "userId parameter required");
|
||||
}
|
||||
|
||||
var user = Database.Users.Find(userId);
|
||||
if (user == null)
|
||||
{
|
||||
return new HttpStatusCodeResult(404, "User not found in Disco");
|
||||
}
|
||||
|
||||
var service = new ADCompareService(Database);
|
||||
var result = service.CompareUser(user);
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(result, Formatting.Indented),
|
||||
ContentType = "application/json",
|
||||
ContentEncoding = Encoding.UTF8
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Export comparison results as CSV
|
||||
/// </summary>
|
||||
private ActionResult ExportCsv()
|
||||
{
|
||||
var service = new ADCompareService(Database);
|
||||
var summary = service.CompareAllUsers();
|
||||
|
||||
var service = new DeviceCompareService(Database);
|
||||
var summary = service.CompareAllDevices();
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("UserId,DisplayName,FoundInAD,ADDisabled,MismatchedFields,Details");
|
||||
|
||||
foreach (var result in summary.Results)
|
||||
sb.AppendLine("SerialNumber,ComputerName,DiscoAssignedUser,DiscoAssignedUserName,ADManagedByUser,ADManagedByName,Match,Reason");
|
||||
foreach (var r in summary.Results.Where(r => !r.IsMatch))
|
||||
{
|
||||
var mismatchFields = result.HasMismatches
|
||||
? string.Join("; ", result.Mismatches.ConvertAll(m => m.FieldName))
|
||||
: "None";
|
||||
|
||||
var details = result.HasMismatches
|
||||
? string.Join("; ", result.Mismatches.ConvertAll(m =>
|
||||
$"{m.FieldName}: Disco='{m.DiscoValue}' AD='{m.ADValue}'"))
|
||||
: "";
|
||||
|
||||
sb.AppendLine($"\"{CsvEscape(result.UserId)}\",\"{CsvEscape(result.DisplayName)}\",{result.UserFoundInAD},{result.ADAccountDisabled},\"{CsvEscape(mismatchFields)}\",\"{CsvEscape(details)}\"");
|
||||
sb.AppendLine(string.Join(",",
|
||||
Csv(r.SerialNumber), Csv(r.ComputerName),
|
||||
Csv(r.DiscoAssignedUserId), Csv(r.DiscoAssignedUserDisplayName),
|
||||
Csv(r.ADManagedByUserId), Csv(r.ADManagedByDisplayName),
|
||||
r.IsMatch.ToString(), Csv(r.MismatchReason)));
|
||||
}
|
||||
HostController.Response.Headers.Add("Content-Disposition",
|
||||
$"attachment; filename=\"AD_ManagedBy_Compare_{DateTime.Now:yyyyMMdd_HHmmss}.csv\"");
|
||||
return new ContentResult { Content = sb.ToString(), ContentType = "text/csv", ContentEncoding = Encoding.UTF8 };
|
||||
}
|
||||
|
||||
var fileName = $"AD_Compare_{DateTime.Now:yyyyMMdd_HHmmss}.csv";
|
||||
private string Csv(string v) => $"\"{(v ?? "").Replace("\"", "\"\"")}\"";
|
||||
|
||||
HostController.Response.Headers.Add("Content-Disposition", $"attachment; filename=\"{fileName}\"");
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = sb.ToString(),
|
||||
ContentType = "text/csv",
|
||||
ContentEncoding = Encoding.UTF8
|
||||
};
|
||||
}
|
||||
|
||||
private string CsvEscape(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return "";
|
||||
return value.Replace("\"", "\"\"");
|
||||
}
|
||||
|
||||
#region HTML Page Builder
|
||||
|
||||
private string BuildIndexPage()
|
||||
private string BuildDashboardHtml()
|
||||
{
|
||||
var pluginUrl = Url.Action(MVC.API.Disco.Plugin.PluginWebAction(Manifest.Id, null));
|
||||
|
||||
return $@"
|
||||
<div id='adcompare-container'>
|
||||
<h2>AD Compare - User Detail Comparison</h2>
|
||||
<p>Compare Active Directory user details against Disco ICT records to identify mismatches.</p>
|
||||
|
||||
<div id='adcompare'>
|
||||
<h2><i class='fa fa-exchange'></i> AD Compare — Device Managed By</h2>
|
||||
<p>Compares the AD computer <strong>Managed By</strong> field against the Disco <strong>Assigned User</strong>.</p>
|
||||
<div style='margin:15px 0;'>
|
||||
<button id='btnRunCompare' class='btn btn-primary' onclick='runComparison()'>
|
||||
<i class='fa fa-refresh'></i> Run Full Comparison
|
||||
</button>
|
||||
<button id='btnExport' class='btn btn-default' onclick='exportCsv()' style='margin-left: 10px;' disabled>
|
||||
<i class='fa fa-download'></i> Export CSV
|
||||
</button>
|
||||
<span id='loading' style='display:none; margin-left: 15px;'>
|
||||
<i class='fa fa-spinner fa-spin'></i> Comparing users against AD... This may take a moment.
|
||||
</span>
|
||||
<button id='btnRun' class='btn btn-primary' onclick='runCompare()'><i class='fa fa-refresh'></i> Run Comparison</button>
|
||||
<button id='btnExport' class='btn btn-default' onclick='location.href=""{pluginUrl}/Export""' style='margin-left:10px;' disabled><i class='fa fa-download'></i> Export Mismatches CSV</button>
|
||||
<span id='loading' style='display:none;margin-left:15px;'><i class='fa fa-spinner fa-spin'></i> Querying AD for all devices…</span>
|
||||
</div>
|
||||
|
||||
<div id='summary' style='display:none;margin:15px 0;padding:15px;background:#f5f5f5;border-radius:4px;'>
|
||||
<h4>Summary</h4>
|
||||
<div class='row'>
|
||||
<div class='col-md-2'><strong>Total Users:</strong> <span id='statTotal'>-</span></div>
|
||||
<div class='col-md-2'><strong>In Sync:</strong> <span id='statSync' class='text-success'>-</span></div>
|
||||
<div class='col-md-2'><strong>Mismatches:</strong> <span id='statMismatch' class='text-warning'>-</span></div>
|
||||
<div class='col-md-2'><strong>Not in AD:</strong> <span id='statNotFound' class='text-danger'>-</span></div>
|
||||
<div class='col-md-2'><strong>AD Disabled:</strong> <span id='statDisabled' class='text-muted'>-</span></div>
|
||||
<div class='col-md-2'><strong>Total Devices</strong><br/><span id='sTotal' style='font-size:24px;'>-</span></div>
|
||||
<div class='col-md-2 text-success'><strong>Matched</strong><br/><span id='sMatch' style='font-size:24px;'>-</span></div>
|
||||
<div class='col-md-2 text-warning'><strong>Mismatched</strong><br/><span id='sMismatch' style='font-size:24px;'>-</span></div>
|
||||
<div class='col-md-2 text-danger'><strong>Not in AD</strong><br/><span id='sNotAD' style='font-size:24px;'>-</span></div>
|
||||
<div class='col-md-2 text-info'><strong>No ManagedBy</strong><br/><span id='sNoMgr' style='font-size:24px;'>-</span></div>
|
||||
<div class='col-md-2 text-muted'><strong>No Assignment</strong><br/><span id='sNoAssign' style='font-size:24px;'>-</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style='margin:10px 0;'>
|
||||
<label><input type='checkbox' id='chkIssuesOnly' checked onchange='filterResults()'> Show issues only</label>
|
||||
<input type='text' id='txtSearch' placeholder='Search by username or display name...'
|
||||
style='margin-left: 15px; padding: 4px 8px; width: 300px;' oninput='filterResults()'>
|
||||
<label><input type='checkbox' id='chkMismatchOnly' checked onchange='renderTable()'> Show mismatches only</label>
|
||||
<input type='text' id='txtFilter' placeholder='Filter by serial, computer name, or user...' style='margin-left:15px;padding:4px 8px;width:350px;' oninput='renderTable()'>
|
||||
</div>
|
||||
|
||||
<table id='resultsTable' class='table table-striped table-condensed' style='display:none;'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>User ID</th>
|
||||
<th>Display Name</th>
|
||||
<th>Mismatched Fields</th>
|
||||
<th>Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id='resultsBody'></tbody>
|
||||
<table id='tbl' class='table table-striped table-condensed' style='display:none;'>
|
||||
<thead><tr>
|
||||
<th></th><th>Serial Number</th><th>Computer Name</th>
|
||||
<th>Disco Assigned User</th><th>AD Managed By</th><th>Reason</th>
|
||||
</tr></thead>
|
||||
<tbody id='tbody'></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var allResults = [];
|
||||
var pluginBaseUrl = '{pluginUrl}';
|
||||
|
||||
function runComparison() {{
|
||||
document.getElementById('loading').style.display = 'inline';
|
||||
document.getElementById('btnRunCompare').disabled = true;
|
||||
document.getElementById('resultsTable').style.display = 'none';
|
||||
|
||||
$.ajax({{
|
||||
url: pluginBaseUrl + '/Compare',
|
||||
method: 'GET',
|
||||
dataType: 'json',
|
||||
success: function(data) {{
|
||||
allResults = data.Results;
|
||||
|
||||
document.getElementById('statTotal').textContent = data.TotalDiscoUsers;
|
||||
document.getElementById('statSync').textContent = data.UsersInSync;
|
||||
document.getElementById('statMismatch').textContent = data.UsersWithMismatches;
|
||||
document.getElementById('statNotFound').textContent = data.UsersNotFoundInAD;
|
||||
document.getElementById('statDisabled').textContent = data.ADAccountsDisabled;
|
||||
|
||||
document.getElementById('summary').style.display = 'block';
|
||||
document.getElementById('btnExport').disabled = false;
|
||||
|
||||
filterResults();
|
||||
document.getElementById('resultsTable').style.display = 'table';
|
||||
}},
|
||||
error: function(xhr) {{
|
||||
alert('Error running comparison: ' + xhr.statusText);
|
||||
}},
|
||||
complete: function() {{
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('btnRunCompare').disabled = false;
|
||||
var data=[];
|
||||
function runCompare(){{
|
||||
$('#loading').show();$('#btnRun').prop('disabled',true);$('#tbl').hide();
|
||||
$.getJSON('{pluginUrl}/Compare',function(d){{
|
||||
data=d.Results;
|
||||
$('#sTotal').text(d.TotalDevices);$('#sMatch').text(d.DevicesMatched);
|
||||
$('#sMismatch').text(d.DevicesMismatched);$('#sNotAD').text(d.DevicesNotInAD);
|
||||
$('#sNoMgr').text(d.DevicesNoManagedBy);$('#sNoAssign').text(d.DevicesNoAssignment);
|
||||
$('#summary').show();$('#btnExport').prop('disabled',false);
|
||||
renderTable();$('#tbl').show();
|
||||
}}).fail(function(x){{alert('Error: '+x.statusText);}})
|
||||
.always(function(){{$('#loading').hide();$('#btnRun').prop('disabled',false);}});
|
||||
}}
|
||||
function renderTable(){{
|
||||
var mo=$('#chkMismatchOnly').is(':checked'),q=$('#txtFilter').val().toLowerCase(),tb=$('#tbody');tb.empty();
|
||||
data.forEach(function(r){{
|
||||
if(mo&&r.IsMatch)return;
|
||||
if(q){{var hay=(r.SerialNumber+' '+r.ComputerName+' '+r.DiscoAssignedUserId+' '+r.ADManagedByUserId+' '+(r.DiscoAssignedUserDisplayName||'')+' '+(r.ADManagedByDisplayName||'')).toLowerCase();if(hay.indexOf(q)<0)return;}}
|
||||
var icon=r.IsMatch?'<i class=""fa fa-check text-success""></i>':(!r.FoundInAD?'<i class=""fa fa-times text-danger""></i>':'<i class=""fa fa-exclamation-triangle text-warning""></i>');
|
||||
var discoUser=r.DiscoAssignedUserId?(esc(r.DiscoAssignedUserId)+(r.DiscoAssignedUserDisplayName?'<br/><small class=""text-muted"">'+esc(r.DiscoAssignedUserDisplayName)+'</small>':'')):'<em class=""text-muted"">Not assigned</em>';
|
||||
var adUser=r.ADManagedByUserId?(esc(r.ADManagedByUserId)+(r.ADManagedByDisplayName?'<br/><small class=""text-muted"">'+esc(r.ADManagedByDisplayName)+'</small>':'')):(r.FoundInAD?'<em class=""text-muted"">Empty</em>':'<em class=""text-muted"">N/A</em>');
|
||||
tb.append('<tr class=""'+(r.IsMatch?'':'warning')+'"">'+'<td>'+icon+'</td><td>'+esc(r.SerialNumber)+'</td><td>'+esc(r.ComputerName||'')+'</td><td>'+discoUser+'</td><td>'+adUser+'</td><td>'+esc(r.MismatchReason||'')+'</td></tr>');
|
||||
}});
|
||||
}}
|
||||
|
||||
function filterResults() {{
|
||||
var issuesOnly = document.getElementById('chkIssuesOnly').checked;
|
||||
var searchTerm = document.getElementById('txtSearch').value.toLowerCase();
|
||||
var tbody = document.getElementById('resultsBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
var filtered = allResults.filter(function(r) {{
|
||||
if (issuesOnly && r.UserFoundInAD && !r.HasMismatches && !r.ADAccountDisabled) return false;
|
||||
if (searchTerm) {{
|
||||
var match = (r.UserId || '').toLowerCase().indexOf(searchTerm) >= 0 ||
|
||||
(r.DisplayName || '').toLowerCase().indexOf(searchTerm) >= 0;
|
||||
if (!match) return false;
|
||||
}}
|
||||
return true;
|
||||
}});
|
||||
|
||||
filtered.forEach(function(r) {{
|
||||
var statusIcon, statusClass;
|
||||
if (!r.UserFoundInAD) {{
|
||||
statusIcon = '<i class=""fa fa-times-circle text-danger""></i>';
|
||||
statusClass = 'danger';
|
||||
}} else if (r.ADAccountDisabled) {{
|
||||
statusIcon = '<i class=""fa fa-ban text-muted""></i>';
|
||||
statusClass = 'warning';
|
||||
}} else if (r.HasMismatches) {{
|
||||
statusIcon = '<i class=""fa fa-exclamation-triangle text-warning""></i>';
|
||||
statusClass = 'warning';
|
||||
}} else {{
|
||||
statusIcon = '<i class=""fa fa-check-circle text-success""></i>';
|
||||
statusClass = '';
|
||||
}}
|
||||
|
||||
var fields = r.HasMismatches
|
||||
? r.Mismatches.map(function(m) {{ return m.FieldName; }}).join(', ')
|
||||
: (!r.UserFoundInAD ? 'Not found in AD' : (r.ADAccountDisabled ? 'AD Account Disabled' : 'All matched'));
|
||||
|
||||
var details = r.HasMismatches
|
||||
? '<table class=""table table-condensed"" style=""margin:0; font-size:12px;"">' +
|
||||
r.Mismatches.map(function(m) {{
|
||||
return '<tr><td><strong>' + m.FieldName + '</strong></td>' +
|
||||
'<td>Disco: <code>' + escapeHtml(m.DiscoValue) + '</code></td>' +
|
||||
'<td>AD: <code>' + escapeHtml(m.ADValue) + '</code></td></tr>';
|
||||
}}).join('') + '</table>'
|
||||
: '';
|
||||
|
||||
var row = '<tr class=""' + statusClass + '"">' +
|
||||
'<td>' + statusIcon + '</td>' +
|
||||
'<td>' + escapeHtml(r.UserId) + '</td>' +
|
||||
'<td>' + escapeHtml(r.DisplayName || '') + '</td>' +
|
||||
'<td>' + fields + '</td>' +
|
||||
'<td>' + details + '</td></tr>';
|
||||
tbody.innerHTML += row;
|
||||
}});
|
||||
}}
|
||||
|
||||
function exportCsv() {{
|
||||
window.location.href = pluginBaseUrl + '/Export';
|
||||
}}
|
||||
|
||||
function escapeHtml(text) {{
|
||||
if (!text) return '';
|
||||
var div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode(text));
|
||||
return div.innerHTML;
|
||||
}}
|
||||
function esc(t){{if(!t)return'';var d=document.createElement('div');d.appendChild(document.createTextNode(t));return d.innerHTML;}}
|
||||
</script>";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user