- Repository migration 0005: insight.SupportCases. The web UI has only db_datareader, so INSERT on this one table is granted to public. - Web: /support (menu "Support") lists the customer's cases and, with Hub sync on and a Professional/Enterprise license, a form (title, description, contact; contact prefilled from the Windows user). /support?fund=<id> prefills from a finding: instance, rule, severity, explanation and non-sensitive measurements. - "Opret supportsag" on findings: right-click a finding link or a finding's heading (support-menu.js, a static file within the CSP; Shift+F10/context key, Escape closes), and a visible button on the finding page that works without JavaScript. Only when support is available. - Agent: sends pending requests to POST /api/v1/cases, stores the case number, backs off on failures and gives up on 400/401/403/422; fetches the customer's cases every 5 minutes (at once after sending). - Docs: support.md (also on the community site), databehandling.md (contact details are sent to the hub). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
88 lines
4.4 KiB
C#
88 lines
4.4 KiB
C#
using System.Text;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using SQLInsight.Contracts;
|
|
using SQLInsight.Repository;
|
|
|
|
namespace SQLInsight.Web;
|
|
|
|
/// <summary>
|
|
/// Support cases with SEOSoft (Professional/Enterprise). The form posts here; the request is stored in the repository
|
|
/// and the agent sends it to SQLInsight Hub, which gives it a case number. The web UI never talks to the hub itself.
|
|
/// </summary>
|
|
public static class Support
|
|
{
|
|
public static void Map(IEndpointRouteBuilder app) => app.MapPost("/support", CreateAsync);
|
|
|
|
/// <summary>Title and description for a case about <paramref name="finding"/>. Sensitive measurements are left out.</summary>
|
|
public static (string Title, string Description) Prefill(StoredFinding finding)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(finding);
|
|
var text = new StringBuilder();
|
|
text.Append("Instans: ").Append(finding.InstanceName).Append('\n');
|
|
text.Append("Regel: ").Append(finding.RuleId).Append('\n');
|
|
text.Append("Alvor: ").Append(Display.SeverityName(finding.Severity)).Append('\n');
|
|
if (!string.IsNullOrEmpty(finding.Subject))
|
|
{
|
|
text.Append("Emne: ").Append(finding.Subject).Append('\n');
|
|
}
|
|
|
|
text.Append('\n').Append(finding.Explanation).Append('\n');
|
|
var evidence = finding.Evidence.Where(e => !e.Sensitive).Take(10).ToList();
|
|
if (evidence.Count > 0)
|
|
{
|
|
text.Append("\nMålinger:\n");
|
|
foreach (var e in evidence)
|
|
{
|
|
text.Append("- ").Append(e.Name).Append(": ").Append(e.Value.Length > 200 ? e.Value[..200] + "…" : e.Value).Append('\n');
|
|
}
|
|
}
|
|
|
|
text.Append("\nHvad har I prøvet, og hvad har ændret sig?\n");
|
|
var title = finding.Title.Length > SupportCaseRequest.MaxTitle ? finding.Title[..SupportCaseRequest.MaxTitle] : finding.Title;
|
|
var description = text.ToString();
|
|
return (title, description.Length > SupportCaseRequest.MaxDescription ? description[..SupportCaseRequest.MaxDescription] : description);
|
|
}
|
|
|
|
/// <summary>The user part of DOMAIN\user, as a starting point for the contact name.</summary>
|
|
public static string ContactNameFrom(string? windowsUser) =>
|
|
windowsUser is { Length: > 0 } user ? user[(user.LastIndexOf('\\') + 1)..] : string.Empty;
|
|
|
|
public static string StatusText(SupportCaseRow row)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(row);
|
|
return row.Status switch
|
|
{
|
|
SupportCaseStatus.Pending => row.Attempts > 0 ? "Venter på hubben (prøver igen)" : "Sendes",
|
|
SupportCaseStatus.Open => "Åben",
|
|
SupportCaseStatus.Closed => "Lukket",
|
|
_ => "Afvist",
|
|
};
|
|
}
|
|
|
|
private static async Task<IResult> CreateAsync(
|
|
HttpContext http, InsightRepository repository,
|
|
[FromForm] string? title, [FromForm] string? description, [FromForm] string? contactName, [FromForm] string? contactEmail,
|
|
[FromForm] string? contactPhone, [FromForm] long? findingId)
|
|
{
|
|
var back = findingId is { } id ? $"&fund={id}" : string.Empty;
|
|
if ((await repository.GetHubStatusAsync(http.RequestAborted))?.AllowsSupportCases != true)
|
|
{
|
|
return Results.Redirect("/support?fejl=" + Uri.EscapeDataString("Supportsager kræver Professional eller Enterprise og at deling med SQLInsight Hub er slået til."));
|
|
}
|
|
|
|
var finding = findingId is { } fid ? await repository.GetFindingAsync(fid, http.RequestAborted) : null;
|
|
var request = new SupportCaseRequest(Guid.NewGuid(), title?.Trim() ?? string.Empty, Blank(description), contactName?.Trim() ?? string.Empty,
|
|
contactEmail?.Trim() ?? string.Empty, Blank(contactPhone), http.User.Identity?.Name, finding?.InstanceName, finding?.RuleId, finding?.Subject);
|
|
if (request.Validate() is { } invalid)
|
|
{
|
|
return Results.Redirect("/support?fejl=" + Uri.EscapeDataString(invalid) + back);
|
|
}
|
|
|
|
await repository.AddSupportRequestAsync(new NewSupportRequest(request.ClientRequestId, request.Title, request.Description, request.ContactName,
|
|
request.ContactEmail, request.ContactPhone, request.RequestedBy, finding?.FindingId, finding?.InstanceName, finding?.RuleId, finding?.Subject),
|
|
DateTimeOffset.UtcNow, http.RequestAborted);
|
|
return Results.Redirect("/support?sendt=1");
|
|
}
|
|
|
|
private static string? Blank(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
|
}
|