Remove FluentValidation; validator/DI cleanup #7
40
src/api/MicCheck.Api/Common/Guard.cs
Normal file
40
src/api/MicCheck.Api/Common/Guard.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
namespace MicCheck.Api.Common;
|
||||
|
||||
public static class Guard
|
||||
{
|
||||
public static void Null<T>(T t, string parameterName) where T : class
|
||||
{
|
||||
if (t is null)
|
||||
throw new ArgumentNullException(parameterName, $"{nameof(parameterName)} can not be null");
|
||||
}
|
||||
|
||||
public static void Empty(string value, string parameterName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
throw new ArgumentException($"{parameterName} can not be empty", parameterName);
|
||||
}
|
||||
|
||||
public static void Empty<T>(IEnumerable<T> collection, string parameterName)
|
||||
{
|
||||
if (collection == null || !collection.Any())
|
||||
throw new ArgumentException($"{parameterName} can not be empty", parameterName);
|
||||
}
|
||||
|
||||
public static void Negative(int value, string parameterName)
|
||||
{
|
||||
if (value < 0)
|
||||
throw new ArgumentOutOfRangeException(parameterName, $"{parameterName} must be a positive number or zero");
|
||||
}
|
||||
|
||||
public static void NegativeOrZero(int value, string parameterName)
|
||||
{
|
||||
if (value <= 0)
|
||||
throw new ArgumentOutOfRangeException(parameterName, $"{nameof(parameterName)} must be a positive number greater then zero");
|
||||
}
|
||||
|
||||
public static void Default<T>(T value, string parameterName)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(value, default))
|
||||
throw new ArgumentException($"{parameterName} can not be a default value", parameterName);
|
||||
}
|
||||
}
|
||||
13
src/api/MicCheck.Api/Common/Validation/IModelValidator.cs
Normal file
13
src/api/MicCheck.Api/Common/Validation/IModelValidator.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace MicCheck.Api.Common.Validation;
|
||||
|
||||
public interface IModelValidator
|
||||
{
|
||||
ValidationResult Validate(object model);
|
||||
}
|
||||
|
||||
public interface IModelValidator<in T> : IModelValidator
|
||||
{
|
||||
ValidationResult Validate(T model);
|
||||
|
||||
ValidationResult IModelValidator.Validate(object model) => Validate((T)model);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace MicCheck.Api.Common.Validation;
|
||||
|
||||
public class ModelValidationActionFilter : IActionFilter
|
||||
{
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
foreach (var argument in context.ActionArguments.Values)
|
||||
{
|
||||
if (argument is null) continue;
|
||||
|
||||
var validatorType = typeof(IModelValidator<>).MakeGenericType(argument.GetType());
|
||||
if (context.HttpContext.RequestServices.GetService(validatorType) is not IModelValidator validator) continue;
|
||||
|
||||
var result = validator.Validate(argument);
|
||||
foreach (var error in result.Errors)
|
||||
context.ModelState.AddModelError(error.PropertyName, error.Message);
|
||||
}
|
||||
|
||||
if (!context.ModelState.IsValid)
|
||||
context.Result = ValidationProblemResponseFactory.Create(context.ModelState);
|
||||
}
|
||||
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace MicCheck.Api.Common.Validation;
|
||||
|
||||
public static class ModelValidatorServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddModelValidatorsFromAssemblyContaining<TMarker>(this IServiceCollection services)
|
||||
{
|
||||
var registrations = typeof(TMarker).Assembly.GetTypes()
|
||||
.Where(type => !type.IsAbstract && !type.IsInterface)
|
||||
.SelectMany(type => type.GetInterfaces()
|
||||
.Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IModelValidator<>))
|
||||
.Select(i => (Interface: i, Implementation: type)));
|
||||
|
||||
foreach (var (@interface, implementation) in registrations)
|
||||
services.AddScoped(@interface, implementation);
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace MicCheck.Api.Common.Validation;
|
||||
|
||||
public static class ValidationProblemResponseFactory
|
||||
{
|
||||
public static IActionResult Create(ModelStateDictionary modelState)
|
||||
{
|
||||
var errors = modelState
|
||||
.Where(e => e.Value?.Errors.Count > 0)
|
||||
.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => kvp.Value!.Errors.Select(e => e.ErrorMessage).ToArray());
|
||||
|
||||
return new UnprocessableEntityObjectResult(new { errors });
|
||||
}
|
||||
}
|
||||
14
src/api/MicCheck.Api/Common/Validation/ValidationResult.cs
Normal file
14
src/api/MicCheck.Api/Common/Validation/ValidationResult.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace MicCheck.Api.Common.Validation;
|
||||
|
||||
public record ValidationError(string PropertyName, string Message);
|
||||
|
||||
public class ValidationResult
|
||||
{
|
||||
private readonly List<ValidationError> _errors = [];
|
||||
|
||||
public IReadOnlyList<ValidationError> Errors => _errors;
|
||||
public bool IsValid => _errors.Count == 0;
|
||||
public bool IsInvalid => _errors.Count > 0;
|
||||
|
||||
public void AddError(string propertyName, string message) => _errors.Add(new ValidationError(propertyName, message));
|
||||
}
|
||||
@@ -1,13 +1,20 @@
|
||||
using FluentValidation;
|
||||
using MicCheck.Api.Common.Validation;
|
||||
|
||||
namespace MicCheck.Api.Environments;
|
||||
|
||||
public record CloneEnvironmentRequest(string Name);
|
||||
|
||||
public class CloneEnvironmentRequestValidator : AbstractValidator<CloneEnvironmentRequest>
|
||||
public class CloneEnvironmentRequestValidator : IModelValidator<CloneEnvironmentRequest>
|
||||
{
|
||||
public CloneEnvironmentRequestValidator()
|
||||
public ValidationResult Validate(CloneEnvironmentRequest model)
|
||||
{
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
|
||||
var result = new ValidationResult();
|
||||
|
||||
if (string.IsNullOrEmpty(model.Name))
|
||||
result.AddError(nameof(model.Name), "'Name' must not be empty.");
|
||||
else if (model.Name.Length > 200)
|
||||
result.AddError(nameof(model.Name), "'Name' must be 200 characters or fewer.");
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
using FluentValidation;
|
||||
using MicCheck.Api.Common.Validation;
|
||||
|
||||
namespace MicCheck.Api.Environments;
|
||||
|
||||
public record CreateEnvironmentRequest(string Name, int ProjectId);
|
||||
|
||||
public class CreateEnvironmentRequestValidator : AbstractValidator<CreateEnvironmentRequest>
|
||||
public class CreateEnvironmentRequestValidator : IModelValidator<CreateEnvironmentRequest>
|
||||
{
|
||||
public CreateEnvironmentRequestValidator()
|
||||
public ValidationResult Validate(CreateEnvironmentRequest model)
|
||||
{
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
|
||||
RuleFor(x => x.ProjectId).GreaterThan(0);
|
||||
var result = new ValidationResult();
|
||||
|
||||
if (string.IsNullOrEmpty(model.Name))
|
||||
result.AddError(nameof(model.Name), "'Name' must not be empty.");
|
||||
else if (model.Name.Length > 200)
|
||||
result.AddError(nameof(model.Name), "'Name' must be 200 characters or fewer.");
|
||||
|
||||
if (model.ProjectId <= 0)
|
||||
result.AddError(nameof(model.ProjectId), "'Project Id' must be greater than 0.");
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
using FluentValidation;
|
||||
using MicCheck.Api.Common.Validation;
|
||||
|
||||
namespace MicCheck.Api.Environments;
|
||||
|
||||
public record UpdateEnvironmentRequest(string Name);
|
||||
|
||||
public class UpdateEnvironmentRequestValidator : AbstractValidator<UpdateEnvironmentRequest>
|
||||
public class UpdateEnvironmentRequestValidator : IModelValidator<UpdateEnvironmentRequest>
|
||||
{
|
||||
public UpdateEnvironmentRequestValidator()
|
||||
public ValidationResult Validate(UpdateEnvironmentRequest model)
|
||||
{
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
|
||||
var result = new ValidationResult();
|
||||
|
||||
if (string.IsNullOrEmpty(model.Name))
|
||||
result.AddError(nameof(model.Name), "'Name' must not be empty.");
|
||||
else if (model.Name.Length > 200)
|
||||
result.AddError(nameof(model.Name), "'Name' must be 200 characters or fewer.");
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using FluentValidation;
|
||||
using System.Text.RegularExpressions;
|
||||
using MicCheck.Api.Common.Validation;
|
||||
|
||||
namespace MicCheck.Api.Features;
|
||||
|
||||
@@ -9,18 +10,24 @@ public record CreateFeatureRequest(
|
||||
string? Description
|
||||
);
|
||||
|
||||
public class CreateFeatureRequestValidator : AbstractValidator<CreateFeatureRequest>
|
||||
public class CreateFeatureRequestValidator : IModelValidator<CreateFeatureRequest>
|
||||
{
|
||||
public CreateFeatureRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty()
|
||||
.MaximumLength(150)
|
||||
.Matches("^[a-zA-Z0-9_-]+$")
|
||||
.WithMessage("Name may only contain letters, digits, underscores, and hyphens.");
|
||||
private static readonly Regex NamePattern = new("^[a-zA-Z0-9_-]+$");
|
||||
|
||||
RuleFor(x => x.InitialValue)
|
||||
.MaximumLength(20_000)
|
||||
.When(x => x.InitialValue is not null);
|
||||
public ValidationResult Validate(CreateFeatureRequest model)
|
||||
{
|
||||
var result = new ValidationResult();
|
||||
|
||||
if (string.IsNullOrEmpty(model.Name))
|
||||
result.AddError(nameof(model.Name), "'Name' must not be empty.");
|
||||
else if (model.Name.Length > 150)
|
||||
result.AddError(nameof(model.Name), "'Name' must be 150 characters or fewer.");
|
||||
else if (!NamePattern.IsMatch(model.Name))
|
||||
result.AddError(nameof(model.Name), "Name may only contain letters, digits, underscores, and hyphens.");
|
||||
|
||||
if (model.InitialValue is not null && model.InitialValue.Length > 20_000)
|
||||
result.AddError(nameof(model.InitialValue), "'Initial Value' must be 20000 characters or fewer.");
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
using FluentValidation;
|
||||
using System.Text.RegularExpressions;
|
||||
using MicCheck.Api.Common.Validation;
|
||||
|
||||
namespace MicCheck.Api.Features;
|
||||
|
||||
public record CreateTagRequest(string Label, string Color);
|
||||
|
||||
public class CreateTagRequestValidator : AbstractValidator<CreateTagRequest>
|
||||
public class CreateTagRequestValidator : IModelValidator<CreateTagRequest>
|
||||
{
|
||||
public CreateTagRequestValidator()
|
||||
private static readonly Regex ColorPattern = new("^#[0-9A-Fa-f]{3,6}$");
|
||||
|
||||
public ValidationResult Validate(CreateTagRequest model)
|
||||
{
|
||||
RuleFor(x => x.Label).NotEmpty().MaximumLength(100);
|
||||
RuleFor(x => x.Color).NotEmpty().MaximumLength(20)
|
||||
.Matches("^#[0-9A-Fa-f]{3,6}$")
|
||||
.WithMessage("Color must be a valid hex color (e.g. #FF0000).");
|
||||
var result = new ValidationResult();
|
||||
|
||||
if (string.IsNullOrEmpty(model.Label))
|
||||
result.AddError(nameof(model.Label), "'Label' must not be empty.");
|
||||
else if (model.Label.Length > 100)
|
||||
result.AddError(nameof(model.Label), "'Label' must be 100 characters or fewer.");
|
||||
|
||||
if (string.IsNullOrEmpty(model.Color))
|
||||
result.AddError(nameof(model.Color), "'Color' must not be empty.");
|
||||
else if (model.Color.Length > 20)
|
||||
result.AddError(nameof(model.Color), "'Color' must be 20 characters or fewer.");
|
||||
else if (!ColorPattern.IsMatch(model.Color))
|
||||
result.AddError(nameof(model.Color), "Color must be a valid hex color (e.g. #FF0000).");
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,13 +23,7 @@ public class FeatureService(IMicCheckDbContext db, AuditService auditService, We
|
||||
return await db.Features.Include(f => f.Tags).FirstOrDefaultAsync(f => f.Id == id, ct);
|
||||
}
|
||||
|
||||
public async Task<Feature> CreateAsync(
|
||||
int projectId,
|
||||
string name,
|
||||
FeatureType type,
|
||||
string? initialValue,
|
||||
string? description,
|
||||
CancellationToken ct = default)
|
||||
public async Task<Feature> CreateAsync(int projectId, string name, FeatureType type, string? initialValue, string? description, CancellationToken ct = default)
|
||||
{
|
||||
var count = await db.Features.CountAsync(f => f.ProjectId == projectId, ct);
|
||||
if (count >= MaxFeaturesPerProject)
|
||||
@@ -78,8 +72,7 @@ public class FeatureService(IMicCheckDbContext db, AuditService auditService, We
|
||||
return feature;
|
||||
}
|
||||
|
||||
public async Task<Feature> UpdateAsync(
|
||||
int id, string name, string? description, CancellationToken ct = default)
|
||||
public async Task<Feature> UpdateAsync(int id, string name, string? description, CancellationToken ct = default)
|
||||
{
|
||||
var feature = await db.Features.Include(f => f.Tags).FirstOrDefaultAsync(f => f.Id == id, ct)
|
||||
?? throw new KeyNotFoundException($"Feature {id} not found.");
|
||||
@@ -162,11 +155,7 @@ public class FeatureService(IMicCheckDbContext db, AuditService auditService, We
|
||||
EventType = WebhookEventTypes.FlagDeleted,
|
||||
EnvironmentId = env.Id,
|
||||
OrganizationId = project.OrganizationId,
|
||||
Data = new FlagDeletedData(
|
||||
null,
|
||||
DateTimeOffset.UtcNow,
|
||||
new FeatureSummary(feature.Id, feature.Name))
|
||||
});
|
||||
Data = new FlagDeletedData(null, DateTimeOffset.UtcNow, new FeatureSummary(feature.Id, feature.Name)) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using FluentValidation;
|
||||
using System.Text.RegularExpressions;
|
||||
using MicCheck.Api.Common.Validation;
|
||||
|
||||
namespace MicCheck.Api.Features;
|
||||
|
||||
@@ -7,14 +8,21 @@ public record UpdateFeatureRequest(
|
||||
string? Description
|
||||
);
|
||||
|
||||
public class UpdateFeatureRequestValidator : AbstractValidator<UpdateFeatureRequest>
|
||||
public class UpdateFeatureRequestValidator : IModelValidator<UpdateFeatureRequest>
|
||||
{
|
||||
public UpdateFeatureRequestValidator()
|
||||
private static readonly Regex NamePattern = new("^[a-zA-Z0-9_-]+$");
|
||||
|
||||
public ValidationResult Validate(UpdateFeatureRequest model)
|
||||
{
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty()
|
||||
.MaximumLength(150)
|
||||
.Matches("^[a-zA-Z0-9_-]+$")
|
||||
.WithMessage("Name may only contain letters, digits, underscores, and hyphens.");
|
||||
var result = new ValidationResult();
|
||||
|
||||
if (string.IsNullOrEmpty(model.Name))
|
||||
result.AddError(nameof(model.Name), "'Name' must not be empty.");
|
||||
else if (model.Name.Length > 150)
|
||||
result.AddError(nameof(model.Name), "'Name' must be 150 characters or fewer.");
|
||||
else if (!NamePattern.IsMatch(model.Name))
|
||||
result.AddError(nameof(model.Name), "Name may only contain letters, digits, underscores, and hyphens.");
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.OpenApi" Version="2.9.0" />
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
using FluentValidation;
|
||||
using MicCheck.Api.Common.Validation;
|
||||
|
||||
namespace MicCheck.Api.Organizations;
|
||||
|
||||
public record CreateOrganizationRequest(string Name);
|
||||
|
||||
public class CreateOrganizationRequestValidator : AbstractValidator<CreateOrganizationRequest>
|
||||
public class CreateOrganizationRequestValidator : IModelValidator<CreateOrganizationRequest>
|
||||
{
|
||||
public CreateOrganizationRequestValidator()
|
||||
public ValidationResult Validate(CreateOrganizationRequest model)
|
||||
{
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
|
||||
var result = new ValidationResult();
|
||||
|
||||
if (string.IsNullOrEmpty(model.Name))
|
||||
result.AddError(nameof(model.Name), "'Name' must not be empty.");
|
||||
else if (model.Name.Length > 200)
|
||||
result.AddError(nameof(model.Name), "'Name' must be 200 characters or fewer.");
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
using FluentValidation;
|
||||
using MicCheck.Api.Common.Validation;
|
||||
|
||||
namespace MicCheck.Api.Organizations;
|
||||
|
||||
public record InviteUserRequest(int UserId, string Role);
|
||||
|
||||
public class InviteUserRequestValidator : AbstractValidator<InviteUserRequest>
|
||||
public class InviteUserRequestValidator : IModelValidator<InviteUserRequest>
|
||||
{
|
||||
public InviteUserRequestValidator()
|
||||
public ValidationResult Validate(InviteUserRequest model)
|
||||
{
|
||||
RuleFor(x => x.UserId).GreaterThan(0);
|
||||
RuleFor(x => x.Role).NotEmpty().Must(r => Enum.TryParse<OrganizationRole>(r, true, out _))
|
||||
.WithMessage("Role must be 'User' or 'Admin'.");
|
||||
var result = new ValidationResult();
|
||||
|
||||
if (model.UserId <= 0)
|
||||
result.AddError(nameof(model.UserId), "'User Id' must be greater than 0.");
|
||||
|
||||
if (string.IsNullOrEmpty(model.Role))
|
||||
result.AddError(nameof(model.Role), "'Role' must not be empty.");
|
||||
else if (!Enum.TryParse<OrganizationRole>(model.Role, true, out _))
|
||||
result.AddError(nameof(model.Role), "Role must be 'User' or 'Admin'.");
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
using FluentValidation;
|
||||
using MicCheck.Api.Common.Validation;
|
||||
|
||||
namespace MicCheck.Api.Organizations;
|
||||
|
||||
public record UpdateOrganizationRequest(string Name);
|
||||
|
||||
public class UpdateOrganizationRequestValidator : AbstractValidator<UpdateOrganizationRequest>
|
||||
public class UpdateOrganizationRequestValidator : IModelValidator<UpdateOrganizationRequest>
|
||||
{
|
||||
public UpdateOrganizationRequestValidator()
|
||||
public ValidationResult Validate(UpdateOrganizationRequest model)
|
||||
{
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
|
||||
var result = new ValidationResult();
|
||||
|
||||
if (string.IsNullOrEmpty(model.Name))
|
||||
result.AddError(nameof(model.Name), "'Name' must not be empty.");
|
||||
else if (model.Name.Length > 200)
|
||||
result.AddError(nameof(model.Name), "'Name' must be 200 characters or fewer.");
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
using System.Text;
|
||||
using System.Threading.RateLimiting;
|
||||
using FluentValidation;
|
||||
using FluentValidation.AspNetCore;
|
||||
using MicCheck.Api.Common.Security.ApiKeys;
|
||||
using MicCheck.Api.Audit;
|
||||
using MicCheck.Api.Common.Security.Authentication;
|
||||
using MicCheck.Api.Common.Security.Authorization;
|
||||
using MicCheck.Api.Common.Validation;
|
||||
using MicCheck.Api.Data;
|
||||
using MicCheck.Api.Environments;
|
||||
using MicCheck.Api.Features;
|
||||
@@ -42,7 +41,7 @@ try
|
||||
.WriteTo.Console());
|
||||
|
||||
builder.Services.AddOpenApi();
|
||||
builder.Services.AddControllers()
|
||||
builder.Services.AddControllers(options => options.Filters.Add<ModelValidationActionFilter>())
|
||||
.AddJsonOptions(options =>
|
||||
options.JsonSerializerOptions.Converters.Add(
|
||||
new System.Text.Json.Serialization.JsonStringEnumConverter()));
|
||||
@@ -92,20 +91,11 @@ try
|
||||
limiter.QueueLimit = 0;
|
||||
}));
|
||||
|
||||
builder.Services.AddFluentValidationAutoValidation();
|
||||
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
|
||||
builder.Services.AddModelValidatorsFromAssemblyContaining<Program>();
|
||||
|
||||
builder.Services.Configure<ApiBehaviorOptions>(options =>
|
||||
{
|
||||
options.InvalidModelStateResponseFactory = context =>
|
||||
{
|
||||
var errors = context.ModelState
|
||||
.Where(e => e.Value?.Errors.Count > 0)
|
||||
.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => kvp.Value!.Errors.Select(e => e.ErrorMessage).ToArray());
|
||||
return new UnprocessableEntityObjectResult(new { errors });
|
||||
};
|
||||
options.InvalidModelStateResponseFactory = context => ValidationProblemResponseFactory.Create(context.ModelState);
|
||||
});
|
||||
|
||||
builder.Services.AddMemoryCache();
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
using FluentValidation;
|
||||
using MicCheck.Api.Common.Validation;
|
||||
|
||||
namespace MicCheck.Api.Projects;
|
||||
|
||||
public record CreateProjectRequest(string Name, int OrganizationId);
|
||||
|
||||
public class CreateProjectRequestValidator : AbstractValidator<CreateProjectRequest>
|
||||
public class CreateProjectRequestValidator : IModelValidator<CreateProjectRequest>
|
||||
{
|
||||
public CreateProjectRequestValidator()
|
||||
public ValidationResult Validate(CreateProjectRequest model)
|
||||
{
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
|
||||
RuleFor(x => x.OrganizationId).GreaterThan(0);
|
||||
var result = new ValidationResult();
|
||||
|
||||
if (string.IsNullOrEmpty(model.Name))
|
||||
result.AddError(nameof(model.Name), "'Name' must not be empty.");
|
||||
else if (model.Name.Length > 200)
|
||||
result.AddError(nameof(model.Name), "'Name' must be 200 characters or fewer.");
|
||||
|
||||
if (model.OrganizationId <= 0)
|
||||
result.AddError(nameof(model.OrganizationId), "'Organization Id' must be greater than 0.");
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
using FluentValidation;
|
||||
using MicCheck.Api.Common.Security.Authorization;
|
||||
|
||||
namespace MicCheck.Api.Projects;
|
||||
|
||||
public record SetUserPermissionsRequest(int UserId, bool IsAdmin, List<string> Permissions);
|
||||
|
||||
public class SetUserPermissionsRequestValidator : AbstractValidator<SetUserPermissionsRequest>
|
||||
{
|
||||
public SetUserPermissionsRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.UserId).GreaterThan(0);
|
||||
RuleForEach(x => x.Permissions)
|
||||
.Must(p => Enum.TryParse<ProjectPermission>(p, true, out _))
|
||||
.WithMessage("Invalid permission value.");
|
||||
}
|
||||
}
|
||||
using MicCheck.Api.Common.Security.Authorization;
|
||||
using MicCheck.Api.Common.Validation;
|
||||
|
||||
namespace MicCheck.Api.Projects;
|
||||
|
||||
public record SetUserPermissionsRequest(int UserId, bool IsAdmin, List<string> Permissions);
|
||||
|
||||
public class SetUserPermissionsRequestValidator : IModelValidator<SetUserPermissionsRequest>
|
||||
{
|
||||
public ValidationResult Validate(SetUserPermissionsRequest model)
|
||||
{
|
||||
var result = new ValidationResult();
|
||||
|
||||
if (model.UserId <= 0)
|
||||
result.AddError(nameof(model.UserId), "'User Id' must be greater than 0.");
|
||||
|
||||
foreach (var permission in model.Permissions)
|
||||
{
|
||||
if (!Enum.TryParse<ProjectPermission>(permission, true, out _))
|
||||
result.AddError(nameof(model.Permissions), "Invalid permission value.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
using FluentValidation;
|
||||
using MicCheck.Api.Common.Validation;
|
||||
|
||||
namespace MicCheck.Api.Projects;
|
||||
|
||||
public record UpdateProjectRequest(string Name, bool HideDisabledFlags);
|
||||
|
||||
public class UpdateProjectRequestValidator : AbstractValidator<UpdateProjectRequest>
|
||||
public class UpdateProjectRequestValidator : IModelValidator<UpdateProjectRequest>
|
||||
{
|
||||
public UpdateProjectRequestValidator()
|
||||
public ValidationResult Validate(UpdateProjectRequest model)
|
||||
{
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
|
||||
var result = new ValidationResult();
|
||||
|
||||
if (string.IsNullOrEmpty(model.Name))
|
||||
result.AddError(nameof(model.Name), "'Name' must not be empty.");
|
||||
else if (model.Name.Length > 200)
|
||||
result.AddError(nameof(model.Name), "'Name' must be 200 characters or fewer.");
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using FluentValidation;
|
||||
using MicCheck.Api.Common.Validation;
|
||||
|
||||
namespace MicCheck.Api.Segments;
|
||||
|
||||
@@ -16,24 +16,38 @@ public record CreateSegmentRequest(
|
||||
string Name,
|
||||
IReadOnlyList<CreateSegmentRuleRequest> Rules);
|
||||
|
||||
public class CreateSegmentRequestValidator : AbstractValidator<CreateSegmentRequest>
|
||||
public class CreateSegmentRequestValidator : IModelValidator<CreateSegmentRequest>
|
||||
{
|
||||
public CreateSegmentRequestValidator()
|
||||
public ValidationResult Validate(CreateSegmentRequest model)
|
||||
{
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
|
||||
RuleFor(x => x.Rules).NotNull();
|
||||
RuleForEach(x => x.Rules).ChildRules(rule =>
|
||||
var result = new ValidationResult();
|
||||
|
||||
if (string.IsNullOrEmpty(model.Name))
|
||||
result.AddError(nameof(model.Name), "'Name' must not be empty.");
|
||||
else if (model.Name.Length > 200)
|
||||
result.AddError(nameof(model.Name), "'Name' must be 200 characters or fewer.");
|
||||
|
||||
if (model.Rules is null)
|
||||
{
|
||||
rule.RuleFor(r => r.Type)
|
||||
.Must(t => Enum.TryParse<SegmentRuleType>(t, true, out _))
|
||||
.WithMessage("Rule type must be 'All', 'Any', or 'None'.");
|
||||
rule.RuleForEach(r => r.Conditions).ChildRules(cond =>
|
||||
result.AddError(nameof(model.Rules), "'Rules' must not be empty.");
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (var rule in model.Rules)
|
||||
{
|
||||
if (!Enum.TryParse<SegmentRuleType>(rule.Type, true, out _))
|
||||
result.AddError(nameof(model.Rules), "Rule type must be 'All', 'Any', or 'None'.");
|
||||
|
||||
foreach (var condition in rule.Conditions)
|
||||
{
|
||||
cond.RuleFor(c => c.Property).NotEmpty();
|
||||
cond.RuleFor(c => c.Operator)
|
||||
.Must(o => Enum.TryParse<SegmentConditionOperator>(o, true, out _))
|
||||
.WithMessage("Invalid operator.");
|
||||
});
|
||||
});
|
||||
if (string.IsNullOrEmpty(condition.Property))
|
||||
result.AddError(nameof(model.Rules), "'Property' must not be empty.");
|
||||
|
||||
if (!Enum.TryParse<SegmentConditionOperator>(condition.Operator, true, out _))
|
||||
result.AddError(nameof(model.Rules), "Invalid operator.");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
using FluentValidation;
|
||||
using MicCheck.Api.Common.Validation;
|
||||
|
||||
namespace MicCheck.Api.Webhooks;
|
||||
|
||||
public record CreateWebhookRequest(string Url, string? Secret, bool Enabled);
|
||||
|
||||
public class CreateWebhookRequestValidator : AbstractValidator<CreateWebhookRequest>
|
||||
public class CreateWebhookRequestValidator : IModelValidator<CreateWebhookRequest>
|
||||
{
|
||||
public CreateWebhookRequestValidator()
|
||||
public ValidationResult Validate(CreateWebhookRequest model)
|
||||
{
|
||||
RuleFor(x => x.Url).NotEmpty().MaximumLength(500).Must(u => Uri.TryCreate(u, UriKind.Absolute, out _))
|
||||
.WithMessage("Url must be a valid absolute URL.");
|
||||
var result = new ValidationResult();
|
||||
|
||||
if (string.IsNullOrEmpty(model.Url))
|
||||
result.AddError(nameof(model.Url), "'Url' must not be empty.");
|
||||
else if (model.Url.Length > 500)
|
||||
result.AddError(nameof(model.Url), "'Url' must be 500 characters or fewer.");
|
||||
else if (!Uri.TryCreate(model.Url, UriKind.Absolute, out _))
|
||||
result.AddError(nameof(model.Url), "Url must be a valid absolute URL.");
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user