diff --git a/IdentityServer.Api/IdentityServer.Api.csproj b/IdentityServer.Api/IdentityServer.Api.csproj index 639c3e4..c893d3e 100644 --- a/IdentityServer.Api/IdentityServer.Api.csproj +++ b/IdentityServer.Api/IdentityServer.Api.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.1 @@ -22,6 +22,7 @@ + diff --git a/IdentityServer.Api/Program.cs b/IdentityServer.Api/Program.cs index 5a4a626..204b57a 100644 --- a/IdentityServer.Api/Program.cs +++ b/IdentityServer.Api/Program.cs @@ -1,11 +1,14 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Core; +using Serilog.Events; +using Serilog.Sinks.MSSqlServer; using System; -using System.Collections.Generic; +using System.Diagnostics; +using System.IO; using System.Linq; -using System.Threading.Tasks; namespace IdentityServer.Api { @@ -13,14 +16,69 @@ namespace IdentityServer.Api { public static void Main(string[] args) { - CreateHostBuilder(args).Build().Run(); + var isConsole = Debugger.IsAttached || args.Contains("--console"); + if (!isConsole) + { + var pathToExe = Process.GetCurrentProcess().MainModule.FileName; + var pathToContentRoot = Path.GetDirectoryName(pathToExe); + Directory.SetCurrentDirectory(pathToContentRoot); + } + + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .Build(); + + var connectionString = configuration.GetConnectionString("DatabaseConnection"); + var loggingLevelParam = configuration.GetValue("Logging:LogLevel:Default"); + + Enum.TryParse(loggingLevelParam, out LogEventLevel loggingLevel); + var loggingLevelSwitch = new LoggingLevelSwitch(loggingLevel); + + var columnOptions = new ColumnOptions(); + columnOptions.Store.Remove(StandardColumn.Properties); + columnOptions.Store.Remove(StandardColumn.MessageTemplate); + columnOptions.Store.Add(StandardColumn.LogEvent); + Log.Logger = new LoggerConfiguration() + .MinimumLevel.ControlledBy(loggingLevelSwitch) + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.MSSqlServer(connectionString, "__Logs", autoCreateSqlTable: true, columnOptions: columnOptions) + .CreateLogger(); + + try + { + var urls = configuration.GetValue("urls"); + Log.Information("Starting identity server API..."); + Log.Information($"API listening on {urls}"); + Console.WriteLine("Application started. Press Ctrl+C to shut down."); + CreateHostBuilder(args, configuration, !isConsole).Build().Run(); + } + catch (Exception ex) + { + Log.Fatal(ex, "Identity server API host terminated unexpectedly"); + } + finally + { + Log.CloseAndFlush(); + } } - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); + public static IHostBuilder CreateHostBuilder(string[] args, IConfiguration configuration, bool useWindowsService) + { + var builder = Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup() + .UseConfiguration(configuration) + .UseSerilog(); + }); + + if (useWindowsService) + builder.UseWindowsService(); + + return builder; + } } } diff --git a/IdentityServer.Api/Startup.cs b/IdentityServer.Api/Startup.cs index 009c8e3..c0d1f03 100644 --- a/IdentityServer.Api/Startup.cs +++ b/IdentityServer.Api/Startup.cs @@ -1,48 +1,80 @@ +using AutoMapper; +using IdentityServer.Api.Swagger; +using IdentityServer.Application; +using IdentityServer.Domain.Data; +using MediatR; +using MediatR.Pipeline; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using Newtonsoft.Json; +using System.Reflection; namespace IdentityServer.Api { public class Startup { + private readonly IConfiguration _configuration; + public Startup(IConfiguration configuration) { - Configuration = configuration; + _configuration = configuration; } - public IConfiguration Configuration { get; } - // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.AddControllers(); + services.AddControllers() + .AddNewtonsoftJson(o => o.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc); + + // MediatR + services.AddMediatR(GetMediatRAssemblies()); + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestPreProcessorBehavior<,>)); + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestPostProcessorBehavior<,>)); + + // AutoMapper + services.AddAutoMapper( + typeof(Application.Mappings.MappingProfile).Assembly); + + // Swagger + services.AddSwagger(); + + // Data access + services.AddDataAccess(); + + // Application + services.AddApplicationServices(); + } + + private Assembly[] GetMediatRAssemblies() + { + var assembly = typeof(Application.Commands.AuthenticateUser).Assembly; + return new Assembly[] { assembly }; } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { + // global cors policy + app.UseCors(x => x + .AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader()); + if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); - app.UseAuthorization(); - app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + app.ConfigureSwagger(); } } } diff --git a/IdentityServer.Api/Swagger/DtoSchemaFilter.cs b/IdentityServer.Api/Swagger/DtoSchemaFilter.cs new file mode 100644 index 0000000..6dbf302 --- /dev/null +++ b/IdentityServer.Api/Swagger/DtoSchemaFilter.cs @@ -0,0 +1,36 @@ +using IdentityServer.Application.Commands; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; +using System.Linq; + +namespace IdentityServer.Api.Swagger +{ + public class DtoSchemaFilter : ISchemaFilter + { + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + var targetType = context.Type; + while (targetType != null) + { + if (typeof(ICommand).IsAssignableFrom(targetType)) + { + foreach (var property in schema.Properties.ToList()) + { + property.Value.ReadOnly = false; + + switch (property.Key) + { + case "metadata": + schema.Properties.Remove(property.Key); + break; + default: + break; + } + } + } + + targetType = targetType.DeclaringType; + } + } + } +} diff --git a/IdentityServer.Api/Swagger/PathParamsOperationFilter.cs b/IdentityServer.Api/Swagger/PathParamsOperationFilter.cs new file mode 100644 index 0000000..884fffa --- /dev/null +++ b/IdentityServer.Api/Swagger/PathParamsOperationFilter.cs @@ -0,0 +1,34 @@ +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; +using System; +using System.Linq; +using System.Text.RegularExpressions; + +namespace IdentityServer.Api.Swagger +{ + public class PathParamsOperationFilter : IOperationFilter + { + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + const string paramCaptureGroup = "param"; + + var openApiPathParameters = operation.Parameters.Where(param => param.In == ParameterLocation.Path).ToList(); + var pathParamRegEx = $@"\{{(?<{paramCaptureGroup}>[^\}}]+)\}}"; + + if (openApiPathParameters.Any()) + { + var pathParameterMatches = Regex.Matches(context.ApiDescription.RelativePath, pathParamRegEx, RegexOptions.Compiled); + var pathParameters = pathParameterMatches.Select(x => x.Groups[paramCaptureGroup].Value); + + foreach (var openApiPathParameter in openApiPathParameters) + { + var correspondingPathParameter = pathParameters.FirstOrDefault(x => + string.Equals(x, openApiPathParameter.Name, StringComparison.InvariantCultureIgnoreCase)); + + if (correspondingPathParameter != null) + openApiPathParameter.Name = correspondingPathParameter; + } + } + } + } +} diff --git a/IdentityServer.Api/Swagger/SwaggerExtensions.cs b/IdentityServer.Api/Swagger/SwaggerExtensions.cs new file mode 100644 index 0000000..52ec645 --- /dev/null +++ b/IdentityServer.Api/Swagger/SwaggerExtensions.cs @@ -0,0 +1,114 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi.Models; +using System.Collections.Generic; +using System.Linq; + +namespace IdentityServer.Api.Swagger +{ + public static class SwaggerExtensions + { + public static IServiceCollection AddSwagger(this IServiceCollection services) + { + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", + new OpenApiInfo + { + Title = "NetworkResurrector API", + Version = "v1" + }); + + c.AddSecurityDefinition("Basic", + new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Description = @"JWT Authorization header using the Basic scheme. Enter 'Basic' [space] and then your token in the text input below. Example: 'Basic 12345abcdef'", + Name = "Authorization", + Scheme = "Basic", + Type = SecuritySchemeType.ApiKey + }); + + c.AddSecurityRequirement(new OpenApiSecurityRequirement() + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Basic" + }, + Scheme = "Basic", + Name = "Authorization", + In = ParameterLocation.Header + }, + new List() + } + }); + + c.OperationFilter(); + c.SchemaFilter(); + c.CustomSchemaIds(type => type.ToString()); + }); + + return services; + } + + public static IApplicationBuilder ConfigureSwagger(this IApplicationBuilder applicationBuilder) + { + applicationBuilder.UseSwagger(c => + { + c.PreSerializeFilters.Add((swagger, httpRequest) => + { + var (host, basePath, scheme) = GetUrlComponents(httpRequest); + + swagger.Servers = new List + { + new OpenApiServer {Url = $"{scheme}://{host}{basePath}"} + }; + }); + c.RouteTemplate = "swagger/{documentName}/swagger.json"; + }); + + applicationBuilder.UseSwaggerUI(c => + { + c.SwaggerEndpoint("v1/swagger.json", "Chatbot API"); + c.RoutePrefix = $"swagger"; + }); + + return applicationBuilder; + } + + private static (string host, string basePath, string scheme) GetUrlComponents(HttpRequest request) + { + var host = ExtractHost(request); + var basePath = ExtractBasePath(request); + var scheme = ExtractScheme(request); + + return (host, basePath, scheme); + } + + private static string ExtractHost(HttpRequest request) + { + if (request.Headers.ContainsKey("X-Forwarded-Host")) + return request.Headers["X-Forwarded-Host"].First(); + + return request.Host.Value; + } + + private static string ExtractBasePath(HttpRequest request) + { + if (request.Headers.ContainsKey("X-Forwarded-PathBase")) + return request.Headers["X-Forwarded-PathBase"].First(); + + return string.Empty; + } + + private static string ExtractScheme(HttpRequest request) + { + return request.Headers["X-Forwarded-Proto"].FirstOrDefault() ?? request.Scheme; + } + } +}