using FastEndpoints; using Socialize.Api.Data; using Socialize.Api.Infrastructure.Security; using Socialize.Api.Modules.Feedback.Contracts; using Socialize.Api.Modules.Feedback.Data; using Socialize.Api.Modules.Feedback.Services; namespace Socialize.Api.Modules.Feedback.Handlers; public record SubmitFeedbackRequest( string Type, string Description, string SubmittedPath, string? BrowserUserAgent, int? ViewportWidth, int? ViewportHeight, string? AppVersion, Guid? WorkspaceId, string? WorkspaceName, Guid? ClientId, string? ClientName, Guid? ProjectId, string? ProjectName, Guid? ContentItemId, string? ContentItemTitle); public class SubmitFeedbackRequestValidator : Validator { public SubmitFeedbackRequestValidator() { RuleFor(x => x.Type).NotEmpty().MaximumLength(32); RuleFor(x => x.Description).NotEmpty().MaximumLength(8000); RuleFor(x => x.SubmittedPath).NotEmpty().MaximumLength(2048); RuleFor(x => x.BrowserUserAgent).MaximumLength(1024); RuleFor(x => x.AppVersion).MaximumLength(128); RuleFor(x => x.WorkspaceName).MaximumLength(256); RuleFor(x => x.ClientName).MaximumLength(256); RuleFor(x => x.ProjectName).MaximumLength(256); RuleFor(x => x.ContentItemTitle).MaximumLength(256); RuleFor(x => x.ViewportWidth).GreaterThan(0).When(x => x.ViewportWidth.HasValue); RuleFor(x => x.ViewportHeight).GreaterThan(0).When(x => x.ViewportHeight.HasValue); } } public class SubmitFeedbackHandler(AppDbContext dbContext) : Endpoint { public override void Configure() { Post("/api/feedback"); Options(o => o.WithTags("Feedback")); } public override async Task HandleAsync(SubmitFeedbackRequest request, CancellationToken ct) { if (!FeedbackRules.TryParseType(request.Type, out FeedbackType type)) { AddError(request => request.Type, "The selected feedback type is not valid."); await SendErrorsAsync(StatusCodes.Status400BadRequest, ct); return; } DateTimeOffset now = DateTimeOffset.UtcNow; FeedbackReport report = new() { Id = Guid.NewGuid(), Type = type, Status = FeedbackStatus.New, Description = request.Description.Trim(), ReporterUserId = User.GetUserId(), ReporterDisplayName = User.GetAlias() ?? User.GetName(), ReporterEmail = User.GetEmail(), SubmittedPath = request.SubmittedPath.Trim(), BrowserUserAgent = NormalizeOptional(request.BrowserUserAgent), ViewportWidth = request.ViewportWidth, ViewportHeight = request.ViewportHeight, AppVersion = NormalizeOptional(request.AppVersion), WorkspaceId = request.WorkspaceId, WorkspaceName = NormalizeOptional(request.WorkspaceName), ClientId = request.ClientId, ClientName = NormalizeOptional(request.ClientName), ProjectId = request.ProjectId, ProjectName = NormalizeOptional(request.ProjectName), ContentItemId = request.ContentItemId, ContentItemTitle = NormalizeOptional(request.ContentItemTitle), CreatedAt = now, LastActivityAt = now, }; dbContext.FeedbackReports.Add(report); await dbContext.SaveChangesAsync(ct); await SendAsync(report.ToDto(), StatusCodes.Status201Created, ct); } private static string? NormalizeOptional(string? value) { string? normalized = value?.Trim(); return string.IsNullOrWhiteSpace(normalized) ? null : normalized; } }