From 775146ee8677c0748a2bc91d9759b73cae1e0562 Mon Sep 17 00:00:00 2001 From: Alexandr Sulimov Date: Tue, 5 Dec 2023 20:21:21 +0200 Subject: [PATCH] fix --- .editorconfig | 16 ++++ MyOffice.Data.Repositories/RepositoryBase.cs | 3 - .../app/core/interceptor/error.interceptor.ts | 7 +- .../currency/rate.currency.component.html | 18 ++-- .../currency/rate.currency.component.ts | 30 +++--- MyOffice.Web/Controllers/BaseApiController.cs | 4 +- .../Controllers/CurrencyController.cs | 15 +-- MyOffice.Web/Controllers/GeneralController.cs | 7 +- .../Attributes/DefaultFromBodyAttribute.cs | 63 ++++++++++++ .../CustomProblemDetailsFactory.cs | 95 +++++++++++++++++++ MyOffice.Web/Program.Routes.cs | 53 +++++++++++ MyOffice.Web/Program.cs | 44 +++++++-- 12 files changed, 307 insertions(+), 48 deletions(-) create mode 100644 .editorconfig create mode 100644 MyOffice.Web/Infrastructure/Attributes/DefaultFromBodyAttribute.cs create mode 100644 MyOffice.Web/Infrastructure/CustomProblemDetailsFactory.cs create mode 100644 MyOffice.Web/Program.Routes.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c6819ea --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = tabs +indent_size = 4 +insert_final_newline = false +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false \ No newline at end of file diff --git a/MyOffice.Data.Repositories/RepositoryBase.cs b/MyOffice.Data.Repositories/RepositoryBase.cs index 3226c01..c1b5caf 100644 --- a/MyOffice.Data.Repositories/RepositoryBase.cs +++ b/MyOffice.Data.Repositories/RepositoryBase.cs @@ -5,7 +5,6 @@ using DbContext; public class RepositoryBase where TEntity : class { - //TODO: Rename protected readonly AppDbContext _context; protected RepositoryBase( @@ -52,7 +51,6 @@ public class RepositoryBase where TEntity : class protected int AddBase(TEntity entity) { - //_context.Add(entity); _context.Entry(entity).State = EntityState.Added; var result = SaveChanges(); @@ -75,7 +73,6 @@ public class RepositoryBase where TEntity : class protected int UpdateBase(TEntity entity) { - //_context.Attach(entity); _context.Entry(entity).State = EntityState.Modified; var result = SaveChanges(); diff --git a/MyOffice.SPA/src/app/core/interceptor/error.interceptor.ts b/MyOffice.SPA/src/app/core/interceptor/error.interceptor.ts index 1b0c7bd..81a4537 100644 --- a/MyOffice.SPA/src/app/core/interceptor/error.interceptor.ts +++ b/MyOffice.SPA/src/app/core/interceptor/error.interceptor.ts @@ -24,14 +24,13 @@ export class ErrorInterceptor implements HttpInterceptor { this.authenticationService.logout(); location.reload(); } - let error = err.message || err.statusText; - error = !err.error ? error : err.error.message || err.error.title; + + let error = err.error || err.message || err.statusText; if (!error) { console.log('not parsed error', err); } - return throwError(error); - //return throwError(err.error || err); + return throwError(() => error); }) ); } diff --git a/MyOffice.SPA/src/app/pages/settings/currency/rate.currency.component.html b/MyOffice.SPA/src/app/pages/settings/currency/rate.currency.component.html index 2c75d88..a3b8730 100644 --- a/MyOffice.SPA/src/app/pages/settings/currency/rate.currency.component.html +++ b/MyOffice.SPA/src/app/pages/settings/currency/rate.currency.component.html @@ -41,7 +41,8 @@
Rate - + Please enter rate @@ -52,7 +53,8 @@
Quantity - + Please enter rate @@ -63,7 +65,8 @@
Rate Date - + DD/MM/YYYY @@ -85,8 +88,11 @@
-
- {{errorMessage}} +
+

{{error.title}}

+
    +
  • {{error.value}}
  • +
@@ -98,4 +104,4 @@
-
+
\ No newline at end of file diff --git a/MyOffice.SPA/src/app/pages/settings/currency/rate.currency.component.ts b/MyOffice.SPA/src/app/pages/settings/currency/rate.currency.component.ts index 03233e7..b4340be 100644 --- a/MyOffice.SPA/src/app/pages/settings/currency/rate.currency.component.ts +++ b/MyOffice.SPA/src/app/pages/settings/currency/rate.currency.component.ts @@ -21,7 +21,7 @@ import { CurrencyModel } from '../../../model/currency.model'; }) export class SettingsRateCurrencyComponent { public addForm!: UntypedFormGroup; - public errorMessage?: string; + public error?: any; constructor( private fb: UntypedFormBuilder, @@ -71,20 +71,26 @@ export class SettingsRateCurrencyComponent { } onSubmitClick() { + this.error = undefined; if (this.addForm.valid) { - this.errorMessage = undefined; this.httpClient .put(ApiRoutes.SettingsCurrency.replace(':id', this.addForm.value.id), this.addForm.value) - .subscribe(response => { - this.httpClient - .post(ApiRoutes.SettingsCurrenciesRate.replace(':id', this.addForm.value.id), this.addForm.value) - .subscribe(response => { - this.dialogRef.close({ refresh: true }); - }, error => { - this.errorMessage = error.detail; - }); - }, error => { - this.errorMessage = error.detail; + .subscribe({ + next: (response) => { + this.httpClient + .post(ApiRoutes.SettingsCurrenciesRate.replace(':id', this.addForm.value.id), this.addForm.value) + .subscribe({ + next: (response) => { + this.dialogRef.close({ refresh: true }); + }, + error: (error) => { + this.error = error; + }, + }); + }, + error: (error) => { + this.error = error; + } }); } } diff --git a/MyOffice.Web/Controllers/BaseApiController.cs b/MyOffice.Web/Controllers/BaseApiController.cs index 1858fcc..95bc5f3 100644 --- a/MyOffice.Web/Controllers/BaseApiController.cs +++ b/MyOffice.Web/Controllers/BaseApiController.cs @@ -1,11 +1,13 @@ namespace MyOffice.Web.Controllers; using System.Security.Authentication; +using Microsoft.AspNetCore.Mvc; using Core.Extensions; using IdentityModel; -using Microsoft.AspNetCore.Mvc; +using MyOffice.Web.Infrastructure.Attributes; using MyOffice.Web.Models; +[DefaultFromBody] public class BaseApiController : ControllerBase { public Guid UserId diff --git a/MyOffice.Web/Controllers/CurrencyController.cs b/MyOffice.Web/Controllers/CurrencyController.cs index d78be53..3f4fd4d 100644 --- a/MyOffice.Web/Controllers/CurrencyController.cs +++ b/MyOffice.Web/Controllers/CurrencyController.cs @@ -3,7 +3,6 @@ namespace MyOffice.Web.Controllers; using AutoMapper; using Core; using Core.Extensions; -using Data.Models.Currencies; using Infrastructure.Attributes; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization; @@ -12,8 +11,7 @@ using Services.Currency; using Services.Currency.Domain; [Authorize] -[ApiController] -[Route("api/[controller]")] +[DefaultFromBody] public class CurrencyController : BaseApiController { private readonly CurrencyService _currencyService; @@ -31,7 +29,6 @@ public class CurrencyController : BaseApiController _mapper = mapper; } - [HttpGet("~/api/settings/currencies")] public ObjectResult Get() { var list = _currencyService.GetAllWithRates(UserId); @@ -39,8 +36,7 @@ public class CurrencyController : BaseApiController return OkResponse(_mapper.Map>(list).OrderBy(x => x.Id)); } - [HttpPost("~/api/settings/currencies")] - public ObjectResult Post(CurrencyAddModel currency) + public ObjectResult Add(CurrencyAddModel currency) { var exec = _currencyService.CurrencyAdd(UserId, new CurrencyDto { @@ -71,7 +67,6 @@ public class CurrencyController : BaseApiController } } - [HttpPut("~/api/settings/currencies/{id}")] public ObjectResult Update(string id, CurrencyEditModel currency) { var exec = _currencyService.CurrencyUpdate( @@ -94,8 +89,7 @@ public class CurrencyController : BaseApiController } } - [HttpDelete("~/api/settings/currencies/{id}")] - public ObjectResult Update([AsGuid] string id) + public ObjectResult Delete([AsGuid] string id) { var exec = _currencyService.Remove(UserId, id.AsGuid()); @@ -113,8 +107,7 @@ public class CurrencyController : BaseApiController } } - [HttpPost("~/api/settings/currencies/{id}/rate")] - public ObjectResult Add(string id, CurrencyRateModel currencyRate) + public ObjectResult AddRate(string id, CurrencyRateModel currencyRate) { var exec = _currencyService.CurrencyRateAdd(UserId, id.AsGuid(), new CurrencyRateDto { diff --git a/MyOffice.Web/Controllers/GeneralController.cs b/MyOffice.Web/Controllers/GeneralController.cs index 99e5506..cd69ce1 100644 --- a/MyOffice.Web/Controllers/GeneralController.cs +++ b/MyOffice.Web/Controllers/GeneralController.cs @@ -5,10 +5,10 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization; using Models.Currency; using Services.Currency; +using MyOffice.Web.Infrastructure.Attributes; [Authorize] -[ApiController] -[Route("api/[controller]")] +[DefaultFromBody] public class GeneralController : BaseApiController { private readonly CurrencyService _currencyService; @@ -23,8 +23,7 @@ public class GeneralController : BaseApiController _mapper = mapper; } - [HttpGet("~/api/general/currencies")] - public ObjectResult Get() + public ObjectResult GetGlobalCurrencies() { var list = _currencyService.GetGlobalAll(); diff --git a/MyOffice.Web/Infrastructure/Attributes/DefaultFromBodyAttribute.cs b/MyOffice.Web/Infrastructure/Attributes/DefaultFromBodyAttribute.cs new file mode 100644 index 0000000..7f13195 --- /dev/null +++ b/MyOffice.Web/Infrastructure/Attributes/DefaultFromBodyAttribute.cs @@ -0,0 +1,63 @@ +namespace MyOffice.Web.Infrastructure.Attributes; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] +public sealed class DefaultFromBodyAttribute : Attribute +{ +} + +public class GlobalModelStateValidatorAttribute : ActionFilterAttribute +{ + private readonly ProblemDetailsFactory _problemDetailsFactory; + + public GlobalModelStateValidatorAttribute(ProblemDetailsFactory problemDetailsFactory) + { + _problemDetailsFactory = problemDetailsFactory; + } + + public override void OnActionExecuting(ActionExecutingContext context) + { + if (!context.ModelState.IsValid) + { + context.Result = new ObjectResult(_problemDetailsFactory.CreateValidationProblemDetails(context.HttpContext, context.ModelState)) + { + StatusCode = StatusCodes.Status400BadRequest + }; + } + + base.OnActionExecuting(context); + } +} +public class DefaultFromBodyBindingConvention : IActionModelConvention +{ + public void Apply(ActionModel action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + if (action.Controller.Attributes.Any(a => a is DefaultFromBodyAttribute)) + { + foreach (var parameter in action.Parameters) + { + var paramType = parameter.ParameterInfo.ParameterType; + var isSimpleType = paramType.IsPrimitive + || paramType.IsEnum + || paramType == typeof(string) + || paramType == typeof(decimal); + + if (!isSimpleType) + { + parameter.BindingInfo = parameter.BindingInfo ?? new BindingInfo(); + parameter.BindingInfo.BindingSource = BindingSource.Body; + } + } + } + } +} diff --git a/MyOffice.Web/Infrastructure/CustomProblemDetailsFactory.cs b/MyOffice.Web/Infrastructure/CustomProblemDetailsFactory.cs new file mode 100644 index 0000000..9b37b12 --- /dev/null +++ b/MyOffice.Web/Infrastructure/CustomProblemDetailsFactory.cs @@ -0,0 +1,95 @@ +namespace MyOffice.Web.Infrastructure; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Options; + +public class CustomProblemDetailsFactory : ProblemDetailsFactory +{ + private readonly ApiBehaviorOptions options; + private readonly JsonOptions jsonOptions; + + public CustomProblemDetailsFactory(IOptions options, IOptions jsonOptions) + { + this.options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + this.jsonOptions = jsonOptions?.Value ?? throw new ArgumentNullException(nameof(jsonOptions)); + } + + public override ProblemDetails CreateProblemDetails( + HttpContext httpContext, + int? statusCode = null, + string? title = null, + string? type = null, + string? detail = null, + string? instance = null) + { + statusCode ??= 500; + + var problemDetails = new ProblemDetails + { + Status = statusCode, + Title = title, + Type = type, + Detail = detail, + Instance = instance, + }; + + ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value); + + return problemDetails; + } + + public override ValidationProblemDetails CreateValidationProblemDetails( + HttpContext httpContext, + ModelStateDictionary modelStateDictionary, + int? statusCode = null, + string? title = null, + string? type = null, + string? detail = null, + string? instance = null) + { + if (modelStateDictionary == null) + { + throw new ArgumentNullException(nameof(modelStateDictionary)); + } + + statusCode ??= 400; + + var errors = modelStateDictionary + .Where(x => x.Value?.Errors.Any() == true) + .ToDictionary( + kvp => jsonOptions?.JsonSerializerOptions?.PropertyNamingPolicy?.ConvertName(kvp.Key) ?? kvp.Key, + kvp => kvp.Value!.Errors.Select(x => x.ErrorMessage).ToArray() + ); + + var problemDetails = new ValidationProblemDetails(errors) + { + Status = statusCode, + Type = type, + Detail = detail, + Instance = instance, + }; + + if (title != null) + { + // For validation problem details, don't overwrite the default title with null. + problemDetails.Title = title; + } + + ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value); + + return problemDetails; + } + + private void ApplyProblemDetailsDefaults(HttpContext httpContext, ProblemDetails problemDetails, int statusCode) + { + problemDetails.Status ??= statusCode; + + if (options.ClientErrorMapping.TryGetValue(statusCode, out var clientErrorData)) + { + problemDetails.Title ??= clientErrorData.Title; + problemDetails.Type ??= clientErrorData.Link; + } + } +} diff --git a/MyOffice.Web/Program.Routes.cs b/MyOffice.Web/Program.Routes.cs new file mode 100644 index 0000000..d887f3d --- /dev/null +++ b/MyOffice.Web/Program.Routes.cs @@ -0,0 +1,53 @@ +namespace MyOffice.Web; + +public partial class Program +{ + private static void MapRoutes(WebApplication app) + { + MapRoute(app, Routes.GlobalCurrencies, "General", "GetGlobalCurrencies", HttpMethod.Get); + + // Settings Currency + MapRoute(app, Routes.SettingsCurrencies, "Currency", "Get", HttpMethod.Get); + MapRoute(app, Routes.SettingsCurrencies, "Currency", "Add", HttpMethod.Post); + MapRoute(app, Routes.SettingsCurrency, "Currency", "Update", HttpMethod.Put); + MapRoute(app, Routes.SettingsCurrency, "Currency", "Delete", HttpMethod.Delete); + MapRoute(app, Routes.SettingsCurrencyRate, "Currency", "AddRate", HttpMethod.Post); + } + + private static void MapRoute( + WebApplication app, + string route, + string controller, + string action, + params HttpMethod[] methods + ) + { + object? constraints = null; + if (methods?.Any() == true) + { + constraints = new + { + httpMethod = new Microsoft.AspNetCore.Routing.Constraints.HttpMethodRouteConstraint(methods.Select(x => x.ToString()).ToArray()) + }; + } + + app.MapControllerRoute( + name: $"{controller}_{action}", + pattern: route, + defaults: new + { + controller, + action, + }, + constraints: constraints + ); + } +} + +public static class Routes +{ + public static readonly string GlobalCurrencies = "api/general/currencies"; + public static readonly string SettingsCurrencies = "api/settings/currencies"; + public static readonly string SettingsCurrency = "api/settings/currencies/{id}"; + public static readonly string SettingsCurrencyRate = "api/settings/currencies/{id}/rate"; +} \ No newline at end of file diff --git a/MyOffice.Web/Program.cs b/MyOffice.Web/Program.cs index 7c5b6f1..02adc61 100644 --- a/MyOffice.Web/Program.cs +++ b/MyOffice.Web/Program.cs @@ -43,8 +43,11 @@ using Services.Account; using Services.Item; using MyOffice.Services.Dashboard; using MyOffice.Services.Identity; -using MyOffice.Services.Mapper; using MyOffice.Services.Account.Domain; +using MyOffice.Web.Infrastructure.Attributes; +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; //TODO: Data.Model only Repository and Service, response <-> mapper <-> web <-> mapper <-> service <-> repository //TODO: Project management @@ -73,7 +76,7 @@ using MyOffice.Services.Account.Domain; //TODO: report income //TODO: report outcome -public class Program +public partial class Program { public static void Main(string[] args) { @@ -88,6 +91,8 @@ public class Program private static void AddServices(WebApplicationBuilder builder) { + #region Loging + builder.Services.AddLogging(logging => logging.AddSimpleConsole(options => { @@ -99,6 +104,8 @@ public class Program //File Logger builder.Logging.AddFile(builder.Configuration.GetSection("Logging")); + #endregion Loging + builder.Services.Configure(options => { //options.AddFilter("IdentityServer4", LogLevel.Warning); // or LogLevel.None to completely disable @@ -108,7 +115,8 @@ public class Program // shared configuration builder.Configuration.AddConfiguration(SharedConfiguration.CreateConfigurationContainer()); - // Database + #region Database + var databaseProvider = builder.Configuration["DatabaseProvider"]; if (databaseProvider == null) throw new NullReferenceException($"Configuration DatabaseProvider {databaseProvider}"); @@ -120,6 +128,8 @@ public class Program var connectionConfiguration = new ConnectionConfiguration(databaseProvider, connectionString); RepositoryInitializer.Initialize(builder.Services, connectionConfiguration); + #endregion Database + // Configurations builder.Services.Configure( builder.Configuration.GetSection("ExternalProviders") @@ -131,7 +141,7 @@ public class Program builder.Services.AddScoped(); - AddMapping(builder); + AddAutoMapper(builder); AddRepositories(builder); @@ -145,6 +155,8 @@ public class Program options.JsonSerializerOptions.MaxDepth = 0; options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; }); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle @@ -152,6 +164,21 @@ public class Program //builder.Services.AddSwaggerGen(); builder.Services.AddCors(); + + builder.Services.AddControllersWithViews(options => + { + options.Conventions.Add(new DefaultFromBodyBindingConvention()); + options.Filters.Add(typeof(GlobalModelStateValidatorAttribute)); + }); + + builder.Services.Configure(options => + { + }); + + builder.Services.Configure(x => { + }); + + builder.Services.AddSingleton(); } private static void InitializeGlobalSettings(WebApplicationBuilder builder) @@ -245,7 +272,7 @@ public class Program } } - private static void AddMapping(WebApplicationBuilder builder) + private static void AddAutoMapper(WebApplicationBuilder builder) { builder.Services.AddAutoMapper( c => @@ -253,7 +280,7 @@ public class Program c.AllowNullCollections = true; c.AllowNullDestinationValues = true; }, - typeof(AccountDto).Assembly, + typeof(AccountDto).Assembly, typeof(AccountViewModel).Assembly ); } @@ -272,7 +299,7 @@ public class Program builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); - + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -423,8 +450,11 @@ public class Program app.UseIdentityServer(); //app.UseAuthentication(); + //app.UseRouting(); app.UseAuthorization(); + MapRoutes(app); + app.MapControllers(); }