diff --git a/Newsbot.Collector.Api/Program.cs b/Newsbot.Collector.Api/Program.cs index 843e449..e3ae5fc 100644 --- a/Newsbot.Collector.Api/Program.cs +++ b/Newsbot.Collector.Api/Program.cs @@ -1,27 +1,13 @@ -using System.Text; using Hangfire; using Hangfire.MemoryStorage; using HealthChecks.UI.Client; -using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Diagnostics.HealthChecks; -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Microsoft.IdentityModel.Tokens; -using Microsoft.OpenApi.Models; -using Newsbot.Collector.Api; -using Newsbot.Collector.Api.Authentication; -using Newsbot.Collector.Api.Domain; -using Newsbot.Collector.Api.Services; -using Newsbot.Collector.Database; -using Newsbot.Collector.Database.Repositories; +using Newsbot.Collector.Api.Startup; using Newsbot.Collector.Domain.Consts; -using Newsbot.Collector.Domain.Entities; -using Newsbot.Collector.Domain.Interfaces; using Newsbot.Collector.Domain.Models; using Newsbot.Collector.Domain.Models.Config; using Newsbot.Collector.Domain.Models.Config.Sources; using Serilog; -using Serilog.Events; using ILogger = Serilog.ILogger; var builder = WebApplication.CreateBuilder(args); @@ -37,26 +23,10 @@ Log.Logger = GetLogger(config); Log.Information("Starting up"); // configure Entity Framework -var dbconn = config.GetConnectionString("Database"); -builder.Services.AddDbContext(o => o.UseNpgsql(dbconn ?? "")); - -// Configure how Identity will be managed -builder.Services.AddIdentity() - .AddRoles() - .AddEntityFrameworkStores(); - // Allow the controllers to access all the table repositories based on the interface -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -// Configure Identity -builder.Services.AddScoped(); +DatabaseStartup.BuildDatabase(builder.Services, config); +DatabaseStartup.InjectTableClasses(builder.Services); +DatabaseStartup.InjectIdentityService(builder.Services); // Configure Hangfire builder.Services.AddHangfire(f => f.UseMemoryStorage()); @@ -70,99 +40,14 @@ builder.Services.AddHealthChecks() builder.Services.AddControllers(); -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +SwaggerStartup.ConfigureSwagger(builder.Services); builder.Services.Configure(config.GetSection("ConnectionStrings")); builder.Services.Configure(config.GetSection(ConfigSectionsConst.ConnectionStrings)); builder.Services.Configure(config.GetSection(ConfigSectionsConst.Rss)); builder.Services.Configure(config.GetSection(ConfigSectionsConst.Youtube)); -// Configure JWT for auth and load it into DI so we can use it in the controllers -var jwtSettings = new JwtSettings(); -config.Bind(nameof(jwtSettings), jwtSettings); -builder.Services.AddSingleton(jwtSettings); - -// Configure how the Token Validation will be handled -var tokenValidationParameters = new TokenValidationParameters -{ - ValidateIssuerSigningKey = true, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtSettings.Secret ?? "")), - ValidateIssuer = false, - ValidateAudience = false, - RequireExpirationTime = false, - ValidateLifetime = true -}; -builder.Services.AddSingleton(tokenValidationParameters); - -// Build the Authentication that will be used -builder.Services.AddAuthentication(x => -{ - x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; - x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; -}).AddJwtBearer(x => -{ - x.SaveToken = true; - x.TokenValidationParameters = tokenValidationParameters; -}); - -// Build the Authorization Policy that the users will conform to. -builder.Services.AddAuthorization(options => - options.AddPolicy(Authorization.AdministratorPolicy, b => b.RequireClaim( Authorization.AdministratorClaim, "true") )); - -// Configure swagger authentication -builder.Services.AddSwaggerGen(cfg => -{ - cfg.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme - { - Description = "The API key to access the API", - Type = SecuritySchemeType.ApiKey, - Name = "x-api-key", - In = ParameterLocation.Header, - Scheme = "ApiKeyScheme" - }); - - cfg.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme - { - Description = "JWT Authorization Header using the bearer scheme", - Name = "Authorization", - In = ParameterLocation.Header, - Type = SecuritySchemeType.ApiKey - }); - - cfg.AddSecurityRequirement(new OpenApiSecurityRequirement - { - //{ - // new OpenApiSecurityScheme - // { - // Reference = new OpenApiReference - // { - // Type = ReferenceType.SecurityScheme, - // Id = "ApiKey" - // }, - // In = ParameterLocation.Header - // }, - // new List() - //}, - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = "Bearer" - }, - Scheme = "oauth2", - Name = "Bearer", - In = ParameterLocation.Header - }, - new List() - } - }); -}); - +IdentityStartup.DefineJwtRequirements(builder.Services, config); var app = builder.Build(); @@ -199,9 +84,7 @@ app.MapControllers(); using var serviceScope = app.Services.CreateScope(); if (config.GetValue(ConfigConst.RunDatabaseMigrationsOnStartup)) { - var dbContext = serviceScope.ServiceProvider.GetRequiredService(); - dbContext.Database.Migrate(); - + await DatabaseStartup.RunDatabaseMigrationsAsync(serviceScope); } else { @@ -209,18 +92,7 @@ else } // Inject the roles -var roleManager = serviceScope.ServiceProvider.GetRequiredService>(); -if (!await roleManager.RoleExistsAsync("Administrators")) -{ - var adminRole = new IdentityRole("Administrators"); - await roleManager.CreateAsync(adminRole); -} - -if (!await roleManager.RoleExistsAsync("Users")) -{ - var userRole = new IdentityRole("Users"); - await roleManager.CreateAsync(userRole); -} +await DatabaseStartup.InjectIdentityRolesAsync(serviceScope); // Start the application app.Run(); diff --git a/Newsbot.Collector.Api/BackgroundJobs.cs b/Newsbot.Collector.Api/Startup/BackgroundJobs.cs similarity index 98% rename from Newsbot.Collector.Api/BackgroundJobs.cs rename to Newsbot.Collector.Api/Startup/BackgroundJobs.cs index e1e6892..8f6fc02 100644 --- a/Newsbot.Collector.Api/BackgroundJobs.cs +++ b/Newsbot.Collector.Api/Startup/BackgroundJobs.cs @@ -3,7 +3,7 @@ using Newsbot.Collector.Domain.Consts; using Newsbot.Collector.Domain.Models.Config; using Newsbot.Collector.Services.Jobs; -namespace Newsbot.Collector.Api; +namespace Newsbot.Collector.Api.Startup; public static class BackgroundJobs { diff --git a/Newsbot.Collector.Api/Startup/DatabaseStartup.cs b/Newsbot.Collector.Api/Startup/DatabaseStartup.cs new file mode 100644 index 0000000..04495ae --- /dev/null +++ b/Newsbot.Collector.Api/Startup/DatabaseStartup.cs @@ -0,0 +1,61 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Newsbot.Collector.Api.Domain; +using Newsbot.Collector.Api.Services; +using Newsbot.Collector.Database; +using Newsbot.Collector.Database.Repositories; +using Newsbot.Collector.Domain.Interfaces; + +namespace Newsbot.Collector.Api.Startup; + +public class DatabaseStartup +{ + public static void BuildDatabase(IServiceCollection services, IConfiguration config) + { + var dbconn = config.GetConnectionString("Database"); + services.AddDbContext(o => o.UseNpgsql(dbconn ?? "")); + + // Add identity to our ef connection + services.AddIdentity() + .AddRoles() + .AddEntityFrameworkStores(); + } + + public static void InjectTableClasses(IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } + + public static void InjectIdentityService(IServiceCollection services) + { + // Configure Identity + services.AddScoped(); + } + + public static async Task RunDatabaseMigrationsAsync(IServiceScope serviceScope) + { + var dbContext = serviceScope.ServiceProvider.GetRequiredService(); + await dbContext.Database.MigrateAsync(); + } + + public static async Task InjectIdentityRolesAsync(IServiceScope serviceScope) + { + var roleManager = serviceScope.ServiceProvider.GetRequiredService>(); + if (!await roleManager.RoleExistsAsync(Authorization.AdministratorsRole)) + { + await roleManager.CreateAsync(new IdentityRole(Authorization.AdministratorsRole)); + } + + if (!await roleManager.RoleExistsAsync(Authorization.UsersRole)) + { + await roleManager.CreateAsync(new IdentityRole(Authorization.UsersRole)); + } + } +} \ No newline at end of file diff --git a/Newsbot.Collector.Api/Startup/IdentityStartup.cs b/Newsbot.Collector.Api/Startup/IdentityStartup.cs new file mode 100644 index 0000000..f233b6d --- /dev/null +++ b/Newsbot.Collector.Api/Startup/IdentityStartup.cs @@ -0,0 +1,50 @@ +using System.Text; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using Newsbot.Collector.Api.Domain; +using Newsbot.Collector.Domain.Models.Config; + +namespace Newsbot.Collector.Api.Startup; + +public static class IdentityStartup +{ + public static void DefineJwtRequirements(IServiceCollection services, IConfiguration config) + { + // Configure JWT for auth and load it into DI so we can use it in the controllers + + var jwtSettings = new JwtSettings(); + config.Bind(nameof(jwtSettings), jwtSettings); + services.AddSingleton(jwtSettings); + + // Configure how the Token Validation will be handled + var tokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtSettings.Secret ?? "")), + ValidateIssuer = false, + ValidateAudience = false, + RequireExpirationTime = false, + ValidateLifetime = true + }; + services.AddSingleton(tokenValidationParameters); + + // Build the Authentication that will be used + services.AddAuthentication(x => + { + x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddJwtBearer(x => + { + x.SaveToken = true; + x.TokenValidationParameters = tokenValidationParameters; + }); + + // Build the Authorization Policy that the users will conform to. + services.AddAuthorization(options => + { + options.AddPolicy(Authorization.AdministratorPolicy, + b => b.RequireClaim(Authorization.AdministratorClaim, "true")); + }); + } +} \ No newline at end of file diff --git a/Newsbot.Collector.Api/Startup/SwaggerStartup.cs b/Newsbot.Collector.Api/Startup/SwaggerStartup.cs new file mode 100644 index 0000000..3854979 --- /dev/null +++ b/Newsbot.Collector.Api/Startup/SwaggerStartup.cs @@ -0,0 +1,67 @@ +using Microsoft.OpenApi.Models; + +namespace Newsbot.Collector.Api.Startup; + +public static class SwaggerStartup +{ + public static void ConfigureSwagger(IServiceCollection services) + { + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(); + + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(); + + // Configure swagger authentication + services.AddSwaggerGen(cfg => + { + cfg.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme + { + Description = "The API key to access the API", + Type = SecuritySchemeType.ApiKey, + Name = "x-api-key", + In = ParameterLocation.Header, + Scheme = "ApiKeyScheme" + }); + + cfg.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = "JWT Authorization Header using the bearer scheme", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey + }); + + cfg.AddSecurityRequirement(new OpenApiSecurityRequirement + { + //{ + // new OpenApiSecurityScheme + // { + // Reference = new OpenApiReference + // { + // Type = ReferenceType.SecurityScheme, + // Id = "ApiKey" + // }, + // In = ParameterLocation.Header + // }, + // new List() + //}, + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + }, + Scheme = "oauth2", + Name = "Bearer", + In = ParameterLocation.Header + }, + new List() + } + }); + }); + } +} \ No newline at end of file