532 lines
14 KiB
C#
532 lines
14 KiB
C#
using Asp.Versioning;
|
|
using Asp.Versioning.ApiExplorer;
|
|
using OnlineSalesAutoCrop.CoreAPI.API.Swagger;
|
|
using OnlineSalesAutoCrop.CoreAPI.Configuration.DI;
|
|
using OnlineSalesAutoCrop.CoreAPI.Configurations;
|
|
using OnlineSalesAutoCrop.CoreAPI.Models;
|
|
using OnlineSalesAutoCrop.CoreAPI.Models.Global;
|
|
using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.Systems;
|
|
using OnlineSalesAutoCrop.CoreAPI.SignalRHub;
|
|
using Hangfire;
|
|
using Hangfire.SqlServer;
|
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.CookiePolicy;
|
|
using Microsoft.AspNetCore.Diagnostics;
|
|
using Microsoft.AspNetCore.Hosting;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.HttpOverrides;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using Microsoft.IdentityModel.Tokens;
|
|
using Newtonsoft.Json;
|
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
|
using System;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using System.Threading.RateLimiting;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace OnlineSalesAutoCrop.CoreAPI
|
|
{
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
public class Startup
|
|
{
|
|
private readonly AppSettings _appSettings;
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
public IConfiguration Configuration { get; }
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the Startup class using the specified configuration settings.
|
|
/// </summary>
|
|
/// <param name="configuration">The configuration settings used to initialize the application. Cannot be null.</param>
|
|
public Startup(IConfiguration configuration)
|
|
{
|
|
Configuration = configuration;
|
|
_appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>();
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="services"></param>
|
|
public void ConfigureServices(IServiceCollection services)
|
|
{
|
|
try
|
|
{
|
|
if (_appSettings.IsValid())
|
|
{
|
|
#region AppSettings & MenuSettings
|
|
|
|
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
|
|
services.Configure<MenuSettings>(Configuration.GetSection("MenuSettings"));
|
|
|
|
#endregion
|
|
|
|
#region Controllers
|
|
|
|
services.AddControllers(options =>
|
|
{
|
|
options.Filters.Add(new ProducesAttribute("application/json"));
|
|
});
|
|
|
|
#endregion
|
|
|
|
#region API versioning
|
|
|
|
services.AddApiVersioning(options =>
|
|
{
|
|
options.ReportApiVersions = true;
|
|
options.DefaultApiVersion = new ApiVersion(1, 0);
|
|
options.AssumeDefaultVersionWhenUnspecified = true;
|
|
options.ApiVersionReader = new UrlSegmentApiVersionReader();
|
|
})
|
|
.AddMvc() // API versioning extensions for MVC Core
|
|
.AddApiExplorer(); // API version-aware API Explorer extensions
|
|
|
|
#endregion
|
|
|
|
#region CORS
|
|
|
|
string[] origins = _appSettings.CorsOrigins.Split(',');
|
|
string[] methods = _appSettings.CorsMethods.Split(',');
|
|
string[] headers = _appSettings.CorsHeaders.Split(',');
|
|
services.AddCors(options =>
|
|
{
|
|
options.DefaultPolicyName = "CORSPolicies";
|
|
options.AddDefaultPolicy(builder =>
|
|
{
|
|
builder.WithOrigins(origins)
|
|
.SetIsOriginAllowedToAllowWildcardSubdomains()
|
|
.WithMethods(methods)
|
|
.WithHeaders(headers)
|
|
.AllowCredentials();
|
|
});
|
|
});
|
|
|
|
#endregion
|
|
|
|
#region Caching
|
|
|
|
if (_appSettings.CacheType == EnumCacheType.Redis && !string.IsNullOrEmpty(_appSettings.CacheUrl))
|
|
{
|
|
services.AddStackExchangeRedisCache(options =>
|
|
{
|
|
options.Configuration = _appSettings.CacheUrl;
|
|
});
|
|
}
|
|
else if (_appSettings.CacheType == EnumCacheType.SqlServer && !string.IsNullOrEmpty(_appSettings.CacheUrl) && !string.IsNullOrEmpty(_appSettings.CacheTableName) && !string.IsNullOrEmpty(_appSettings.CacheSchemaName))
|
|
{
|
|
services.AddDistributedSqlServerCache(options =>
|
|
{
|
|
options.ConnectionString = _appSettings.CacheUrl;
|
|
options.TableName = _appSettings.CacheTableName;
|
|
options.SchemaName = _appSettings.CacheSchemaName;
|
|
options.ExpiredItemsDeletionInterval = TimeSpan.FromMinutes(5);
|
|
});
|
|
}
|
|
else
|
|
{
|
|
services.AddMemoryCache();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Cookie Settings
|
|
|
|
services.Configure<CookiePolicyOptions>(options =>
|
|
{
|
|
options.ConsentCookieValue = "true";
|
|
options.CheckConsentNeeded = context => true;
|
|
options.MinimumSameSitePolicy = (SameSiteMode)_appSettings.CookieSameSite;
|
|
if (_appSettings.CookieHttpOnly)
|
|
{
|
|
options.HttpOnly = HttpOnlyPolicy.Always;
|
|
}
|
|
});
|
|
|
|
#endregion
|
|
|
|
#region Session
|
|
|
|
services.AddSession(options =>
|
|
{
|
|
options.Cookie.Name = "OnlineSalesAutoCrop.Net9.Session.Id";
|
|
options.Cookie.HttpOnly = _appSettings.CookieHttpOnly;
|
|
options.Cookie.SameSite = (SameSiteMode)_appSettings.CookieSameSite;
|
|
options.IdleTimeout = TimeSpan.FromMinutes(_appSettings.CookieLifeTime);
|
|
});
|
|
|
|
#endregion
|
|
|
|
#region JWT Authentication
|
|
|
|
var key = Encoding.ASCII.GetBytes(_appSettings.JwtCryptoKey);
|
|
services.AddAuthentication(options =>
|
|
{
|
|
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
|
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
|
})
|
|
.AddJwtBearer(options =>
|
|
{
|
|
options.SaveToken = true;
|
|
options.RequireHttpsMetadata = false;
|
|
options.TokenValidationParameters = new TokenValidationParameters
|
|
{
|
|
ClockSkew = TimeSpan.Zero,
|
|
ValidateIssuerSigningKey = true,
|
|
ValidIssuer = _appSettings.JwtIssuer,
|
|
ValidAudience = _appSettings.JwtAudience,
|
|
ValidateIssuer = _appSettings.JwtValidateIssuer,
|
|
IssuerSigningKey = new SymmetricSecurityKey(key),
|
|
ValidateAudience = _appSettings.JwtValidateAudience
|
|
};
|
|
|
|
options.Events = new JwtBearerEvents
|
|
{
|
|
OnAuthenticationFailed = context =>
|
|
{
|
|
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
|
|
context.Response.Headers.Append("Token-Expired", "true");
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
};
|
|
});
|
|
|
|
#endregion
|
|
|
|
#region Mvc
|
|
|
|
services.AddMvc();
|
|
|
|
#endregion
|
|
|
|
#region Forwarded Headers
|
|
|
|
services.Configure<ForwardedHeadersOptions>(options =>
|
|
{
|
|
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
|
});
|
|
|
|
#endregion
|
|
|
|
#region Antiforgery
|
|
|
|
services.AddAntiforgery(options =>
|
|
{
|
|
options.SuppressXFrameOptionsHeader = false;
|
|
options.HeaderName = "OnlineSalesAutoCrop.X-XSRF-TOKEN";
|
|
options.Cookie.Name = "OnlineSalesAutoCrop.X-XSRF-ANTIFORGERY";
|
|
options.Cookie.HttpOnly = _appSettings.CookieHttpOnly;
|
|
options.Cookie.SameSite = (SameSiteMode)_appSettings.CookieSameSite;
|
|
});
|
|
|
|
#endregion
|
|
|
|
#region SignalR
|
|
|
|
services.AddSignalR();
|
|
services.AddResponseCompression(opt =>
|
|
{
|
|
opt.MimeTypes = ["application/octet-stream"];
|
|
});
|
|
|
|
#endregion
|
|
|
|
#region Rate Limit
|
|
|
|
if (!string.IsNullOrEmpty(_appSettings.RLName))
|
|
{
|
|
services.AddRateLimiter(options =>
|
|
{
|
|
options.AddPolicy(_appSettings.RLName, httpContext =>
|
|
RateLimitPartition.GetFixedWindowLimiter(
|
|
partitionKey: httpContext.GetIpAddress(),
|
|
factory: _ => new FixedWindowRateLimiterOptions
|
|
{
|
|
QueueLimit = _appSettings.RLQueueLimit,
|
|
PermitLimit = _appSettings.RLPermitLimit,
|
|
AutoReplenishment = _appSettings.RLAutoReplenishment,
|
|
Window = TimeSpan.FromSeconds(_appSettings.RLWindowInSecond),
|
|
QueueProcessingOrder = (QueueProcessingOrder)_appSettings.RLQueueProcessingOrder
|
|
}));
|
|
|
|
options.OnRejected = (context, cancellationToken) =>
|
|
{
|
|
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
|
|
{
|
|
context.HttpContext.Response.Headers.RetryAfter = retryAfter.TotalSeconds.ToString();
|
|
}
|
|
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
|
|
context.HttpContext.Response.WriteAsync(text: $"Too many requests (more than {_appSettings.RLPermitLimit} calls in {_appSettings.RLWindowInSecond} seconds). Please try again later.", cancellationToken: cancellationToken);
|
|
return new ValueTask();
|
|
};
|
|
});
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Hangfire
|
|
|
|
if (!string.IsNullOrEmpty(_appSettings.HangfireDb))
|
|
{
|
|
services.AddHangfire(config =>
|
|
{
|
|
config.UseFilter(new TimeRestrictionServerFilter(startHour: 1, endHour: 7));
|
|
config.UseSqlServerStorage(_appSettings.HangfireDb, new SqlServerStorageOptions
|
|
{
|
|
DisableGlobalLocks = true,
|
|
QueuePollInterval = TimeSpan.Zero,
|
|
UseRecommendedIsolationLevel = true,
|
|
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
|
|
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5)
|
|
});
|
|
});
|
|
services.AddHangfireServer();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region SWAGGER
|
|
|
|
if (_appSettings.Swagger.Enabled)
|
|
{
|
|
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
|
|
services.AddSwaggerGen(options =>
|
|
{
|
|
options.OperationFilter<SwaggerDefaultValues>();
|
|
options.IncludeXmlComments(filePath: XmlCommentsFilePath);
|
|
});
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Business Service
|
|
|
|
services.ConfigureBusinessServices();
|
|
|
|
#endregion
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
//Nothimg to do here as we are handling all exceptions in Configure method using global exception handler
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="app"></param>
|
|
/// <param name="env"></param>
|
|
/// <param name="provider"></param>
|
|
/// <param name="loggerFactory"></param>
|
|
/// <param name="logger"></param>
|
|
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider, ILoggerFactory loggerFactory, ILogger<Startup> logger)
|
|
{
|
|
logger.LogTrace("Startup::Configure");
|
|
try
|
|
{
|
|
if (env.IsDevelopment())
|
|
{
|
|
app.UseDeveloperExceptionPage();
|
|
}
|
|
else
|
|
{
|
|
#region Exception Handling in Production mode
|
|
|
|
app.UseExceptionHandler(a => a.Run(async context =>
|
|
{
|
|
IExceptionHandlerPathFeature feature = context.Features.Get<IExceptionHandlerPathFeature>();
|
|
Exception exception = feature.Error;
|
|
HttpStatusCode code = HttpStatusCode.InternalServerError;
|
|
|
|
if (exception is ArgumentNullException)
|
|
code = HttpStatusCode.BadRequest;
|
|
else if (exception is ArgumentException)
|
|
code = HttpStatusCode.BadRequest;
|
|
else if (exception is UnauthorizedAccessException)
|
|
code = HttpStatusCode.Unauthorized;
|
|
else if (exception is WebException && (exception as WebException).Response is HttpWebResponse response)
|
|
code = response.StatusCode;
|
|
|
|
logger.LogError($"GLOBAL ERROR HANDLER::HTTP:{code}::{exception.Message}");
|
|
|
|
var result = JsonConvert.SerializeObject(exception, Formatting.Indented);
|
|
|
|
context.Response.Clear();
|
|
context.Response.ContentType = "application/json";
|
|
await context.Response.WriteAsync(result);
|
|
}));
|
|
|
|
app.UseHsts();
|
|
app.UseHttpsRedirection();
|
|
|
|
#endregion
|
|
}
|
|
|
|
#region CORS
|
|
|
|
app.UseCors("CORSPolicies");
|
|
|
|
#endregion
|
|
|
|
#region Use Static Files
|
|
|
|
app.UseStaticFiles();
|
|
|
|
#endregion
|
|
|
|
#region Routing
|
|
|
|
app.UseRouting();
|
|
|
|
#endregion
|
|
|
|
#region Session
|
|
|
|
app.UseSession();
|
|
|
|
#endregion
|
|
|
|
#region For Authentication
|
|
|
|
app.UseAuthentication();
|
|
|
|
#endregion
|
|
|
|
#region For Authorization
|
|
|
|
app.UseAuthorization();
|
|
|
|
#endregion
|
|
|
|
#region X-Frame-Options
|
|
|
|
app.Use(async (context, next) =>
|
|
{
|
|
if (_appSettings.CookieHttpOnly)
|
|
{
|
|
string tokens = context.Request.Cookies["OnlineSalesAutoCrop.X-XSRF-TOKEN"];
|
|
if (!string.IsNullOrEmpty(tokens))
|
|
context.Request.Headers.Append("OnlineSalesAutoCrop.X-XSRF-TOKEN", tokens);
|
|
}
|
|
await next();
|
|
});
|
|
|
|
#endregion
|
|
|
|
#region RateLimiter
|
|
|
|
if (!string.IsNullOrEmpty(_appSettings.RLName))
|
|
{
|
|
app.UseRateLimiter();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Logger
|
|
|
|
if (!string.IsNullOrEmpty(_appSettings.LoggerPath))
|
|
{
|
|
loggerFactory.AddFile(pathFormat: $@"{_appSettings.LoggerPath}\Log.txt", minimumLevel: (LogLevel)_appSettings.LoggerMinLevel);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Controllers and SignalR
|
|
|
|
app.UseEndpoints(endpoints =>
|
|
{
|
|
if (!string.IsNullOrEmpty(_appSettings.RLName))
|
|
{
|
|
endpoints.MapControllers().RequireRateLimiting(_appSettings.RLName);
|
|
}
|
|
else
|
|
{
|
|
endpoints.MapControllers();
|
|
}
|
|
endpoints.MapHub<NotificationHub>("/signalRHub");
|
|
});
|
|
|
|
#endregion
|
|
|
|
#region Forwarded Headers
|
|
|
|
app.UseForwardedHeaders();
|
|
|
|
#endregion
|
|
|
|
#region Request Localization
|
|
|
|
app.UseRequestLocalization();
|
|
|
|
#endregion
|
|
|
|
#region Hangfire
|
|
|
|
if (!string.IsNullOrEmpty(_appSettings.HangfireDb))
|
|
{
|
|
app.UseHangfireDashboard();
|
|
|
|
RecurringJobOptions options = new() { TimeZone = TimeZoneInfo.Local, MisfireHandling = MisfireHandlingMode.Relaxed };
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Swagger
|
|
|
|
if (_appSettings.IsValid() && _appSettings.Swagger.Enabled)
|
|
{
|
|
app.UseSwagger();
|
|
app.UseSwaggerUI(options =>
|
|
{
|
|
foreach (var description in provider.ApiVersionDescriptions)
|
|
{
|
|
string sjp = string.IsNullOrWhiteSpace(options.RoutePrefix) ? "." : "..";
|
|
options.SwaggerEndpoint($"{sjp}/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
|
|
}
|
|
});
|
|
}
|
|
|
|
#endregion
|
|
|
|
app.Run(async (context) =>
|
|
{
|
|
context.Response.ContentType = "text/html";
|
|
await context.Response.WriteAsync("<b>Ease Taskforce Api is Running</b>");
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
static string XmlCommentsFilePath
|
|
{
|
|
get
|
|
{
|
|
var basePath = AppContext.BaseDirectory;
|
|
var fileName = typeof(Startup).GetTypeInfo().Assembly.GetName().Name + ".xml";
|
|
return Path.Combine(basePath, fileName);
|
|
}
|
|
}
|
|
}
|
|
}
|