diff --git a/src/Infrastructure/Dependencies.cs b/src/Infrastructure/Dependencies.cs index 96456763..6ef27704 100644 --- a/src/Infrastructure/Dependencies.cs +++ b/src/Infrastructure/Dependencies.cs @@ -8,7 +8,7 @@ namespace Microsoft.eShopWeb.Infrastructure; public static class Dependencies { - public static void ConfigureServices(IConfiguration configuration, IServiceCollection services) + public static void ConfigureServices(this IServiceCollection services, IConfiguration configuration) { bool useOnlyInMemoryDatabase = false; if (configuration["UseOnlyInMemoryDatabase"] != null) diff --git a/src/PublicApi/Extensions/ServiceCollectionExtensions.cs b/src/PublicApi/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..b2273c00 --- /dev/null +++ b/src/PublicApi/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,107 @@ +using System.Collections.Generic; +using System.Text; +using BlazorShared; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.eShopWeb.ApplicationCore.Constants; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using Microsoft.eShopWeb.ApplicationCore.Services; +using Microsoft.eShopWeb.Infrastructure.Data; +using Microsoft.eShopWeb.Infrastructure.Identity; +using Microsoft.eShopWeb.Infrastructure.Logging; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; + +namespace Microsoft.eShopWeb.PublicApi.Extensions; + +public static class ServiceCollectionExtensions +{ + public static void AddCustomServices(this IServiceCollection services, ConfigurationManager configuration) + { + services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)); + services.AddScoped(typeof(IReadRepository<>), typeof(EfRepository<>)); + services.Configure(configuration); + + var catalogSettings = configuration.Get() ?? new CatalogSettings(); + services.AddSingleton(new UriComposer(catalogSettings)); + services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>)); + services.AddScoped(); + } + + public static void AddJwtAuthentication(this IServiceCollection services) + { + var key = Encoding.ASCII.GetBytes(AuthorizationConstants.JWT_SECRET_KEY); + + services.AddAuthentication(config => + { + config.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(config => + { + config.RequireHttpsMetadata = false; + config.SaveToken = true; + config.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = false, + ValidateAudience = false + }; + }); + } + + public static void AddCorsPolicy(this IServiceCollection services, string policyName, BaseUrlConfiguration baseUrlConfig) + { + services.AddCors(options => + { + options.AddPolicy(name: policyName, + corsPolicyBuilder => + { + corsPolicyBuilder.WithOrigins(baseUrlConfig!.WebBase.Replace("host.docker.internal", "localhost").TrimEnd('/')); + corsPolicyBuilder.AllowAnyMethod(); + corsPolicyBuilder.AllowAnyHeader(); + }); + }); + } + + public static void AddSwagger(this IServiceCollection services) + { + // TODO: Update to use FastEndpoints approach to Swagger + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + c.EnableAnnotations(); + c.SchemaFilter(); + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = @"JWT Authorization header using the Bearer scheme. \r\n\r\n + Enter 'Bearer' [space] and then your token in the text input below. + \r\n\r\nExample: 'Bearer 12345abcdef'", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey, + Scheme = "Bearer" + }); + + c.AddSecurityRequirement(new OpenApiSecurityRequirement() + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + }, + Scheme = "oauth2", + Name = "Bearer", + In = ParameterLocation.Header, + }, + new List() + } + }); + }); + } +} diff --git a/src/PublicApi/Extensions/WebApplicationExtensions.cs b/src/PublicApi/Extensions/WebApplicationExtensions.cs new file mode 100644 index 00000000..73940885 --- /dev/null +++ b/src/PublicApi/Extensions/WebApplicationExtensions.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Identity; +using Microsoft.eShopWeb.Infrastructure.Data; +using Microsoft.eShopWeb.Infrastructure.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; + +namespace Microsoft.eShopWeb.PublicApi.Extensions; + +public static class WebApplicationExtensions +{ + public static async Task SeedDatabaseAsync(this WebApplication app) + { + app.Logger.LogInformation("Seeding Database..."); + + using var scope = app.Services.CreateScope(); + var scopedProvider = scope.ServiceProvider; + try + { + var catalogContext = scopedProvider.GetRequiredService(); + await CatalogContextSeed.SeedAsync(catalogContext, app.Logger); + + var userManager = scopedProvider.GetRequiredService>(); + var roleManager = scopedProvider.GetRequiredService>(); + var identityContext = scopedProvider.GetRequiredService(); + await AppIdentityDbContextSeed.SeedAsync(identityContext, userManager, roleManager); + } + catch (Exception ex) + { + app.Logger.LogError(ex, "An error occurred seeding the DB."); + } + } +} diff --git a/src/PublicApi/Program.cs b/src/PublicApi/Program.cs index f643180b..fde4edf8 100644 --- a/src/PublicApi/Program.cs +++ b/src/PublicApi/Program.cs @@ -1,25 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Text; -using BlazorShared; -using Microsoft.AspNetCore.Authentication.JwtBearer; +using BlazorShared; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Identity; -using Microsoft.eShopWeb; -using Microsoft.eShopWeb.ApplicationCore.Constants; -using Microsoft.eShopWeb.ApplicationCore.Interfaces; -using Microsoft.eShopWeb.ApplicationCore.Services; -using Microsoft.eShopWeb.Infrastructure.Data; +using Microsoft.eShopWeb.Infrastructure; using Microsoft.eShopWeb.Infrastructure.Identity; -using Microsoft.eShopWeb.Infrastructure.Logging; using Microsoft.eShopWeb.PublicApi; +using Microsoft.eShopWeb.PublicApi.Extensions; using Microsoft.eShopWeb.PublicApi.Middleware; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.Tokens; -using Microsoft.OpenApi.Models; using MinimalApi.Endpoint.Configurations.Extensions; using MinimalApi.Endpoint.Extensions; @@ -29,132 +19,38 @@ // Use to force loading of appsettings.json of test project builder.Configuration.AddConfigurationFile("appsettings.test.json"); -builder.Logging.AddConsole(); -Microsoft.eShopWeb.Infrastructure.Dependencies.ConfigureServices(builder.Configuration, builder.Services); +builder.Services.ConfigureServices(builder.Configuration); builder.Services.AddIdentity() .AddEntityFrameworkStores() .AddDefaultTokenProviders(); -builder.Services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)); -builder.Services.AddScoped(typeof(IReadRepository<>), typeof(EfRepository<>)); -builder.Services.Configure(builder.Configuration); -var catalogSettings = builder.Configuration.Get() ?? new CatalogSettings(); -builder.Services.AddSingleton(new UriComposer(catalogSettings)); -builder.Services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>)); -builder.Services.AddScoped(); - -var configSection = builder.Configuration.GetRequiredSection(BaseUrlConfiguration.CONFIG_NAME); -builder.Services.Configure(configSection); -var baseUrlConfig = configSection.Get(); +builder.Services.AddCustomServices(builder.Configuration); builder.Services.AddMemoryCache(); -// TODO: Move to extension method to wire up auth -var key = Encoding.ASCII.GetBytes(AuthorizationConstants.JWT_SECRET_KEY); -builder.Services.AddAuthentication(config => -{ - config.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; -}) -.AddJwtBearer(config => -{ - config.RequireHttpsMetadata = false; - config.SaveToken = true; - config.TokenValidationParameters = new TokenValidationParameters - { - ValidateIssuerSigningKey = true, - IssuerSigningKey = new SymmetricSecurityKey(key), - ValidateIssuer = false, - ValidateAudience = false - }; -}); +builder.Services.AddJwtAuthentication(); -// TODO: Move to extension method to wire up CORS const string CORS_POLICY = "CorsPolicy"; -builder.Services.AddCors(options => -{ - options.AddPolicy(name: CORS_POLICY, - corsPolicyBuilder => - { - corsPolicyBuilder.WithOrigins(baseUrlConfig!.WebBase.Replace("host.docker.internal", "localhost").TrimEnd('/')); - corsPolicyBuilder.AllowAnyMethod(); - corsPolicyBuilder.AllowAnyHeader(); - }); -}); + +var configSection = builder.Configuration.GetRequiredSection(BaseUrlConfiguration.CONFIG_NAME); +builder.Services.Configure(configSection); +var baseUrlConfig = configSection.Get(); +builder.Services.AddCorsPolicy(CORS_POLICY, baseUrlConfig!); builder.Services.AddControllers(); // TODO: Consider removing AutoMapper dependency (FastEndpoints already has its own Mapper support) builder.Services.AddAutoMapper(typeof(MappingProfile).Assembly); -// TODO: Do we need this? This should work by default -builder.Configuration.AddEnvironmentVariables(); - -// TODO: Update to use FastEndpoints approach to Swagger -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(c => -{ - c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); - c.EnableAnnotations(); - c.SchemaFilter(); - c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme - { - Description = @"JWT Authorization header using the Bearer scheme. \r\n\r\n - Enter 'Bearer' [space] and then your token in the text input below. - \r\n\r\nExample: 'Bearer 12345abcdef'", - Name = "Authorization", - In = ParameterLocation.Header, - Type = SecuritySchemeType.ApiKey, - Scheme = "Bearer" - }); - - c.AddSecurityRequirement(new OpenApiSecurityRequirement() - { - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = "Bearer" - }, - Scheme = "oauth2", - Name = "Bearer", - In = ParameterLocation.Header, - - }, - new List() - } - }); -}); +builder.Services.AddSwagger(); var app = builder.Build(); app.Logger.LogInformation("PublicApi App created..."); - -// TODO: Move to extension method to seed database -app.Logger.LogInformation("Seeding Database..."); - -using (var scope = app.Services.CreateScope()) -{ - var scopedProvider = scope.ServiceProvider; - try - { - var catalogContext = scopedProvider.GetRequiredService(); - await CatalogContextSeed.SeedAsync(catalogContext, app.Logger); - - var userManager = scopedProvider.GetRequiredService>(); - var roleManager = scopedProvider.GetRequiredService>(); - var identityContext = scopedProvider.GetRequiredService(); - await AppIdentityDbContextSeed.SeedAsync(identityContext, userManager, roleManager); - } - catch (Exception ex) - { - app.Logger.LogError(ex, "An error occurred seeding the DB."); - } -} +await app.SeedDatabaseAsync(); if (app.Environment.IsDevelopment()) { diff --git a/src/Web/Program.cs b/src/Web/Program.cs index d66c145a..725ec549 100644 --- a/src/Web/Program.cs +++ b/src/Web/Program.cs @@ -11,6 +11,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.eShopWeb; using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using Microsoft.eShopWeb.Infrastructure; using Microsoft.eShopWeb.Infrastructure.Data; using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.eShopWeb.Web; @@ -23,7 +24,7 @@ if (builder.Environment.IsDevelopment() || builder.Environment.EnvironmentName == "Docker"){ // Configure SQL Server (local) - Microsoft.eShopWeb.Infrastructure.Dependencies.ConfigureServices(builder.Configuration, builder.Services); + builder.Services.ConfigureServices(builder.Configuration); } else{ // Configure SQL Server (prod)