diff --git a/Endpoint/Configuration/AppConfig/ApiVersioningConfiguration.cs b/Endpoint/Configuration/AppConfig/ApiVersioningConfiguration.cs new file mode 100644 index 0000000..dd35369 --- /dev/null +++ b/Endpoint/Configuration/AppConfig/ApiVersioningConfiguration.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Versioning; +using Microsoft.Extensions.DependencyInjection; + +namespace Mirea.Api.Endpoint.Configuration.AppConfig; + +public static class ApiVersioningConfiguration +{ + public static void AddCustomApiVersioning(this IServiceCollection services) + { + services.AddApiVersioning(options => + { + options.DefaultApiVersion = new ApiVersion(1, 0); + options.AssumeDefaultVersionWhenUnspecified = true; + options.ReportApiVersions = true; + options.ApiVersionReader = new UrlSegmentApiVersionReader(); + }); + + services.AddVersionedApiExplorer(options => + { + options.GroupNameFormat = "'v'VVV"; + options.SubstituteApiVersionInUrl = true; + }); + + services.AddEndpointsApiExplorer(); + } +} \ No newline at end of file diff --git a/Endpoint/Configuration/AppConfig/EnvironmentConfiguration.cs b/Endpoint/Configuration/AppConfig/EnvironmentConfiguration.cs new file mode 100644 index 0000000..80f2fb0 --- /dev/null +++ b/Endpoint/Configuration/AppConfig/EnvironmentConfiguration.cs @@ -0,0 +1,68 @@ +using Microsoft.Extensions.Configuration; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Mirea.Api.Endpoint.Configuration.AppConfig; + +public static class EnvironmentConfiguration +{ + private static IDictionary LoadEnvironment(string envFile) + { + Dictionary environment = new(); + + if (!File.Exists(envFile)) return environment; + + foreach (var line in File.ReadAllLines(envFile)) + { + if (string.IsNullOrEmpty(line)) continue; + + var commentIndex = line.IndexOf('#', StringComparison.Ordinal); + + string arg = line; + + if (commentIndex != -1) + arg = arg.Remove(commentIndex, arg.Length - commentIndex); + + var parts = arg.Split( + '=', + StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length > 2) + parts = [parts[0], string.Join("=", parts[1..])]; + + if (parts.Length != 2) + continue; + + environment.Add(parts[0].Trim(), parts[1].Trim()); + } + + return environment; + } + + public static IConfigurationRoot GetEnvironment() + { + var variablesFromFile = LoadEnvironment(".env"); + + var environmentVariables = Environment.GetEnvironmentVariables() + .OfType() + .ToDictionary( + entry => entry.Key.ToString() ?? string.Empty, + entry => entry.Value?.ToString() ?? string.Empty + ); + + var result = new ConfigurationBuilder() + .AddInMemoryCollection(environmentVariables!) + .AddInMemoryCollection(variablesFromFile!); + +#if DEBUG + result.AddInMemoryCollection(LoadEnvironment(".env.develop")!); +#endif + + Environment.SetEnvironmentVariable("PATH_TO_SAVE", variablesFromFile["PATH_TO_SAVE"]); + + return result.Build(); + } +} \ No newline at end of file diff --git a/Endpoint/Configuration/AppConfig/JwtConfiguration.cs b/Endpoint/Configuration/AppConfig/JwtConfiguration.cs new file mode 100644 index 0000000..213e244 --- /dev/null +++ b/Endpoint/Configuration/AppConfig/JwtConfiguration.cs @@ -0,0 +1,66 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using Mirea.Api.Endpoint.Common.Services.Security; +using Mirea.Api.Security.Common.Interfaces; +using System; +using System.Text; + +namespace Mirea.Api.Endpoint.Configuration.AppConfig; + +public static class JwtConfiguration +{ + public static IServiceCollection AddJwtToken(this IServiceCollection services, IConfiguration configuration) + { + var lifeTimeJwt = TimeSpan.FromMinutes(int.Parse(configuration["SECURITY_LIFE_TIME_JWT"]!)); + + var jwtDecrypt = Encoding.UTF8.GetBytes(configuration["SECURITY_ENCRYPTION_TOKEN"] ?? string.Empty); + + if (jwtDecrypt.Length != 32) + throw new InvalidOperationException("The secret token \"SECURITY_ENCRYPTION_TOKEN\" cannot be less than 32 characters long. Now the size is equal is " + jwtDecrypt.Length); + + var jwtKey = Encoding.UTF8.GetBytes(configuration["SECURITY_SIGNING_TOKEN"] ?? string.Empty); + + if (jwtKey.Length != 64) + throw new InvalidOperationException("The signature token \"SECURITY_SIGNING_TOKEN\" cannot be less than 64 characters. Now the size is " + jwtKey.Length); + + var jwtIssuer = configuration["SECURITY_JWT_ISSUER"]; + var jwtAudience = configuration["SECURITY_JWT_AUDIENCE"]; + + if (string.IsNullOrEmpty(jwtAudience) || string.IsNullOrEmpty(jwtIssuer)) + throw new InvalidOperationException("The \"SECURITY_JWT_ISSUER\" and \"SECURITY_JWT_AUDIENCE\" are not specified"); + + services.AddSingleton(_ => new JwtTokenService + { + Audience = jwtAudience, + Issuer = jwtIssuer, + Lifetime = lifeTimeJwt, + EncryptionKey = jwtDecrypt, + SigningKey = jwtKey + }); + + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = jwtIssuer, + + ValidateAudience = true, + ValidAudience = jwtAudience, + + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(jwtKey), + TokenDecryptionKey = new SymmetricSecurityKey(jwtDecrypt) + }; + }); + + return services; + } +} diff --git a/Endpoint/Configuration/AppConfig/SecureConfiguration.cs b/Endpoint/Configuration/AppConfig/SecureConfiguration.cs new file mode 100644 index 0000000..fb8cd00 --- /dev/null +++ b/Endpoint/Configuration/AppConfig/SecureConfiguration.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Mirea.Api.Endpoint.Common.Services.Security; +using Mirea.Api.Endpoint.Configuration.General.Settings; +using Mirea.Api.Security; +using Mirea.Api.Security.Common.Interfaces; + +namespace Mirea.Api.Endpoint.Configuration.AppConfig; + +public static class SecureConfiguration +{ + public static IServiceCollection AddSecurity(this IServiceCollection services, IConfiguration configuration) + { + services.AddSecurityServices(configuration); + + services.AddSingleton(); + services.AddSingleton(); + + if (configuration.Get()?.TypeDatabase == CacheSettings.CacheEnum.Redis) + services.AddSingleton(); + else + services.AddSingleton(); + + return services; + } +} \ No newline at end of file diff --git a/Endpoint/Configuration/AppConfig/SwaggerConfiguration.cs b/Endpoint/Configuration/AppConfig/SwaggerConfiguration.cs new file mode 100644 index 0000000..13f8e1f --- /dev/null +++ b/Endpoint/Configuration/AppConfig/SwaggerConfiguration.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Mirea.Api.Endpoint.Configuration.Swagger; +using Swashbuckle.AspNetCore.SwaggerGen; +using System; +using System.IO; + +namespace Mirea.Api.Endpoint.Configuration.AppConfig; + +public static class SwaggerConfiguration +{ + public static IServiceCollection AddCustomSwagger(this IServiceCollection services) + { + services.AddSwaggerGen(options => + { + options.SchemaFilter(); + options.OperationFilter(); + var basePath = AppDomain.CurrentDomain.BaseDirectory; + + options.IncludeXmlComments(Path.Combine(basePath, "docs.xml")); + options.IncludeXmlComments(Path.Combine(basePath, "ApiDtoDocs.xml")); + }); + + services.AddTransient, ConfigureSwaggerOptions>(); + + return services; + } + + public static IApplicationBuilder UseCustomSwagger(this IApplicationBuilder app, IServiceProvider services) + { + app.UseSwagger(); + app.UseSwaggerUI(options => + { + var provider = services.GetService(); + + foreach (var description in provider!.ApiVersionDescriptions) + { + var url = $"/swagger/{description.GroupName}/swagger.json"; + var name = description.GroupName.ToUpperInvariant(); + options.SwaggerEndpoint(url, name); + } + }); + + return app; + } +} \ No newline at end of file diff --git a/Endpoint/Configuration/EnvironmentManager.cs b/Endpoint/Configuration/EnvironmentManager.cs deleted file mode 100644 index c21d59e..0000000 --- a/Endpoint/Configuration/EnvironmentManager.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.IO; - -namespace Mirea.Api.Endpoint.Configuration; - -internal static class EnvironmentManager -{ - public static void LoadEnvironment(string envFile) - { - if (!File.Exists(envFile)) return; - - foreach (var line in File.ReadAllLines(envFile)) - { - if (string.IsNullOrEmpty(line)) continue; - - var commentIndex = line.IndexOf('#', StringComparison.Ordinal); - - string arg = line; - - if (commentIndex != -1) - arg = arg.Remove(commentIndex, arg.Length - commentIndex); - - var parts = arg.Split( - '=', - StringSplitOptions.RemoveEmptyEntries); - - if (parts.Length > 2) - parts = [parts[0], string.Join("=", parts[1..])]; - - if (parts.Length != 2) - continue; - - Environment.SetEnvironmentVariable(parts[0].Trim(), parts[1].Trim()); - } - } -} \ No newline at end of file diff --git a/Endpoint/Configuration/Swagger/SwaggerExampleFilter.cs b/Endpoint/Configuration/Swagger/SwaggerExampleFilter.cs new file mode 100644 index 0000000..2af76f4 --- /dev/null +++ b/Endpoint/Configuration/Swagger/SwaggerExampleFilter.cs @@ -0,0 +1,16 @@ +using Microsoft.OpenApi.Models; +using Mirea.Api.Endpoint.Common.Attributes; +using Swashbuckle.AspNetCore.SwaggerGen; +using System.Reflection; + +namespace Mirea.Api.Endpoint.Configuration.Swagger; + +public class SwaggerExampleFilter : ISchemaFilter +{ + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + var att = context.ParameterInfo?.GetCustomAttribute(); + if (att != null) + schema.Example = new Microsoft.OpenApi.Any.OpenApiString(att.Value); + } +} \ No newline at end of file diff --git a/Endpoint/Program.cs b/Endpoint/Program.cs index 87756d9..4dab4df 100644 --- a/Endpoint/Program.cs +++ b/Endpoint/Program.cs @@ -1,173 +1,77 @@ -using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.AspNetCore.Mvc.Versioning; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; using Mirea.Api.DataAccess.Application; using Mirea.Api.DataAccess.Persistence; using Mirea.Api.DataAccess.Persistence.Common; using Mirea.Api.Endpoint.Common.Interfaces; using Mirea.Api.Endpoint.Common.Services; -using Mirea.Api.Endpoint.Common.Services.Security; -using Mirea.Api.Endpoint.Configuration; +using Mirea.Api.Endpoint.Configuration.AppConfig; using Mirea.Api.Endpoint.Configuration.General; using Mirea.Api.Endpoint.Configuration.General.Settings; using Mirea.Api.Endpoint.Configuration.General.Validators; -using Mirea.Api.Endpoint.Configuration.Swagger; using Mirea.Api.Endpoint.Middleware; -using Mirea.Api.Security.Common.Interfaces; -using Swashbuckle.AspNetCore.SwaggerGen; using System; -using System.Collections; using System.IO; -using System.Linq; -using System.Text; namespace Mirea.Api.Endpoint; public class Program { - private static IConfigurationRoot ConfigureEnvironment() + public static IServiceCollection AddDatabase(IServiceCollection services, IConfiguration configuration) { - EnvironmentManager.LoadEnvironment(".env"); - var environmentVariables = Environment.GetEnvironmentVariables() - .OfType() - .ToDictionary( - entry => entry.Key.ToString() ?? string.Empty, - entry => entry.Value?.ToString() ?? string.Empty - ); - - var result = new ConfigurationBuilder().AddInMemoryCollection(environmentVariables!); - - return result.Build(); - } - - private static IServiceCollection ConfigureJwtToken(IServiceCollection services, IConfiguration configuration) - { - var lifeTimeJwt = TimeSpan.FromMinutes(int.Parse(configuration["SECURITY_LIFE_TIME_JWT"]!)); - - var jwtDecrypt = Encoding.UTF8.GetBytes(configuration["SECURITY_ENCRYPTION_TOKEN"] ?? string.Empty); - - if (jwtDecrypt.Length != 32) - throw new InvalidOperationException("The secret token \"SECURITY_ENCRYPTION_TOKEN\" cannot be less than 32 characters long. Now the size is equal is " + jwtDecrypt.Length); - - var jwtKey = Encoding.UTF8.GetBytes(configuration["SECURITY_SIGNING_TOKEN"] ?? string.Empty); - - if (jwtKey.Length != 64) - throw new InvalidOperationException("The signature token \"SECURITY_SIGNING_TOKEN\" cannot be less than 64 characters. Now the size is " + jwtKey.Length); - - var jwtIssuer = configuration["SECURITY_JWT_ISSUER"]; - var jwtAudience = configuration["SECURITY_JWT_AUDIENCE"]; - - if (string.IsNullOrEmpty(jwtAudience) || string.IsNullOrEmpty(jwtIssuer)) - throw new InvalidOperationException("The \"SECURITY_JWT_ISSUER\" and \"SECURITY_JWT_AUDIENCE\" are not specified"); - - services.AddSingleton(_ => new JwtTokenService - { - Audience = jwtAudience, - Issuer = jwtIssuer, - Lifetime = lifeTimeJwt, - EncryptionKey = jwtDecrypt, - SigningKey = jwtKey - }); - - services.AddAuthentication(options => - { - options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; - }).AddJwtBearer(options => - { - options.TokenValidationParameters = new TokenValidationParameters - { - ValidateIssuer = true, - ValidIssuer = jwtIssuer, - - ValidateAudience = true, - ValidAudience = jwtAudience, - - ValidateLifetime = true, - ValidateIssuerSigningKey = true, - IssuerSigningKey = new SymmetricSecurityKey(jwtKey), - TokenDecryptionKey = new SymmetricSecurityKey(jwtDecrypt) - }; - }); + var dbSettings = configuration.Get(); + services.AddApplication(); + services.AddPersistence( + dbSettings?.DatabaseProvider ?? DatabaseProvider.Sqlite, + dbSettings?.ConnectionStringSql ?? string.Empty); return services; } - private static IServiceCollection ConfigureSecurity(IServiceCollection services) - { - services.AddSingleton(); - services.AddSingleton(); - - return services; - } public static void Main(string[] args) { Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory); var builder = WebApplication.CreateBuilder(args); - builder.Configuration.AddConfiguration(ConfigureEnvironment()); + builder.Configuration.AddConfiguration(EnvironmentConfiguration.GetEnvironment()); builder.Configuration.AddJsonFile(PathBuilder.Combine(GeneralConfig.FilePath), optional: true, reloadOnChange: true); builder.Services.Configure(builder.Configuration); - var generalConfig = builder.Configuration.Get(); - builder.Services.AddApplication(); - builder.Services.AddPersistence(generalConfig?.DbSettings?.DatabaseProvider ?? DatabaseProvider.Sqlite, generalConfig?.DbSettings?.ConnectionStringSql ?? string.Empty); + builder.Host.AddCustomSerilog(); + AddDatabase(builder.Services, builder.Configuration); + builder.Services.AddControllers(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + + builder.Services.AddMemoryCache(); + builder.Services.AddCustomRedis(builder.Configuration); + builder.Services.AddCors(options => { options.AddPolicy("AllowAll", policy => { policy.AllowAnyHeader(); policy.AllowAnyMethod(); - policy.AllowAnyOrigin(); + policy.WithOrigins("http://localhost:4200"); + policy.AllowCredentials(); }); }); - builder.Services.AddApiVersioning(options => - { - options.DefaultApiVersion = new ApiVersion(1, 0); - options.AssumeDefaultVersionWhenUnspecified = true; - options.ReportApiVersions = true; - options.ApiVersionReader = new UrlSegmentApiVersionReader(); - }); + builder.Services.AddCustomApiVersioning(); + builder.Services.AddCustomSwagger(); - builder.Services.AddVersionedApiExplorer(options => - { - options.GroupNameFormat = "'v'VVV"; - options.SubstituteApiVersionInUrl = true; - }); - - builder.Services.AddEndpointsApiExplorer(); - - builder.Services.AddSwaggerGen(options => - { - options.OperationFilter(); - var basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory); - - var xmlPath = Path.Combine(basePath, "docs.xml"); - options.IncludeXmlComments(xmlPath); - }); - - builder.Services.AddTransient, ConfigureSwaggerOptions>(); + builder.Services.AddJwtToken(builder.Configuration); + builder.Services.AddSecurity(builder.Configuration); var app = builder.Build(); -#if DEBUG - // Write configurations - foreach (var item in app.Configuration.AsEnumerable()) - Console.WriteLine($"{item.Key}:{item.Value}"); -#endif + app.UseCors("AllowAll"); + app.UseCustomSerilog(); using (var scope = app.Services.CreateScope()) { @@ -184,27 +88,11 @@ public class Program maintenanceModeService.DisableMaintenanceMode(); DbInitializer.Initialize(uberDbContext); - - // todo: if admin not found } } - // Configure the HTTP request pipeline. - if (app.Environment.IsDevelopment()) - { - app.UseSwagger(); - app.UseSwaggerUI(options => - { - var provider = app.Services.GetService(); + app.UseCustomSwagger(app.Services); - foreach (var description in provider!.ApiVersionDescriptions) - { - var url = $"/swagger/{description.GroupName}/swagger.json"; - var name = description.GroupName.ToUpperInvariant(); - options.SwaggerEndpoint(url, name); - } - }); - } app.UseMiddleware(); app.UseMiddleware(); @@ -212,7 +100,6 @@ public class Program app.UseAuthorization(); - app.MapControllers(); app.Run();