using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using SQLInsight.Contracts;
using SQLInsight.Hub.Data;
using SQLInsight.Hub.Support;
namespace SQLInsight.Hub.Api;
///
/// Support cases for paying customers, through their agent:
/// POST /api/v1/cases creates a case (idempotent on ClientRequestId) and returns its number;
/// GET /api/v1/cases lists the customer's cases, also those SEOSoft created, so SQLInsight Web can show their status.
/// POST /api/v1/cases/{reference}/close closes one of the customer's own cases (idempotent).
///
public static class CasesEndpoint
{
private const int MaxListed = 200;
public static void Map(IEndpointRouteBuilder app)
{
app.MapPost("/api/v1/cases", CreateAsync).DisableAntiforgery().AllowAnonymous();
app.MapGet("/api/v1/cases", ListAsync).AllowAnonymous();
app.MapPost("/api/v1/cases/{reference}/close", CloseAsync).DisableAntiforgery().AllowAnonymous();
}
internal static async Task CreateAsync(
HttpRequest request, HubDbContext db, IOptions options, SupportNotifier notifier, TimeProvider time, CancellationToken cancellationToken)
{
var (license, rejected) = await FindingsEndpoint.AuthorizeAsync(request, db, options.Value, time, cancellationToken).ConfigureAwait(false);
if (license is null)
{
return rejected!;
}
if (!license.IncludesPrioritySupport)
{
return FindingsEndpoint.Problem(403, "Licensen giver ikke support", "Supportsager kræver Professional eller Enterprise.");
}
if (request.ContentLength > 64 * 1024)
{
return FindingsEndpoint.Problem(413, "For stor", "Højst 64 KB.");
}
SupportCaseRequest? body;
try
{
body = await JsonSerializer.DeserializeAsync(request.Body, JsonSerializerOptions.Web, cancellationToken).ConfigureAwait(false);
}
catch (JsonException ex)
{
return FindingsEndpoint.Problem(400, "Ugyldig JSON", ex.Message);
}
if (body is null)
{
return FindingsEndpoint.Problem(400, "Ugyldig sag", "Body mangler.");
}
if (body.Validate() is { } invalid)
{
return FindingsEndpoint.Problem(422, "Ugyldig sag", invalid);
}
var now = time.GetUtcNow().UtcDateTime;
await using var transaction = await db.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
await CustomerRegistration.LockAsync(db, license.Customer, cancellationToken).ConfigureAwait(false);
var record = await db.Licenses.SingleOrDefaultAsync(l => l.LicenseId == license.LicenseId, cancellationToken).ConfigureAwait(false);
if (record?.RevokedAt is not null)
{
return FindingsEndpoint.Problem(403, "Licensen er spærret", record.RevokedReason ?? "Kontakt SQLInsight-support.");
}
var (customer, _) = await CustomerRegistration.EnsureAsync(db, license, record, now, cancellationToken).ConfigureAwait(false);
var existing = await db.SupportCases.SingleOrDefaultAsync(c => c.CustomerId == customer.Id && c.ClientRequestId == body.ClientRequestId, cancellationToken).ConfigureAwait(false);
if (existing is not null)
{
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
return Results.Ok(SupportCaseStore.ToDto(existing));
}
var (created, error) = await SupportCaseStore.AddAsync(db, new SupportCase
{
CustomerId = customer.Id,
Reference = string.Empty,
Title = body.Title.Trim(),
Description = Trimmed(body.Description),
ContactName = body.ContactName.Trim(),
ContactEmail = body.ContactEmail.Trim(),
ContactPhone = Trimmed(body.ContactPhone),
RequestedBy = Trimmed(body.RequestedBy),
ClientRequestId = body.ClientRequestId,
Source = SupportCaseSources.Customer,
CreatedAt = now,
}, cancellationToken).ConfigureAwait(false);
if (created is null)
{
return FindingsEndpoint.Problem(409, "Sagen kunne ikke oprettes", error!);
}
// The finding the customer created the case from, if the hub has it as active and not yet in a case.
if (body.InstanceName is { Length: > 0 } instance && body.RuleId is { Length: > 0 } rule && body.Subject is { } subject)
{
var finding = await db.Findings.SingleOrDefaultAsync(f => f.Instance!.CustomerId == customer.Id && f.Instance.InstanceKey == instance
&& f.RuleId == rule && f.Subject == subject && f.ResolvedAt == null && f.SupportCaseId == null, cancellationToken).ConfigureAwait(false);
if (finding is not null)
{
finding.SupportCaseId = created.Id;
}
}
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
await notifier.NotifyAsync(created, customer.Name, cancellationToken).ConfigureAwait(false);
return Results.Created($"/api/v1/cases/{Uri.EscapeDataString(created.Reference)}", SupportCaseStore.ToDto(created));
}
internal static async Task ListAsync(
HttpRequest request, HubDbContext db, IOptions options, TimeProvider time, CancellationToken cancellationToken)
{
var (license, rejected) = await FindingsEndpoint.AuthorizeAsync(request, db, options.Value, time, cancellationToken).ConfigureAwait(false);
if (license is null)
{
return rejected!;
}
if (!license.IncludesPrioritySupport)
{
return FindingsEndpoint.Problem(403, "Licensen giver ikke support", "Supportsager kræver Professional eller Enterprise.");
}
var cases = await db.SupportCases.AsNoTracking()
.Where(c => c.Customer!.Name == license.Customer)
.OrderByDescending(c => c.CreatedAt)
.Take(MaxListed)
.ToListAsync(cancellationToken).ConfigureAwait(false);
return Results.Ok(cases.Select(SupportCaseStore.ToDto).ToList());
}
internal static async Task CloseAsync(
string reference, HttpRequest request, HubDbContext db, IOptions options, TimeProvider time, CancellationToken cancellationToken)
{
var (license, rejected) = await FindingsEndpoint.AuthorizeAsync(request, db, options.Value, time, cancellationToken).ConfigureAwait(false);
if (license is null)
{
return rejected!;
}
if (!license.IncludesPrioritySupport)
{
return FindingsEndpoint.Problem(403, "Licensen giver ikke support", "Supportsager kræver Professional eller Enterprise.");
}
SupportCaseCloseRequest body;
try
{
body = request.ContentLength is null or 0
? new SupportCaseCloseRequest(null, null)
: await JsonSerializer.DeserializeAsync(request.Body, JsonSerializerOptions.Web, cancellationToken).ConfigureAwait(false)
?? new SupportCaseCloseRequest(null, null);
}
catch (JsonException ex)
{
return FindingsEndpoint.Problem(400, "Ugyldig JSON", ex.Message);
}
if (body.Validate() is { } invalid)
{
return FindingsEndpoint.Problem(422, "Ugyldig lukning", invalid);
}
// Only the license's own customer's cases: anyone else's is simply not found.
var supportCase = await db.SupportCases
.SingleOrDefaultAsync(c => c.Customer!.Name == license.Customer && c.Reference == reference, cancellationToken).ConfigureAwait(false);
if (supportCase is null)
{
return FindingsEndpoint.Problem(404, "Ukendt sag", $"Sagen {reference} findes ikke.");
}
if (supportCase.IsOpen)
{
supportCase.IsOpen = false;
supportCase.ClosedAt = time.GetUtcNow().UtcDateTime;
supportCase.ClosedBy = "Kunde: " + (string.IsNullOrWhiteSpace(body.RequestedBy) ? "ukendt" : body.RequestedBy.Trim());
supportCase.ClosedComment = Trimmed(body.Comment);
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
return Results.Ok(SupportCaseStore.ToDto(supportCase));
}
private static string? Trimmed(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}