diff --git a/DocuMed.Api/AppSettings/Production/appsettings.Production.json b/DocuMed.Api/AppSettings/Production/appsettings.Production.json new file mode 100644 index 0000000..d386c77 --- /dev/null +++ b/DocuMed.Api/AppSettings/Production/appsettings.Production.json @@ -0,0 +1,56 @@ +{ + "ConnectionStrings": { + "Postgres": "User ID=postgres;Password=root;Host=localhost;Port=5432;Database=iGarsonDB;", + "PostgresServer": "Host=pg-0,pg-1;Username=igarsonAgent;Password=xHTpBf4wC+bBeNg2pL6Ga7VEWKFJx7VPEUpqxwPFfOc2YYTVwFQuHfsiqoVeT9+6;Database=BrizcoDB;Load Balance Hosts=true;Target Session Attributes=primary;Application Name=iGLS" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "None", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.AspNetCore.SignalR": "Debug", + "Microsoft.AspNetCore.Http.Connections": "Debug" + } + }, + "SiteSettings": { + "BaseUrl": "http://localhost:32769", + "UserSetting": { + "Username": "Root", + "Email": "info@brizco.io", + "Password": "root1234", + "Phone": "09211111111", + "RoleName": "RootAdmin", + "FirstName": "همه کاره", + "LastName": "سیستم" + }, + "JwtSettings": { + "SecretKey": "pg8mt74/bk5yx2mr23Zvsu/81Z2czAycEo9ewcm34AndD8SFDXGqBiYv_YaHosseinYaAli_ABOOOOOOOOOLFAZL_BIMEH_JAD_NASABE_YA_GHARIBAL_GHORABA_@@@@_06/0CZWyAqy2H6Xpjp0npg8mt74/bk5yx2mr23Zvsu/81Z2czAycEo9ewcm34AndD8SFDXGqBiYvACLB0dED9vjy+h5sK1BnB30=", + "Issuer": "Brizco", + "Audience": "Brizco", + "ExpireAddDay": "15" + } + }, + "IpRateLimiting": { + "EnableEndpointRateLimiting": false, + "StackBlockedRequests": false, + "RealIpHeader": "X-Real-IP", + "ClientIdHeader": "X-ClientId", + "HttpStatusCode": 429, + "IpWhitelist": [ "127.0.0.1", "::1/10", "192.168.0.0/24" ], + "EndpointWhitelist": [ "get:/api/license", "*:/api/status" ], + "ClientWhitelist": [ "dev-id-1", "dev-id-2" ], + "GeneralRules": [ + { + "Endpoint": "*", + "Period": "1m", + "Limit": 60 + }, + { + "Endpoint": "*", + "Period": "15m", + "Limit": 250 + } + ] + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/DocuMed.Api/appsettings.Development.json b/DocuMed.Api/AppSettings/Production/appsettings.json similarity index 100% rename from DocuMed.Api/appsettings.Development.json rename to DocuMed.Api/AppSettings/Production/appsettings.json diff --git a/DocuMed.Api/AppSettings/appsettings.Development.json b/DocuMed.Api/AppSettings/appsettings.Development.json new file mode 100644 index 0000000..980f464 --- /dev/null +++ b/DocuMed.Api/AppSettings/appsettings.Development.json @@ -0,0 +1,56 @@ +{ + "ConnectionStrings": { + "Postgres": "User ID=postgres;Password=root;Host=localhost;Port=5432;Database=iGarsonDB;", + "PostgresServer": "Host=pg-0,pg-1;Username=igarsonAgent;Password=xHTpBf4wC+bBeNg2pL6Ga7VEWKFJx7VPEUpqxwPFfOc2YYTVwFQuHfsiqoVeT9+6;Database=BrizcoDB;Load Balance Hosts=true;Target Session Attributes=primary;Application Name=iGLS", + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "None", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.AspNetCore.SignalR": "Debug", + "Microsoft.AspNetCore.Http.Connections": "Debug" + } + }, + "SiteSettings": { + "BaseUrl": "http://localhost:32769", + "UserSetting": { + "Username": "root", + "Email": "info@documed.ir", + "Password": "i1nLGN86rU/HQgzC", + "Phone": "09211111111", + "RoleName": "RootAdmin", + "FirstName": "همه کاره", + "LastName": "سیستم" + }, + "JwtSettings": { + "SecretKey": "YaHosseinYaAli_pg8mt74/bk5yx2mr23Zvsu/81Z2czAycEo9ewcm34AndD8SFDXGqBiYv_ABOOOOOOOOOLFAZL_BIMEH_JAD_NASABE_pg8mt74/bk5yx2mr23Zvsu/81Z2czAycEo9ewcm34AndD8SFDXGqBiYv_YA_GHARIBAL_GHORABA_@@@@_06/0CZWyAqy2H6Xpjp0npg8mt74/bk5yx2mr23Zvsu/81Z2czAycEo9ewcm34AndD8SFDXGqBiYvACLB0dED9vjy+h5sK1BnB30=", + "Issuer": "Brizco", + "Audience": "Brizco", + "ExpireAddDay": "15" + } + }, + "IpRateLimiting": { + "EnableEndpointRateLimiting": false, + "StackBlockedRequests": false, + "RealIpHeader": "X-Real-IP", + "ClientIdHeader": "X-ClientId", + "HttpStatusCode": 429, + "IpWhitelist": [ "127.0.0.1", "::1/10", "192.168.0.0/24" ], + "EndpointWhitelist": [ "get:/api/license", "*:/api/status" ], + "ClientWhitelist": [ "dev-id-1", "dev-id-2" ], + "GeneralRules": [ + { + "Endpoint": "*", + "Period": "1m", + "Limit": 60 + }, + { + "Endpoint": "*", + "Period": "15m", + "Limit": 250 + } + ] + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/DocuMed.Api/appsettings.json b/DocuMed.Api/AppSettings/appsettings.json similarity index 100% rename from DocuMed.Api/appsettings.json rename to DocuMed.Api/AppSettings/appsettings.json diff --git a/DocuMed.Api/DocuMed.Api.csproj b/DocuMed.Api/DocuMed.Api.csproj index 3102210..586307d 100644 --- a/DocuMed.Api/DocuMed.Api.csproj +++ b/DocuMed.Api/DocuMed.Api.csproj @@ -53,4 +53,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DocuMed.Api/Program.cs b/DocuMed.Api/Program.cs index df2434c..b6665e2 100644 --- a/DocuMed.Api/Program.cs +++ b/DocuMed.Api/Program.cs @@ -1,23 +1,96 @@ +using DocuMed.Api.WebFramework.MiddleWares; + var builder = WebApplication.CreateBuilder(args); +builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory()); +builder.Host.UseSerilog(); +LoggerConfig.ConfigureSerilog(); +string env = builder.Environment.IsDevelopment() == true ? "Development" : "Production"; +builder.Host.UseContentRoot(Directory.GetCurrentDirectory()); + +if (builder.Environment.IsDevelopment()) + builder.Configuration + .AddJsonFile($"AppSettings/appsettings.json") + .AddJsonFile($"AppSettings/appsettings.{env}.json"); + +if (builder.Environment.IsProduction()) + builder.Configuration + .AddJsonFile($"AppSettings/Production/appsettings.{env}.json"); +var configuration = builder.Configuration; +var siteSetting = configuration.GetSection(nameof(SiteSettings)).Get(); +builder.Services.Configure(configuration.GetSection(nameof(SiteSettings))); // Add services to the container. + builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddCustomSwagger(siteSetting.BaseUrl); +builder.Services.AddCustomApiVersioning(); +builder.Services.AddCustomController(); +builder.Services.AddControllers(); +builder.Services.AddCustomResponseCompression(); +builder.Services.AddCustomMvc(); +builder.Services.AddCustomAuthorization(); +builder.Services.AddJwtCustomAuthentication(siteSetting.JwtSettings); +builder.Services.AddCustomIdentity(); +builder.Services.AddCustomDbContext(configuration); +builder.Services.AddCarter(); + +builder.Host.ConfigureContainer(builder => +{ + + var assembly = typeof(CoreConfig).Assembly; + builder + .RegisterAssemblyTypes(assembly) + .AssignableTo() + .AsImplementedInterfaces() + .InstancePerLifetimeScope(); + + var assemblyB = typeof(InfrastructureConfig).Assembly; + builder.RegisterAssemblyTypes(assemblyB) + .AssignableTo() + .AsImplementedInterfaces() + .InstancePerLifetimeScope(); + + var assemblyC = typeof(RepositoryConfig).Assembly; + builder.RegisterAssemblyTypes(assemblyC) + .AssignableTo() + .AsImplementedInterfaces() + .InstancePerLifetimeScope(); + + + var assemblyD = typeof(Program).Assembly; + builder.RegisterAssemblyTypes(assemblyD) + .AssignableTo() + .AsImplementedInterfaces() + .InstancePerLifetimeScope(); +}); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { - app.UseSwagger(); - app.UseSwaggerUI(); + app.UseCustomSwagger(siteSetting.BaseUrl); + //app.UseSwagger(); + //app.UseSwaggerUI(); } app.UseAuthorization(); +app.UseAuthentication(); +app.UseCors(x => x + .SetIsOriginAllowed(origin => true) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials()); + +app.UseExceptionHandlerMiddleware(); + +app.MapCarter(); +app.UseStaticFiles(); +await app.InitialDb(); app.MapControllers(); app.Run(); diff --git a/DocuMed.Api/Services/CurrentUserService.cs b/DocuMed.Api/Services/CurrentUserService.cs new file mode 100644 index 0000000..7ca8c8e --- /dev/null +++ b/DocuMed.Api/Services/CurrentUserService.cs @@ -0,0 +1,15 @@ +namespace DocuMed.Api.Services; + +public class CurrentUserService : ICurrentUserService +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + public CurrentUserService(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public string? UserId => _httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier); + public string? RoleName => _httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.Role); + public string? UserName => _httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.Name); +} \ No newline at end of file diff --git a/DocuMed.Api/WebFramework/Bases/ApiResultFilterAttribute.cs b/DocuMed.Api/WebFramework/Bases/ApiResultFilterAttribute.cs new file mode 100644 index 0000000..52ae250 --- /dev/null +++ b/DocuMed.Api/WebFramework/Bases/ApiResultFilterAttribute.cs @@ -0,0 +1,74 @@ +namespace DocuMed.Api.WebFramework.Bases; + +public class ApiResultFactory +{ + +} +public class ApiResultFilterAttribute : ActionFilterAttribute +{ + public override void OnResultExecuting(ResultExecutingContext context) + { + if (context.Result is OkObjectResult okObjectResult) + { + var apiResult = new ApiResult(true, ApiResultStatusCode.Success, okObjectResult.Value); + context.Result = new JsonResult(apiResult) { StatusCode = okObjectResult.StatusCode }; + } + else if (context.Result is OkResult okResult) + { + var apiResult = new ApiResult(true, ApiResultStatusCode.Success); + context.Result = new JsonResult(apiResult) { StatusCode = okResult.StatusCode }; + } + else if (context.Result is BadRequestResult badRequestResult) + { + var apiResult = new ApiResult(false, ApiResultStatusCode.BadRequest); + context.Result = new JsonResult(apiResult) { StatusCode = badRequestResult.StatusCode }; + } + else if (context.Result is BadRequestObjectResult badRequestObjectResult) + { + var message = badRequestObjectResult.Value.ToString(); + + if (badRequestObjectResult.Value is SerializableError errors) + { + var errorMessages = errors.SelectMany(p => (string[])p.Value).Distinct(); + message = string.Join(" | ", errorMessages); + } + + if (badRequestObjectResult.Value is ValidationProblemDetails problemDetails) + { + var errorMessages = problemDetails.Errors.Values.SelectMany(v => v); + message = string.Join(" | ", errorMessages); + } + + var apiResult = new ApiResult(false, ApiResultStatusCode.BadRequest, message); + context.Result = new JsonResult(apiResult) { StatusCode = badRequestObjectResult.StatusCode }; + } + else if (context.Result is ContentResult contentResult) + { + var apiResult = new ApiResult(true, ApiResultStatusCode.Success, contentResult.Content); + context.Result = new JsonResult(apiResult) { StatusCode = contentResult.StatusCode }; + } + else if (context.Result is NotFoundResult notFoundResult) + { + var apiResult = new ApiResult(false, ApiResultStatusCode.NotFound); + context.Result = new JsonResult(apiResult) { StatusCode = notFoundResult.StatusCode }; + } + else if (context.Result is NotFoundObjectResult notFoundObjectResult) + { + var apiResult = new ApiResult(false, ApiResultStatusCode.NotFound, notFoundObjectResult.Value); + context.Result = new JsonResult(apiResult) { StatusCode = notFoundObjectResult.StatusCode }; + } + else if (context.Result is ObjectResult objectResult && objectResult.StatusCode == null + && !(objectResult.Value is ApiResult)) + { + var apiResult = new ApiResult(true, ApiResultStatusCode.Success, objectResult.Value); + context.Result = new JsonResult(apiResult) { StatusCode = objectResult.StatusCode }; + } + else if (context.Result is ObjectResult objectResultBad && objectResultBad.Value is ApiResult) + { + var apiResult = objectResultBad.Value as ApiResult; + context.Result = new JsonResult(apiResult) { StatusCode = objectResultBad.StatusCode }; + } + + base.OnResultExecuting(context); + } +} \ No newline at end of file diff --git a/DocuMed.Api/WebFramework/Bases/ClaimRequirement.cs b/DocuMed.Api/WebFramework/Bases/ClaimRequirement.cs new file mode 100644 index 0000000..a42c6b8 --- /dev/null +++ b/DocuMed.Api/WebFramework/Bases/ClaimRequirement.cs @@ -0,0 +1,33 @@ +using System.Net; +using Microsoft.AspNetCore.Authorization; + +namespace DocuMed.Api.WebFramework.Bases; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] +public class ClaimRequirement : AuthorizeAttribute, IAuthorizationFilter +{ + private readonly string _claimsType; + private readonly string _claimsValue; + + public ClaimRequirement(string type,string value) + { + type = value; + } + + public void OnAuthorization(AuthorizationFilterContext context) + { + var user = context.HttpContext.User; + var permissions = user.Claims?.Where(c => c.Type == _claimsType)?.ToList(); + if (permissions == null) + { + context.Result = new StatusCodeResult((int)HttpStatusCode.Forbidden); + } + else + { + bool isAccepted = permissions.FirstOrDefault(p => p.Value == _claimsValue) != null; + if (!isAccepted) + context.Result = new StatusCodeResult((int)HttpStatusCode.Forbidden); + } + } +} + diff --git a/DocuMed.Api/WebFramework/Bases/CrudController.cs b/DocuMed.Api/WebFramework/Bases/CrudController.cs new file mode 100644 index 0000000..eee66f9 --- /dev/null +++ b/DocuMed.Api/WebFramework/Bases/CrudController.cs @@ -0,0 +1,224 @@ +using System.Linq.Expressions; +using Mapster; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.EntityFrameworkCore; + +namespace DocuMed.Api.WebFramework.Bases; + + +public class CrudEndpoint where TEntity : ApiEntity, new() +{ + private readonly string _endpointName; + + public CrudEndpoint(string endpointName) + { + _endpointName = endpointName; + } + + public virtual void AddRoutes(IEndpointRouteBuilder app) + { + var group = app.NewVersionedApi(_endpointName).MapGroup($"api/{_endpointName}"); + + group.MapGet("", GetAllAsync) + .WithDisplayName("GetAll") + .HasApiVersion(1.0); + + group.MapGet("{id}", GetAsync) + .WithName("GetOne") + .HasApiVersion(1.0); + + group.MapPost("", Post) + .HasApiVersion(1.0); + + group.MapPut("", Put) + .HasApiVersion(1.0); + + group.MapDelete("", Delete) + .HasApiVersion(1.0); + } + + // GET:Get All Entity + public virtual async Task GetAllAsync(ISender sender , CancellationToken cancellationToken) + { + var res = sender.Send(Activator.CreateInstance(),cancellationToken); + return TypedResults.Ok(res); + } + + // GET:Get An Entity By Id + public async Task GetAsync(Guid id, ISender sender, CancellationToken cancellationToken) + => TypedResults.Ok(sender.Send(Activator.CreateInstance())); + + // POST:Create Entity + public virtual async Task Post([FromBody] TCreateCommand ent , ISender mediator , CancellationToken cancellationToken) + { + return TypedResults.Ok(await mediator.Send(ent, cancellationToken)); + } + + // PUT:Update Entity + public virtual async Task Put([FromBody] TEntity ent , IRepositoryWrapper _repositoryWrapper, CancellationToken cancellationToken) + { + _repositoryWrapper.SetRepository().Update(ent); + await _repositoryWrapper.SaveChangesAsync(cancellationToken); + return TypedResults.Ok(); + } + + // DELETE:Delete Entity + public virtual async Task Delete(Guid id,IRepositoryWrapper _repositoryWrapper, CancellationToken cancellationToken) + { + var ent = await _repositoryWrapper.SetRepository().GetByIdAsync(cancellationToken, id); + _repositoryWrapper.SetRepository().Delete(ent); + await _repositoryWrapper.SaveChangesAsync(cancellationToken); + return TypedResults.Ok(); + } +} + +[ApiController] +//[AllowAnonymous] +[ApiResultFilter] +[Route("api/v{version:apiVersion}/[controller]")] // api/v1/[controller] +public class BaseController : ControllerBase +{ + //public UserRepository UserRepository { get; set; } => property injection + public bool UserIsAutheticated => HttpContext.User.Identity.IsAuthenticated; +} + +[Authorize(AuthenticationSchemes = "Bearer")] +public class CrudController : BaseController + where TDto : BaseDto, new() + where TEntity : ApiEntity, new() +{ + protected readonly IRepositoryWrapper _repositoryWrapper; + + public CrudController(IRepositoryWrapper repositoryWrapper) + { + _repositoryWrapper = repositoryWrapper; + } + + // GET:Get All Entity + [HttpGet] + public virtual async Task GetAllAsync(CancellationToken cancellationToken) + { + var projectTo = typeof(TDto).BaseType?.GetProperty("ProjectToDto")?.GetValue(null, null); + if (projectTo != null) + { + var exprss = projectTo as Expression>; + var entites = await _repositoryWrapper + .SetRepository() + .TableNoTracking + .Select(exprss) + .ToListAsync(cancellationToken); + return Ok(entites); + } + + throw new BaseApiException("ProjectTo Not Found"); + } + + // GET:Get An Entity By Id + [HttpGet("{id}")] + public virtual async Task GetAsync(string id, CancellationToken cancellationToken) + { + var ent = await _repositoryWrapper.SetRepository().GetByIdAsync(cancellationToken, id); + var dto = ent.Adapt(); + return Ok(dto); + } + + // POST:Add New Entity + [HttpPost] + public virtual async Task PostOrginal([FromBody] TEntity ent, CancellationToken cancellationToken) + { + _repositoryWrapper.SetRepository().Add(ent); + await _repositoryWrapper.SaveChangesAsync(cancellationToken); + return Ok(ent); + } + + // POST:Add New Entity By Dto + [HttpPost("Dto")] + public async Task PostDto([FromBody] TDto dto, CancellationToken cancellationToken) + { + _repositoryWrapper + .SetRepository() + .Add(dto.ToEntity()); + await _repositoryWrapper.SaveChangesAsync(cancellationToken); + return Ok(); + } + + // PUT:Update Entity + [HttpPut] + public virtual async Task Put([FromBody] TEntity ent, CancellationToken cancellationToken) + { + _repositoryWrapper + .SetRepository() + .Update(ent); + await _repositoryWrapper.SaveChangesAsync(cancellationToken); + return Ok(); + } + + // DELETE:Delete Entity + [HttpDelete] + [Route("{id:int}")] + public virtual async Task Delete(int id, CancellationToken cancellationToken) + { + var ent = await _repositoryWrapper + .SetRepository() + .GetByIdAsync(cancellationToken, id); + _repositoryWrapper.SetRepository().Delete(ent); + await _repositoryWrapper.SaveChangesAsync(cancellationToken); + return Ok(); + } +} + +[Authorize(AuthenticationSchemes = "Bearer")] +public class CrudController : BaseController + where TEntity : ApiEntity, new() +{ + protected readonly IRepositoryWrapper _repositoryWrapper; + + public CrudController(IRepositoryWrapper repositoryWrapper) + { + _repositoryWrapper = repositoryWrapper; + } + + // GET:Get All Entity + [HttpGet] + public virtual async Task GetAllAsync() + { + return Ok(await _repositoryWrapper.SetRepository().TableNoTracking.ToListAsync()); + } + + // GET:Get An Entity By Id + [HttpGet("{id}")] + public async Task GetAsync(int id, CancellationToken cancellationToken) + { + var ent = await _repositoryWrapper.SetRepository().GetByIdAsync(cancellationToken, id); + return Ok(ent); + } + + // POST:Add New Entity + [HttpPost] + public virtual async Task Post([FromBody] TEntity ent, CancellationToken cancellationToken) + { + _repositoryWrapper.SetRepository().Add(ent); + await _repositoryWrapper.SaveChangesAsync(cancellationToken); + return Ok(ent); + } + + // PUT:Update Entity + [HttpPut] + public virtual async Task Put([FromBody] TEntity ent, CancellationToken cancellationToken) + { + _repositoryWrapper.SetRepository().Update(ent); + await _repositoryWrapper.SaveChangesAsync(cancellationToken); + return Ok(); + } + + // DELETE:Delete Entity + [HttpDelete("{id}")] + public virtual async Task Delete(int id, CancellationToken cancellationToken) + { + var ent = await _repositoryWrapper.SetRepository().GetByIdAsync(cancellationToken, id); + _repositoryWrapper.SetRepository().Delete(ent); + await _repositoryWrapper.SaveChangesAsync(cancellationToken); + return Ok(); + } +} \ No newline at end of file diff --git a/DocuMed.Api/WebFramework/Configurations/ConfigureJwtBearerOptions.cs b/DocuMed.Api/WebFramework/Configurations/ConfigureJwtBearerOptions.cs new file mode 100644 index 0000000..22ae76a --- /dev/null +++ b/DocuMed.Api/WebFramework/Configurations/ConfigureJwtBearerOptions.cs @@ -0,0 +1,21 @@ +namespace DocuMed.Api.WebFramework.Configurations; + +public class ConfigureJwtBearerOptions : IPostConfigureOptions +{ + public void PostConfigure(string name, JwtBearerOptions options) + { + var originalOnMessageReceived = options.Events.OnMessageReceived; + options.Events.OnMessageReceived = async context => + { + await originalOnMessageReceived(context); + + if (string.IsNullOrEmpty(context.Token)) + { + var accessToken = context.Request.Query["access_token"]; + var path = context.HttpContext.Request.Path; + + if (!string.IsNullOrEmpty(accessToken)) context.Token = accessToken; + } + }; + } +} \ No newline at end of file diff --git a/DocuMed.Api/WebFramework/Configurations/LoggerConfig.cs b/DocuMed.Api/WebFramework/Configurations/LoggerConfig.cs new file mode 100644 index 0000000..fd4af6c --- /dev/null +++ b/DocuMed.Api/WebFramework/Configurations/LoggerConfig.cs @@ -0,0 +1,19 @@ +namespace DocuMed.Api.WebFramework.Configurations; + +public static class LoggerConfig +{ + public static void ConfigureSerilog() + { + var logName = $"{DirectoryAddress.Logs}/Log_Server_.log"; + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Console(theme: AnsiConsoleTheme.Literate) + .WriteTo.Sentry(o => + { + o.MinimumEventLevel = LogEventLevel.Error; + o.Dsn = "https://592b7fbb29464442a8e996247abe857f@watcher.igarson.app/7"; + }) + .MinimumLevel.Override("Microsoft.EntityFrameworkCore.Database.Command", LogEventLevel.Warning) + .CreateLogger(); + } +} \ No newline at end of file diff --git a/DocuMed.Api/WebFramework/Configurations/PersianIdentityErrorDescriber.cs b/DocuMed.Api/WebFramework/Configurations/PersianIdentityErrorDescriber.cs new file mode 100644 index 0000000..ac10483 --- /dev/null +++ b/DocuMed.Api/WebFramework/Configurations/PersianIdentityErrorDescriber.cs @@ -0,0 +1,134 @@ +namespace DocuMed.Api.WebFramework.Configurations; + +public class PersianIdentityErrorDescriber : IdentityErrorDescriber +{ + public override IdentityError DefaultError() + { + return new IdentityError { Code = nameof(DefaultError), Description = "ارور ناشناخته ای رخ داده است" }; + } + + public override IdentityError ConcurrencyFailure() + { + return new IdentityError + { Code = nameof(ConcurrencyFailure), Description = "در درخواست شما تداخلی ایجاد شده است" }; + } + + public override IdentityError PasswordMismatch() + { + return new IdentityError { Code = nameof(PasswordMismatch), Description = "رمز عبور اشتباه است" }; + } + + public override IdentityError InvalidToken() + { + return new IdentityError { Code = nameof(InvalidToken), Description = "توکن ارسالی اشتباه است" }; + } + + public override IdentityError LoginAlreadyAssociated() + { + return new IdentityError + { Code = nameof(LoginAlreadyAssociated), Description = "یوزری با این مشخصات در حال حاضر لاگین کرده است" }; + } + + public override IdentityError InvalidUserName(string userName) + { + return new IdentityError + { + Code = nameof(InvalidUserName), + Description = $"یوزر نیم '{userName}' صحیح نمی باشد فقط می توانید از حروف و اعداد استفاده کنید" + }; + } + + public override IdentityError InvalidEmail(string email) + { + return new IdentityError { Code = nameof(InvalidEmail), Description = $"ایمیل '{email}' صحیح نمی باشد" }; + } + + public override IdentityError DuplicateUserName(string userName) + { + return new IdentityError + { + Code = nameof(DuplicateUserName), + Description = $"یوزرنیم '{userName}' قبلا توسط اکانت دیگری استفاده شده است" + }; + } + + public override IdentityError DuplicateEmail(string email) + { + return new IdentityError + { Code = nameof(DuplicateEmail), Description = $"ایمیل '{email}' قبل استفاده شده است" }; + } + + public override IdentityError InvalidRoleName(string role) + { + return new IdentityError { Code = nameof(InvalidRoleName), Description = $"نقش '{role}' موجود نمی باشد" }; + } + + public override IdentityError DuplicateRoleName(string role) + { + return new IdentityError + { Code = nameof(DuplicateRoleName), Description = $"نقش '{role}' قبلا برای این کاربر استفاده شده است" }; + } + + public override IdentityError UserAlreadyHasPassword() + { + return new IdentityError + { Code = nameof(UserAlreadyHasPassword), Description = "کاربر قبلا رمز عبوری را استفاده کرده است" }; + } + + public override IdentityError UserLockoutNotEnabled() + { + return new IdentityError { Code = nameof(UserLockoutNotEnabled), Description = "کاربر مورد نظر قفل شده است" }; + } + + public override IdentityError UserAlreadyInRole(string role) + { + return new IdentityError + { Code = nameof(UserAlreadyInRole), Description = "نشق مورد نظر برای این کاربر استفاده شده است" }; + } + + public override IdentityError UserNotInRole(string role) + { + return new IdentityError { Code = nameof(UserNotInRole), Description = $"کاربر مورد نظر در نقش '{role}' نیست" }; + } + + public override IdentityError PasswordTooShort(int length) + { + return new IdentityError + { Code = nameof(PasswordTooShort), Description = $"پسورد حداقل باید {length} کاراکتر باشد" }; + } + + public override IdentityError PasswordRequiresNonAlphanumeric() + { + return new IdentityError + { + Code = nameof(PasswordRequiresNonAlphanumeric), + Description = "رمز عبور باید حداقل یک کاراکتر غیر عددی داشته باشد" + }; + } + + public override IdentityError PasswordRequiresDigit() + { + return new IdentityError + { + Code = nameof(PasswordRequiresDigit), Description = "پسور مورد نظر باید حداقل یک عدد داشته باشد ('0'-'9')" + }; + } + + public override IdentityError PasswordRequiresLower() + { + return new IdentityError + { + Code = nameof(PasswordRequiresLower), + Description = "پسورد مورد نظر باید حداقل یکی از حروف ('a'-'z') به صورت کوچک داشته باشد" + }; + } + + public override IdentityError PasswordRequiresUpper() + { + return new IdentityError + { + Code = nameof(PasswordRequiresUpper), + Description = "پسورد مورد نظر باید حداقل یکی از حروف ('A'-'Z') به صورت بزرگ داشته باشد" + }; + } +} \ No newline at end of file diff --git a/DocuMed.Api/WebFramework/Configurations/ServiceExtensions.cs b/DocuMed.Api/WebFramework/Configurations/ServiceExtensions.cs new file mode 100644 index 0000000..3782cb9 --- /dev/null +++ b/DocuMed.Api/WebFramework/Configurations/ServiceExtensions.cs @@ -0,0 +1,195 @@ +using System.IO.Compression; +using System.Text; +using Asp.Versioning; +using AspNetCoreRateLimit; +using AspNetCoreRateLimit.Redis; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc.Authorization; +using Microsoft.AspNetCore.ResponseCompression; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json; +using StackExchange.Redis.Extensions.Core.Configuration; +using StackExchange.Redis.Extensions.Newtonsoft; + +namespace DocuMed.Api.WebFramework.Configurations; + +public static class ServiceExtensions +{ + public static void AddIpRateLimit(this IServiceCollection services, IConfigurationRoot configuration) + { + + //load general configuration from appsettings.json + services.Configure(configuration.GetSection("IpRateLimiting")); + + //load ip rules from appsettings.json + services.Configure(configuration.GetSection("IpRateLimitPolicies")); + + // inject counter and rules stores + //services.AddInMemoryRateLimiting(); + services.AddSingleton(); + services.AddSingleton(); + services.AddDistributedRateLimiting(); + services.AddDistributedRateLimiting(); + services.AddRedisRateLimiting(); + + // configuration (resolvers, counter key builders) + services.AddSingleton(); + } + public static void AddCustomStackExchangeRedis(this IServiceCollection serviceCollection, SiteSettings siteSettings) + { + serviceCollection.AddStackExchangeRedisExtensions(options => + { + return new List + { + new() + { + Hosts = new[] + { + new RedisHost + { + Port = siteSettings.MasterRedisConfiguration.Port, + Host = siteSettings.MasterRedisConfiguration.Host + } + }, + Password = siteSettings.MasterRedisConfiguration.Password, + Ssl = false + } + }; + }); + } + + public static void AddCustomDbContext(this IServiceCollection serviceCollection, IConfigurationRoot Configuration) + { + serviceCollection.AddDbContextFactory(options => + { + options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); + options.UseNpgsql(Configuration.GetConnectionString("PostgresServer"), b => b.MigrationsAssembly("Brizco.Repository")) + .UseProjectAssembly(typeof(ApplicationUser).Assembly); + //options.EnableServiceProviderCaching(true); + }).BuildServiceProvider(); + AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); + } + + public static void AddCustomResponseCompression(this IServiceCollection serviceCollection) + { + serviceCollection.Configure(options => + options.Level = CompressionLevel.Fastest); + serviceCollection.AddResponseCompression(options => + { + options.Providers.Add(); + options.EnableForHttps = true; + }); + } + + public static void AddCustomCores(this IServiceCollection serviceCollection) + { + serviceCollection.AddCors(options => options.AddPolicy("CorsPolicy", + builder => + { + builder.AllowAnyMethod() + .SetPreflightMaxAge(TimeSpan.FromHours(24)) + .WithExposedHeaders("Access-control-allow-origins") + .AllowAnyHeader() + .SetIsOriginAllowed(_ => true) + .AllowCredentials(); + })); + } + + public static void AddCustomController(this IServiceCollection serviceCollection) + { + serviceCollection.AddControllers(options => { options.Filters.Add(new AuthorizeFilter()); }) + .AddControllersAsServices() + .AddNewtonsoftJson(options => + { + options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + //options.SerializerSettings.ContractResolver = new DefaultContractResolver(); + } + ); + } + + public static void AddCustomMvc(this IServiceCollection serviceCollection) + { + serviceCollection + .AddMvc() + .AddNewtonsoftJson(options => + { + options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + }); + } + + public static void AddCustomAuthorization(this IServiceCollection serviceCollection) + { + serviceCollection.AddAuthorization(); + } + + public static void AddJwtCustomAuthentication(this IServiceCollection serviceCollection, JwtSettings jwtSettings) + { + serviceCollection.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + var secretKey = Encoding.UTF8.GetBytes(jwtSettings.SecretKey); + var validateParammetrs = new TokenValidationParameters + { + ClockSkew = TimeSpan.Zero, + RequireSignedTokens = true, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(secretKey), + RequireExpirationTime = true, + ValidateLifetime = true, + ValidateAudience = true, + ValidAudience = jwtSettings.Audience, + ValidateIssuer = true, + ValidIssuer = jwtSettings.Issuer + }; + options.RequireHttpsMetadata = true; + options.SaveToken = true; + options.TokenValidationParameters = validateParammetrs; + options.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var accessToken = context.Request.Query["access_token"]; + + // If the request is for our hub... + var path = context.HttpContext.Request.Path; + if (!string.IsNullOrEmpty(accessToken)) + // Read the token out of the query string + context.Token = accessToken.ToString(); + return Task.CompletedTask; + } + }; + }); + } + + public static void AddCustomIdentity(this IServiceCollection serviceCollection) + { + serviceCollection.AddIdentity(options => + { + options.Password.RequireLowercase = false; + options.Password.RequireUppercase = false; + options.Password.RequireDigit = false; + options.Password.RequireNonAlphanumeric = false; + options.User.RequireUniqueEmail = false; + }).AddEntityFrameworkStores() + .AddDefaultTokenProviders() + .AddErrorDescriber(); + ; + } + + public static void AddCustomApiVersioning(this IServiceCollection serviceCollection) + { + serviceCollection.AddApiVersioning(options => + { + options.AssumeDefaultVersionWhenUnspecified = true; + options.DefaultApiVersion = new ApiVersion(1, 0); + options.ApiVersionReader = new HeaderApiVersionReader("api-version"); + options.ReportApiVersions = true; + }); + } +} \ No newline at end of file diff --git a/DocuMed.Api/WebFramework/MiddleWares/ExceptionHandlerMiddleware.cs b/DocuMed.Api/WebFramework/MiddleWares/ExceptionHandlerMiddleware.cs new file mode 100644 index 0000000..e9d6c1a --- /dev/null +++ b/DocuMed.Api/WebFramework/MiddleWares/ExceptionHandlerMiddleware.cs @@ -0,0 +1,214 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Net; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using Task = System.Threading.Tasks.Task; + +namespace DocuMed.Api.WebFramework.MiddleWares; + +public static class ExceptionHandlerMiddlewareExtensions +{ + public static IApplicationBuilder UseExceptionHandlerMiddleware(this IApplicationBuilder applicationBuilder) + { + return applicationBuilder.UseMiddleware(); + } +} + +public class ExceptionHandlerMiddleware +{ + private readonly IWebHostEnvironment _env; + private readonly ILogger _logger; + private readonly RequestDelegate _next; + + public ExceptionHandlerMiddleware( + RequestDelegate next, + IWebHostEnvironment env, + ILogger logger) + { + _next = next; + _env = env; + _logger = logger; + } + + public async Task Invoke(HttpContext context) + { + string message = null; + var httpStatusCode = HttpStatusCode.InternalServerError; + var apiStatusCode = ApiResultStatusCode.ServerError; + + try + { + await _next(context); + } + catch (BaseApiException exception) + { + _logger.LogError(exception, exception.Message); + httpStatusCode = exception.HttpStatusCode; + apiStatusCode = exception.ApiStatusCode; + + if (_env.IsDevelopment()) + { + var dic = new Dictionary + { + ["Exception"] = exception.Message, + ["StackTrace"] = exception.StackTrace + }; + if (exception.InnerException != null) + { + dic.Add("InnerException.Exception", exception.InnerException.Message); + dic.Add("InnerException.StackTrace", exception.InnerException.StackTrace); + } + + if (exception.AdditionalData != null) + dic.Add("AdditionalData", JsonConvert.SerializeObject(exception.AdditionalData)); + var contractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy() + }; + + message = JsonConvert.SerializeObject(dic, new JsonSerializerSettings + { + ContractResolver = contractResolver, + Formatting = Formatting.Indented + }); + } + else + { + message = exception.Message; + } + if(exception.AdditionalData==null) + await WriteToResponseAsync(); + else + await WriteToResponseWithObjectAsync(exception.AdditionalData); + } + catch (SecurityTokenExpiredException exception) + { + _logger.LogError(exception, exception.Message); + SetUnAuthorizeResponse(exception); + await WriteToResponseAsync(); + } + catch (UnauthorizedAccessException exception) + { + _logger.LogError(exception, exception.Message); + SetUnAuthorizeResponse(exception); + await WriteToResponseAsync(); + } + + catch (Exception exception) + { + _logger.LogError(exception, exception.Message); + + if (_env.IsDevelopment()) + { + var dic = new Dictionary + { + ["Exception"] = exception.Message, + ["InnerException"] = exception.InnerException?.Message, + ["StackTrace"] = exception.StackTrace + }; + message = JsonConvert.SerializeObject(dic); + } + + message = exception.Message; + await WriteToResponseAsync(); + } + + async Task WriteToResponseAsync() + { + if (context.Response.HasStarted) + throw new InvalidOperationException("The response has already started, the http status code middleware will not be executed."); + + var result = new ApiResult(false, apiStatusCode, message); + + var contractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy() + }; + + var json = JsonConvert.SerializeObject(result, new JsonSerializerSettings + { + ContractResolver = contractResolver, + Formatting = Formatting.Indented + }); + + context.Response.StatusCode = (int)httpStatusCode; + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(json); + } + + + + async Task WriteToResponseWithObjectAsync(object additionalData) + { + if (context.Response.HasStarted) + throw new InvalidOperationException( + "The response has already started, the http status code middleware will not be executed."); + + var result = new ApiResult(false, apiStatusCode, additionalData, message); + + var contractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy() + }; + + var json = JsonConvert.SerializeObject(result, new JsonSerializerSettings + { + ContractResolver = contractResolver, + Formatting = Formatting.Indented + }); + + context.Response.StatusCode = (int)httpStatusCode; + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(json); + } + + void SetUnAuthorizeResponse(Exception exception) + { + httpStatusCode = HttpStatusCode.Unauthorized; + apiStatusCode = ApiResultStatusCode.UnAuthorized; + + if (_env.IsDevelopment()) + { + var dic = new Dictionary + { + ["Exception"] = exception.Message, + ["StackTrace"] = exception.StackTrace + }; + if (exception is SecurityTokenExpiredException tokenException) + dic.Add("Expires", tokenException.Expires.ToString()); + + message = JsonConvert.SerializeObject(dic); + } + } + + + JwtSecurityToken ReadJwtToken(bool fromHeader = true) + { + try + { + if (fromHeader) + { + var stream = context.Request.Headers.Values.First(v => v.FirstOrDefault().Contains("Bearer")) + .FirstOrDefault(); + var handler = new JwtSecurityTokenHandler(); + var jsonToken = handler.ReadToken(stream.Split(" ").Last()); + return jsonToken as JwtSecurityToken; + } + else + { + string stream = context.Request.Query["access_token"]; + ; + var handler = new JwtSecurityTokenHandler(); + var jsonToken = handler.ReadToken(stream.Split(" ").Last()); + return jsonToken as JwtSecurityToken; + } + } + catch (Exception e) + { + throw new BaseApiException(ApiResultStatusCode.UnAuthorized, e.Message + " Jwt is wrong", + HttpStatusCode.Unauthorized); + } + } + } +} \ No newline at end of file diff --git a/DocuMed.Api/WebFramework/MiddleWares/PerformanceMiddleware.cs b/DocuMed.Api/WebFramework/MiddleWares/PerformanceMiddleware.cs new file mode 100644 index 0000000..18b42ca --- /dev/null +++ b/DocuMed.Api/WebFramework/MiddleWares/PerformanceMiddleware.cs @@ -0,0 +1,37 @@ +using System.Diagnostics; + +namespace DocuMed.Api.WebFramework.MiddleWares; + +public static class PerformanceMiddlewareExtensions +{ + public static IApplicationBuilder UsePerformanceMiddlewar(this IApplicationBuilder applicationBuilder) + { + return applicationBuilder.UseMiddleware(); + } +} + +public class PerformanceMiddleware +{ + private readonly ILogger _logger; + private readonly RequestDelegate _next; + private readonly Stopwatch _timer; + + public PerformanceMiddleware( + RequestDelegate next, + ILogger logger) + { + _next = next; + _logger = logger; + _timer = new Stopwatch(); + } + + public async System.Threading.Tasks.Task Invoke(HttpContext context) + { + _timer.Start(); + await _next(context); + _timer.Stop(); + + var elapsedMilliseconds = _timer.ElapsedMilliseconds; + _logger.LogWarning($"REQUEST TIMER : {elapsedMilliseconds}"); + } +} \ No newline at end of file diff --git a/DocuMed.Api/WebFramework/Swagger/SwaggerConfiguration.cs b/DocuMed.Api/WebFramework/Swagger/SwaggerConfiguration.cs new file mode 100644 index 0000000..e861908 --- /dev/null +++ b/DocuMed.Api/WebFramework/Swagger/SwaggerConfiguration.cs @@ -0,0 +1,280 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.OpenApi.Models; +using Pluralize.NET; +using Swashbuckle.AspNetCore.SwaggerGen; +using Swashbuckle.AspNetCore.SwaggerUI; + +namespace DocuMed.Api.WebFramework.Swagger; + +public static class SwaggerConfiguration +{ + public static void AddCustomSwagger(this IServiceCollection services,string baseUrl) + { + services.AddSwaggerGen(options => + { + //var xmlDuc = Path.Combine(AppContext.BaseDirectory, "swaggerApi.xml"); + //options.IncludeXmlComments(xmlDuc,true); + options.SwaggerDoc("v1", + new OpenApiInfo + { + Version = "v1", + Title = "iGarson Api Dacument", + Description = "iGarson api for clients that wana use", + License = new OpenApiLicense { Name = "Vira Safir Fanavar " }, + Contact = new OpenApiContact + { + Name = "Amir Hossein Khademi", + Email = "avvampier@gmail.com", + Url = new Uri("http://amir-khademi.ir/") + } + }); + options.EnableAnnotations(); + options.DescribeAllParametersInCamelCase(); + options.IgnoreObsoleteActions(); + + //#region Versioning + + //// Remove version parameter from all Operations + //options.OperationFilter(); + + ////set version "api/v{version}/[controller]" from current swagger doc verion + //options.DocumentFilter(); + + ////Seperate and categorize end-points by doc version + //options.DocInclusionPredicate((version, desc) => + //{ + // if (!desc.TryGetMethodInfo(out var methodInfo)) return false; + // var versions = methodInfo.DeclaringType + // .GetCustomAttributes(true) + // .OfType() + // .SelectMany(attr => attr.Versions) + // .ToList(); + + // return versions.Any(v => $"v{v.ToString()}" == version); + //}); + + //#endregion + + #region Security + + var url = $"{baseUrl}/api/auth/login/swagger"; + options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2, + Scheme = "Bearer", + Name = "Bearer", + Flows = new OpenApiOAuthFlows + { + Password = new OpenApiOAuthFlow + { + TokenUrl = new Uri(url) + } + } + }); + options.OperationFilter(true, "Bearer"); + + #endregion + + #region Customize + + options.OperationFilter(); + + #endregion + }); + } + + public static void UseCustomSwagger(this IApplicationBuilder app,string baseUrl) + { + app.UseSwagger(options => + { + options.SerializeAsV2 = true; + }); + + app.UseSwaggerUI(options => + { + options.InjectStylesheet("/assets/swagger-ui/x3/theme-flattop.css"); + options.DocExpansion(DocExpansion.None); + // Display + options.DefaultModelExpandDepth(2); + options.DefaultModelRendering(ModelRendering.Model); + options.DefaultModelsExpandDepth(-1); + options.DisplayOperationId(); + options.DisplayRequestDuration(); + options.EnableDeepLinking(); + options.EnableFilter(); + options.ShowExtensions(); + + options.OAuthUseBasicAuthenticationWithAccessCodeGrant(); + options.SwaggerEndpoint($"{baseUrl}/swagger/v1/swagger.json", "V1 Docs"); + }); + } +} + +public class RemoveVersionParameters : IOperationFilter +{ + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + // Remove version parameter from all Operations + var versionParameter = operation.Parameters.SingleOrDefault(p => p.Name == "version"); + if (versionParameter != null) + operation.Parameters.Remove(versionParameter); + } +} + +public class SetVersionInPaths : IDocumentFilter +{ + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + if (swaggerDoc == null) + throw new ArgumentNullException(nameof(swaggerDoc)); + + var replacements = new OpenApiPaths(); + + foreach (var (key, value) in swaggerDoc.Paths) + replacements.Add(key.Replace("v{version}", swaggerDoc.Info.Version, StringComparison.InvariantCulture), + value); + + swaggerDoc.Paths = replacements; + } +} + +public class UnauthorizedResponsesOperationFilter : IOperationFilter +{ + private readonly bool includeUnauthorizedAndForbiddenResponses; + private readonly string schemeName; + + public UnauthorizedResponsesOperationFilter(bool includeUnauthorizedAndForbiddenResponses, + string schemeName = "Bearer") + { + this.includeUnauthorizedAndForbiddenResponses = includeUnauthorizedAndForbiddenResponses; + this.schemeName = schemeName; + } + + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + var filters = context.ApiDescription.ActionDescriptor.FilterDescriptors; + + var hasAnynomousEndPoint = context.ApiDescription.ActionDescriptor.EndpointMetadata.Any(e => + e.GetType() == typeof(AllowAnonymousAttribute)); + + //var hasAnonymous = filters.Any(p => p.Filter is AllowAnonymousFilter); + if (hasAnynomousEndPoint) + return; + + /*var hasAuthorize = filters.Any(p => p.Filter is AuthorizeFilter); + if (!hasAuthorize) + return;*/ + + if (includeUnauthorizedAndForbiddenResponses) + { + operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" }); + operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" }); + } + + operation.Security.Add(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + new string[] { } + } + }); + } +} + +public class ApplySummariesOperationFilter : IOperationFilter +{ + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + var controllerActionDescriptor = context.ApiDescription.ActionDescriptor as ControllerActionDescriptor; + if (controllerActionDescriptor == null) return; + + var pluralizer = new Pluralizer(); + + var actionName = controllerActionDescriptor.ActionName; + var singularizeName = pluralizer.Singularize(controllerActionDescriptor.ControllerName); + var pluralizeName = pluralizer.Pluralize(singularizeName); + + var parameterCount = operation.Parameters.Where(p => p.Name != "version" && p.Name != "api-version").Count(); + + if (IsGetAllAction()) + { + if (!operation.Summary.HasValue()) + operation.Summary = $"Returns all {pluralizeName}"; + } + else if (IsActionName("Post", "Create")) + { + if (!operation.Summary.HasValue()) + operation.Summary = $"Creates a {singularizeName}"; + + if (operation.Parameters.Count > 0 && !operation.Parameters[0].Description.HasValue()) + operation.Parameters[0].Description = $"A {singularizeName} representation"; + } + else if (IsActionName("Read", "Get")) + { + if (!operation.Summary.HasValue()) + operation.Summary = $"Retrieves a {singularizeName} by unique id"; + + if (operation.Parameters.Count > 0 && !operation.Parameters[0].Description.HasValue()) + operation.Parameters[0].Description = $"a unique id for the {singularizeName}"; + } + else if (IsActionName("Put", "Edit", "Update")) + { + if (!operation.Summary.HasValue()) + operation.Summary = $"Updates a {singularizeName} by unique id"; + + //if (!operation.Parameters[0].OrderDescription.HasValue()) + // operation.Parameters[0].OrderDescription = $"A unique id for the {singularizeName}"; + + if (operation.Parameters.Count > 0 && !operation.Parameters[0].Description.HasValue()) + operation.Parameters[0].Description = $"A {singularizeName} representation"; + } + else if (IsActionName("Delete", "Remove")) + { + if (!operation.Summary.HasValue()) + operation.Summary = $"Deletes a {singularizeName} by unique id"; + + if (operation.Parameters.Count > 0 && !operation.Parameters[0].Description.HasValue()) + operation.Parameters[0].Description = $"A unique id for the {singularizeName}"; + } + else + { + if (!operation.Summary.HasValue()) + operation.Summary = $"{actionName} {pluralizeName}"; + } + + #region Local Functions + + bool IsGetAllAction() + { + foreach (var name in new[] { "Get", "Read", "Select" }) + if (actionName.Equals(name, StringComparison.OrdinalIgnoreCase) && parameterCount == 0 || + actionName.Equals($"{name}All", StringComparison.OrdinalIgnoreCase) || + actionName.Equals($"{name}{pluralizeName}", StringComparison.OrdinalIgnoreCase) || + actionName.Equals($"{name}All{singularizeName}", StringComparison.OrdinalIgnoreCase) || + actionName.Equals($"{name}All{pluralizeName}", StringComparison.OrdinalIgnoreCase)) + return true; + return false; + } + + bool IsActionName(params string[] names) + { + foreach (var name in names) + if (actionName.Contains(name, StringComparison.OrdinalIgnoreCase) || + actionName.Contains($"{name}ById", StringComparison.OrdinalIgnoreCase) || + actionName.Contains($"{name}{singularizeName}", StringComparison.OrdinalIgnoreCase) || + actionName.Contains($"{name}{singularizeName}ById", StringComparison.OrdinalIgnoreCase)) + return true; + return false; + } + + #endregion + } +} \ No newline at end of file diff --git a/DocuMed.Core/Class1.cs b/DocuMed.Core/CoreConfig.cs similarity index 58% rename from DocuMed.Core/Class1.cs rename to DocuMed.Core/CoreConfig.cs index 5e31edd..fc535e8 100644 --- a/DocuMed.Core/Class1.cs +++ b/DocuMed.Core/CoreConfig.cs @@ -1,6 +1,6 @@ namespace DocuMed.Core { - public class Class1 + public class CoreConfig { } diff --git a/DocuMed.Core/DocuMed.Core.csproj b/DocuMed.Core/DocuMed.Core.csproj index 6fc9784..5d5b38a 100644 --- a/DocuMed.Core/DocuMed.Core.csproj +++ b/DocuMed.Core/DocuMed.Core.csproj @@ -19,4 +19,15 @@ + + + + + + + + + + + diff --git a/DocuMed.Core/Models/Api/ApiResult.cs b/DocuMed.Core/Models/Api/ApiResult.cs new file mode 100644 index 0000000..89cc760 --- /dev/null +++ b/DocuMed.Core/Models/Api/ApiResult.cs @@ -0,0 +1,127 @@ +namespace DocuMed.Core.Models.Api; + +public class ApiResult +{ + public ApiResult(bool isSuccess, ApiResultStatusCode statusCode, string message = null) + { + IsSuccess = isSuccess; + StatusCode = statusCode; + Message = message ?? statusCode.ToDisplay(); + } + + public bool IsSuccess { get; set; } + public ApiResultStatusCode StatusCode { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string Message { get; set; } + + #region Implicit Operators + + public static implicit operator ApiResult(OkResult result) + { + return new ApiResult(true, ApiResultStatusCode.Success); + } + + public static implicit operator ApiResult(BadRequestResult result) + { + return new ApiResult(false, ApiResultStatusCode.BadRequest); + } + + public static implicit operator ApiResult(BadRequestObjectResult result) + { + var message = result.Value.ToString(); + if (result.Value is SerializableError errors) + { + var errorMessages = errors.SelectMany(p => (string[])p.Value).Distinct(); + message = string.Join(" | ", errorMessages); + } + + return new ApiResult(false, ApiResultStatusCode.BadRequest, message); + } + + public static implicit operator ApiResult(ContentResult result) + { + return new ApiResult(true, ApiResultStatusCode.Success, result.Content); + } + + public static implicit operator ApiResult(NotFoundResult result) + { + return new ApiResult(false, ApiResultStatusCode.NotFound); + } + + public static implicit operator ApiResult(ForbidResult result) + { + return new ApiResult(false, ApiResultStatusCode.NotFound); + } + + public static implicit operator ApiResult(StatusCodeResult result) + { + return new ApiResult(false, ApiResultStatusCode.NotFound); + } + + #endregion +} + +public class ApiResult : ApiResult + where TData : class +{ + public ApiResult(bool isSuccess, ApiResultStatusCode statusCode, TData data, string message = null) + : base(isSuccess, statusCode, message) + { + Data = data; + } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public TData Data { get; set; } + + #region Implicit Operators + + public static implicit operator ApiResult(TData data) + { + return new ApiResult(true, ApiResultStatusCode.Success, data); + } + + public static implicit operator ApiResult(OkResult result) + { + return new ApiResult(true, ApiResultStatusCode.Success, null); + } + + public static implicit operator ApiResult(OkObjectResult result) + { + return new ApiResult(true, ApiResultStatusCode.Success, (TData)result.Value); + } + + public static implicit operator ApiResult(BadRequestResult result) + { + return new ApiResult(false, ApiResultStatusCode.BadRequest, null); + } + + public static implicit operator ApiResult(BadRequestObjectResult result) + { + var message = result.Value.ToString(); + if (result.Value is SerializableError errors) + { + var errorMessages = errors.SelectMany(p => (string[])p.Value).Distinct(); + message = string.Join(" | ", errorMessages); + } + + return new ApiResult(false, ApiResultStatusCode.BadRequest, null, message); + } + + public static implicit operator ApiResult(ContentResult result) + { + return new ApiResult(true, ApiResultStatusCode.Success, null, result.Content); + } + + public static implicit operator ApiResult(NotFoundResult result) + { + return new ApiResult(false, ApiResultStatusCode.NotFound, null); + } + + public static implicit operator ApiResult(NotFoundObjectResult result) + { + return new ApiResult(false, ApiResultStatusCode.NotFound, (TData)result.Value); + } + + #endregion +} \ No newline at end of file diff --git a/DocuMed.Infrastructure/DocuMed.Infrastructure.csproj b/DocuMed.Infrastructure/DocuMed.Infrastructure.csproj index 3419028..67ba8d5 100644 --- a/DocuMed.Infrastructure/DocuMed.Infrastructure.csproj +++ b/DocuMed.Infrastructure/DocuMed.Infrastructure.csproj @@ -1,4 +1,4 @@ - + net7.0 @@ -14,4 +14,8 @@ + + + + diff --git a/DocuMed.Infrastructure/Class1.cs b/DocuMed.Infrastructure/InfrastructureConfig.cs similarity index 56% rename from DocuMed.Infrastructure/Class1.cs rename to DocuMed.Infrastructure/InfrastructureConfig.cs index 9fcbf72..10396d6 100644 --- a/DocuMed.Infrastructure/Class1.cs +++ b/DocuMed.Infrastructure/InfrastructureConfig.cs @@ -1,6 +1,6 @@ namespace DocuMed.Infrastructure { - public class Class1 + public class InfrastructureConfig { } diff --git a/DocuMed.Infrastructure/Models/DirectoryAddress.cs b/DocuMed.Infrastructure/Models/DirectoryAddress.cs new file mode 100644 index 0000000..590e850 --- /dev/null +++ b/DocuMed.Infrastructure/Models/DirectoryAddress.cs @@ -0,0 +1,6 @@ +namespace DocuMed.Infrastructure.Models; +public static class DirectoryAddress +{ + private static readonly string BaseDire = $"{Directory.GetCurrentDirectory()}/wwwroot"; + public static string Logs = $"{BaseDire}/logs"; +} \ No newline at end of file diff --git a/DocuMed.Repository/Abstracts/ICurrentUserService.cs b/DocuMed.Repository/Abstracts/ICurrentUserService.cs index 9f3e332..3cdc638 100644 --- a/DocuMed.Repository/Abstracts/ICurrentUserService.cs +++ b/DocuMed.Repository/Abstracts/ICurrentUserService.cs @@ -4,6 +4,5 @@ public interface ICurrentUserService : IScopedDependency { string? UserId { get; } string? RoleName { get; } - string? ComplexId { get; } string? UserName { get; } } \ No newline at end of file diff --git a/DocuMed.Repository/DocuMed.Repository.csproj b/DocuMed.Repository/DocuMed.Repository.csproj index f149bd1..dc858ae 100644 --- a/DocuMed.Repository/DocuMed.Repository.csproj +++ b/DocuMed.Repository/DocuMed.Repository.csproj @@ -34,6 +34,7 @@ + diff --git a/DocuMed.Repository/RepositoryConfig.cs b/DocuMed.Repository/RepositoryConfig.cs index ddd7adc..a892910 100644 --- a/DocuMed.Repository/RepositoryConfig.cs +++ b/DocuMed.Repository/RepositoryConfig.cs @@ -1,5 +1,19 @@ -namespace DocuMed.Repository; - public class RepositoryConfig - { +using Microsoft.AspNetCore.Builder; - } \ No newline at end of file +namespace DocuMed.Repository; +public static class RepositoryConfig +{ + public static async Task InitialDb(this IApplicationBuilder app) + { + var scopeFactory = app.ApplicationServices.GetRequiredService(); + using (var scope = scopeFactory.CreateScope()) + { + var identityDbInitialize = scope.ServiceProvider.GetService(); + if (identityDbInitialize != null) + { + identityDbInitialize.Initialize(); + await identityDbInitialize.SeedDate(); + } + } + } +} \ No newline at end of file