This commit is contained in:
2023-12-05 20:21:21 +02:00
parent 045f60e373
commit 775146ee86
12 changed files with 307 additions and 48 deletions
+16
View File
@@ -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
@@ -5,7 +5,6 @@ using DbContext;
public class RepositoryBase<TEntity> where TEntity : class public class RepositoryBase<TEntity> where TEntity : class
{ {
//TODO: Rename
protected readonly AppDbContext _context; protected readonly AppDbContext _context;
protected RepositoryBase( protected RepositoryBase(
@@ -52,7 +51,6 @@ public class RepositoryBase<TEntity> where TEntity : class
protected int AddBase(TEntity entity) protected int AddBase(TEntity entity)
{ {
//_context.Add(entity);
_context.Entry(entity).State = EntityState.Added; _context.Entry(entity).State = EntityState.Added;
var result = SaveChanges(); var result = SaveChanges();
@@ -75,7 +73,6 @@ public class RepositoryBase<TEntity> where TEntity : class
protected int UpdateBase(TEntity entity) protected int UpdateBase(TEntity entity)
{ {
//_context.Attach(entity);
_context.Entry(entity).State = EntityState.Modified; _context.Entry(entity).State = EntityState.Modified;
var result = SaveChanges(); var result = SaveChanges();
@@ -24,14 +24,13 @@ export class ErrorInterceptor implements HttpInterceptor {
this.authenticationService.logout(); this.authenticationService.logout();
location.reload(); 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) { if (!error) {
console.log('not parsed error', err); console.log('not parsed error', err);
} }
return throwError(error); return throwError(() => error);
//return throwError(err.error || err);
}) })
); );
} }
@@ -41,7 +41,8 @@
<div class="text-inside"> <div class="text-inside">
<mat-form-field class="example-full-width" appearance="fill"> <mat-form-field class="example-full-width" appearance="fill">
<mat-label>Rate</mat-label> <mat-label>Rate</mat-label>
<input matInput [value]="currency.rate | number:'0.2-2'" formControlName="rate" currencyMask [options]="{ prefix: '', precision: 4, inputMode: 1 }" required> <input matInput [value]="currency.rate | number:'0.2-2'" formControlName="rate" currencyMask
[options]="{ prefix: '', precision: 4, inputMode: 1 }" required>
<mat-error *ngIf="addForm.controls?.['rate']?.hasError('required')"> <mat-error *ngIf="addForm.controls?.['rate']?.hasError('required')">
Please enter rate Please enter rate
</mat-error> </mat-error>
@@ -52,7 +53,8 @@
<div class="text-inside"> <div class="text-inside">
<mat-form-field class="example-full-width" appearance="fill"> <mat-form-field class="example-full-width" appearance="fill">
<mat-label>Quantity</mat-label> <mat-label>Quantity</mat-label>
<input matInput [value]="currency.rate | number:'0.0-0'" formControlName="quantity" currencyMask [options]="{ prefix: '', precision: 0 }" required> <input matInput [value]="currency.rate | number:'0.0-0'" formControlName="quantity"
currencyMask [options]="{ prefix: '', precision: 0 }" required>
<mat-error *ngIf="addForm.controls?.['rate']?.hasError('required')"> <mat-error *ngIf="addForm.controls?.['rate']?.hasError('required')">
Please enter rate Please enter rate
</mat-error> </mat-error>
@@ -63,7 +65,8 @@
<div class="text-inside"> <div class="text-inside">
<mat-form-field class="example-full-width"> <mat-form-field class="example-full-width">
<mat-label>Rate Date</mat-label> <mat-label>Rate Date</mat-label>
<input matInput [matDatepicker]="picker3" (focus)="picker3.open()" value={{currency.rateDate}} formControlName="rateDate" required> <input matInput [matDatepicker]="picker3" (focus)="picker3.open()"
value={{currency.rateDate}} formControlName="rateDate" required>
<mat-hint>DD/MM/YYYY</mat-hint> <mat-hint>DD/MM/YYYY</mat-hint>
<mat-datepicker-toggle matSuffix [for]="picker3"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="picker3"></mat-datepicker-toggle>
<mat-datepicker #picker3></mat-datepicker> <mat-datepicker #picker3></mat-datepicker>
@@ -85,8 +88,11 @@
</div> </div>
<mat-dialog-actions class="mat-dialog-actions"> <mat-dialog-actions class="mat-dialog-actions">
<div class="mat-dialog-left"> <div class="mat-dialog-left">
<div class="alert alert-danger mat-dialog-error" *ngIf="errorMessage"> <div class="alert alert-danger mat-dialog-error" *ngIf="error">
{{errorMessage}} <p *ngIf="error.title">{{error.title}}</p>
<ul *ngIf="error.errors">
<li *ngFor="let error of error.errors | keyvalue">{{error.value}}</li>
</ul>
</div> </div>
</div> </div>
<div class="mat-dialog-right"> <div class="mat-dialog-right">
@@ -21,7 +21,7 @@ import { CurrencyModel } from '../../../model/currency.model';
}) })
export class SettingsRateCurrencyComponent { export class SettingsRateCurrencyComponent {
public addForm!: UntypedFormGroup; public addForm!: UntypedFormGroup;
public errorMessage?: string; public error?: any;
constructor( constructor(
private fb: UntypedFormBuilder, private fb: UntypedFormBuilder,
@@ -71,20 +71,26 @@ export class SettingsRateCurrencyComponent {
} }
onSubmitClick() { onSubmitClick() {
this.error = undefined;
if (this.addForm.valid) { if (this.addForm.valid) {
this.errorMessage = undefined;
this.httpClient this.httpClient
.put<CurrencyModel>(ApiRoutes.SettingsCurrency.replace(':id', this.addForm.value.id), this.addForm.value) .put<CurrencyModel>(ApiRoutes.SettingsCurrency.replace(':id', this.addForm.value.id), this.addForm.value)
.subscribe(response => { .subscribe({
this.httpClient next: (response) => {
.post<CurrencyModel[]>(ApiRoutes.SettingsCurrenciesRate.replace(':id', this.addForm.value.id), this.addForm.value) this.httpClient
.subscribe(response => { .post<CurrencyModel[]>(ApiRoutes.SettingsCurrenciesRate.replace(':id', this.addForm.value.id), this.addForm.value)
this.dialogRef.close({ refresh: true }); .subscribe({
}, error => { next: (response) => {
this.errorMessage = error.detail; this.dialogRef.close({ refresh: true });
}); },
}, error => { error: (error) => {
this.errorMessage = error.detail; this.error = error;
},
});
},
error: (error) => {
this.error = error;
}
}); });
} }
} }
@@ -1,11 +1,13 @@
namespace MyOffice.Web.Controllers; namespace MyOffice.Web.Controllers;
using System.Security.Authentication; using System.Security.Authentication;
using Microsoft.AspNetCore.Mvc;
using Core.Extensions; using Core.Extensions;
using IdentityModel; using IdentityModel;
using Microsoft.AspNetCore.Mvc; using MyOffice.Web.Infrastructure.Attributes;
using MyOffice.Web.Models; using MyOffice.Web.Models;
[DefaultFromBody]
public class BaseApiController : ControllerBase public class BaseApiController : ControllerBase
{ {
public Guid UserId public Guid UserId
+4 -11
View File
@@ -3,7 +3,6 @@ namespace MyOffice.Web.Controllers;
using AutoMapper; using AutoMapper;
using Core; using Core;
using Core.Extensions; using Core.Extensions;
using Data.Models.Currencies;
using Infrastructure.Attributes; using Infrastructure.Attributes;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@@ -12,8 +11,7 @@ using Services.Currency;
using Services.Currency.Domain; using Services.Currency.Domain;
[Authorize] [Authorize]
[ApiController] [DefaultFromBody]
[Route("api/[controller]")]
public class CurrencyController : BaseApiController public class CurrencyController : BaseApiController
{ {
private readonly CurrencyService _currencyService; private readonly CurrencyService _currencyService;
@@ -31,7 +29,6 @@ public class CurrencyController : BaseApiController
_mapper = mapper; _mapper = mapper;
} }
[HttpGet("~/api/settings/currencies")]
public ObjectResult Get() public ObjectResult Get()
{ {
var list = _currencyService.GetAllWithRates(UserId); var list = _currencyService.GetAllWithRates(UserId);
@@ -39,8 +36,7 @@ public class CurrencyController : BaseApiController
return OkResponse(_mapper.Map<List<CurrencyViewModel>>(list).OrderBy(x => x.Id)); return OkResponse(_mapper.Map<List<CurrencyViewModel>>(list).OrderBy(x => x.Id));
} }
[HttpPost("~/api/settings/currencies")] public ObjectResult Add(CurrencyAddModel currency)
public ObjectResult Post(CurrencyAddModel currency)
{ {
var exec = _currencyService.CurrencyAdd(UserId, new CurrencyDto 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) public ObjectResult Update(string id, CurrencyEditModel currency)
{ {
var exec = _currencyService.CurrencyUpdate( var exec = _currencyService.CurrencyUpdate(
@@ -94,8 +89,7 @@ public class CurrencyController : BaseApiController
} }
} }
[HttpDelete("~/api/settings/currencies/{id}")] public ObjectResult Delete([AsGuid] string id)
public ObjectResult Update([AsGuid] string id)
{ {
var exec = _currencyService.Remove(UserId, id.AsGuid()); var exec = _currencyService.Remove(UserId, id.AsGuid());
@@ -113,8 +107,7 @@ public class CurrencyController : BaseApiController
} }
} }
[HttpPost("~/api/settings/currencies/{id}/rate")] public ObjectResult AddRate(string id, CurrencyRateModel currencyRate)
public ObjectResult Add(string id, CurrencyRateModel currencyRate)
{ {
var exec = _currencyService.CurrencyRateAdd(UserId, id.AsGuid(), new CurrencyRateDto var exec = _currencyService.CurrencyRateAdd(UserId, id.AsGuid(), new CurrencyRateDto
{ {
@@ -5,10 +5,10 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Models.Currency; using Models.Currency;
using Services.Currency; using Services.Currency;
using MyOffice.Web.Infrastructure.Attributes;
[Authorize] [Authorize]
[ApiController] [DefaultFromBody]
[Route("api/[controller]")]
public class GeneralController : BaseApiController public class GeneralController : BaseApiController
{ {
private readonly CurrencyService _currencyService; private readonly CurrencyService _currencyService;
@@ -23,8 +23,7 @@ public class GeneralController : BaseApiController
_mapper = mapper; _mapper = mapper;
} }
[HttpGet("~/api/general/currencies")] public ObjectResult GetGlobalCurrencies()
public ObjectResult Get()
{ {
var list = _currencyService.GetGlobalAll(); var list = _currencyService.GetGlobalAll();
@@ -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;
}
}
}
}
}
@@ -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<ApiBehaviorOptions> options, IOptions<JsonOptions> 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;
}
}
}
+53
View File
@@ -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";
}
+35 -5
View File
@@ -43,8 +43,11 @@ using Services.Account;
using Services.Item; using Services.Item;
using MyOffice.Services.Dashboard; using MyOffice.Services.Dashboard;
using MyOffice.Services.Identity; using MyOffice.Services.Identity;
using MyOffice.Services.Mapper;
using MyOffice.Services.Account.Domain; 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: Data.Model only Repository and Service, response <-> mapper <-> web <-> mapper <-> service <-> repository
//TODO: Project management //TODO: Project management
@@ -73,7 +76,7 @@ using MyOffice.Services.Account.Domain;
//TODO: report income //TODO: report income
//TODO: report outcome //TODO: report outcome
public class Program public partial class Program
{ {
public static void Main(string[] args) public static void Main(string[] args)
{ {
@@ -88,6 +91,8 @@ public class Program
private static void AddServices(WebApplicationBuilder builder) private static void AddServices(WebApplicationBuilder builder)
{ {
#region Loging
builder.Services.AddLogging(logging => builder.Services.AddLogging(logging =>
logging.AddSimpleConsole(options => logging.AddSimpleConsole(options =>
{ {
@@ -99,6 +104,8 @@ public class Program
//File Logger //File Logger
builder.Logging.AddFile(builder.Configuration.GetSection("Logging")); builder.Logging.AddFile(builder.Configuration.GetSection("Logging"));
#endregion Loging
builder.Services.Configure<LoggerFilterOptions>(options => builder.Services.Configure<LoggerFilterOptions>(options =>
{ {
//options.AddFilter("IdentityServer4", LogLevel.Warning); // or LogLevel.None to completely disable //options.AddFilter("IdentityServer4", LogLevel.Warning); // or LogLevel.None to completely disable
@@ -108,7 +115,8 @@ public class Program
// shared configuration // shared configuration
builder.Configuration.AddConfiguration(SharedConfiguration.CreateConfigurationContainer()); builder.Configuration.AddConfiguration(SharedConfiguration.CreateConfigurationContainer());
// Database #region Database
var databaseProvider = builder.Configuration["DatabaseProvider"]; var databaseProvider = builder.Configuration["DatabaseProvider"];
if (databaseProvider == null) if (databaseProvider == null)
throw new NullReferenceException($"Configuration DatabaseProvider {databaseProvider}"); throw new NullReferenceException($"Configuration DatabaseProvider {databaseProvider}");
@@ -120,6 +128,8 @@ public class Program
var connectionConfiguration = new ConnectionConfiguration(databaseProvider, connectionString); var connectionConfiguration = new ConnectionConfiguration(databaseProvider, connectionString);
RepositoryInitializer.Initialize(builder.Services, connectionConfiguration); RepositoryInitializer.Initialize(builder.Services, connectionConfiguration);
#endregion Database
// Configurations // Configurations
builder.Services.Configure<ExternalProvidersConfig>( builder.Services.Configure<ExternalProvidersConfig>(
builder.Configuration.GetSection("ExternalProviders") builder.Configuration.GetSection("ExternalProviders")
@@ -131,7 +141,7 @@ public class Program
builder.Services.AddScoped<IContextProvider, ContextProvider>(); builder.Services.AddScoped<IContextProvider, ContextProvider>();
AddMapping(builder); AddAutoMapper(builder);
AddRepositories(builder); AddRepositories(builder);
@@ -145,6 +155,8 @@ public class Program
options.JsonSerializerOptions.MaxDepth = 0; options.JsonSerializerOptions.MaxDepth = 0;
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; 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 // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
@@ -152,6 +164,21 @@ public class Program
//builder.Services.AddSwaggerGen(); //builder.Services.AddSwaggerGen();
builder.Services.AddCors(); builder.Services.AddCors();
builder.Services.AddControllersWithViews(options =>
{
options.Conventions.Add(new DefaultFromBodyBindingConvention());
options.Filters.Add(typeof(GlobalModelStateValidatorAttribute));
});
builder.Services.Configure<MvcOptions>(options =>
{
});
builder.Services.Configure<ApiBehaviorOptions>(x => {
});
builder.Services.AddSingleton<ProblemDetailsFactory, CustomProblemDetailsFactory>();
} }
private static void InitializeGlobalSettings(WebApplicationBuilder builder) 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( builder.Services.AddAutoMapper(
c => c =>
@@ -423,8 +450,11 @@ public class Program
app.UseIdentityServer(); app.UseIdentityServer();
//app.UseAuthentication(); //app.UseAuthentication();
//app.UseRouting();
app.UseAuthorization(); app.UseAuthorization();
MapRoutes(app);
app.MapControllers(); app.MapControllers();
} }