sqlinsight-pro/src/SQLInsight.Web/Support.cs
Søren Eggert Lundsteen Olsen 50bc878776 Customers file support cases in SQLInsight Web and follow their status
- 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>
2026-10-01 11:34:06 +02:00

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();
}