using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using Microsoft.Extensions.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.OpenApi.Models; using AyaNova.Models; using AyaNova.Util; using AyaNova.Generator; using AyaNova.Biz; using Swashbuckle.AspNetCore.Swagger; using Swashbuckle.AspNetCore.SwaggerGen; using Swashbuckle.AspNetCore.SwaggerUI; using System.IO; using System.Reflection; using System.Linq; using System; using System.Collections.Generic; using Newtonsoft.Json.Serialization; namespace AyaNova { public class Startup { ///////////////////////////////////////////////////////////// // public Startup(ILogger logger, ILoggerFactory logFactory, Microsoft.AspNetCore.Hosting.IWebHostEnvironment hostingEnvironment) { _log = logger; _hostingEnvironment = hostingEnvironment; AyaNova.Util.ApplicationLogging.LoggerFactory = logFactory; //this must be set here ServerBootConfig.AYANOVA_CONTENT_ROOT_PATH = hostingEnvironment.ContentRootPath; } private readonly ILogger _log; private string _connectionString = ""; private readonly Microsoft.AspNetCore.Hosting.IWebHostEnvironment _hostingEnvironment; //////////////////////////////////////////////////////////// // This method gets called by the runtime. Use this method to add services to the container. // public void ConfigureServices(IServiceCollection services) { _log.LogDebug("BOOT: initializing services..."); //dotnet 3.x added this wasn't here before services.AddControllers(); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); }); //Server state service for shutting people out of api _log.LogDebug("BOOT: init ApiServerState service"); services.AddSingleton(new AyaNova.Api.ControllerHelpers.ApiServerState()); //dotnet 3 commented this out, new project doesn't have it //Init mvc // _log.LogDebug("BOOT: init MVC Core service"); // var mvc = services.AddMvcCore(); // _log.LogDebug("BOOT: add json service"); // mvc.AddNewtonsoftJson(); // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service // note: the specified format code will format the version as "'v'major[.minor][-status]" _log.LogDebug("BOOT: init ApiExplorer service"); // services.AddVersionedApiExplorer(o => o.GroupNameFormat = "'v'VVV"); _log.LogDebug("BOOT: ensuring user and backup folders exist and are separate locations..."); FileUtil.EnsureUserAndUtilityFoldersExistAndAreNotIdentical(_hostingEnvironment.ContentRootPath); #region DATABASE _connectionString = ServerBootConfig.AYANOVA_DB_CONNECTION; //Check DB server exists and can be connected to _log.LogDebug("BOOT: Testing database server connection..."); //parse the connection string properly DbUtil.ParseConnectionString(_log, _connectionString); //Probe for database server //Will retry every 10 seconds for up to 5 minutes before bailing if (!DbUtil.DatabaseServerExists(_log, "BOOT: waiting for db server ")) { var err = $"BOOT: E1000 - AyaNova can't connect to the database server after trying for 5 minutes (connection string is:\"{DbUtil.DisplayableConnectionString}\")"; _log.LogCritical(err); throw new System.ApplicationException(err); } _log.LogInformation("BOOT: Connected to database server - {0}", DbUtil.DisplayableConnectionString); //ensure database is ready and present DbUtil.EnsureDatabaseExists(_log); bool LOG_SENSITIVE_DATA = false; #if (DEBUG) // LOG_SENSITIVE_DATA = true; #endif _log.LogDebug("BOOT: init EF service"); services.AddEntityFrameworkNpgsql().AddDbContext( options => options.UseNpgsql(_connectionString //,opt => opt.EnableRetryOnFailure()//REMOVED THIS BECAUSE IT WAS INTEFERING WITH TRANSACTIONS BUT THEN DIDN'T USE THE TRANSACTION BUT IT SEEMS FASTER WITHOUT IT AS WELL SO...?? )//http://www.npgsql.org/efcore/misc.html?q=execution%20strategy#execution-strategy .ConfigureWarnings(warnings => //https://livebook.manning.com/#!/book/entity-framework-core-in-action/chapter-12/v-10/85 warnings.Throw( //Throw an exception on client eval, not necessarily an error but a smell // Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.QueryClientEvaluationWarning )) .EnableSensitiveDataLogging(LOG_SENSITIVE_DATA) ); #endregion _log.LogDebug("BOOT: init ApiVersioning service"); // services // .AddApiVersioning(options => // { // options.AssumeDefaultVersionWhenUnspecified = true; // options.DefaultApiVersion = Microsoft.AspNetCore.Mvc.ApiVersion.Parse("8.0"); // options.ReportApiVersions = true; // }); //v3 commented out to try to figure out why the swagger docs aren't generating and may be superfluous // // Add service and create Policy with options // _log.LogDebug("BOOT: init CORS service"); // services.AddCors(options => // { // options.AddPolicy("CorsPolicy", // builder => builder.AllowAnyOrigin() // .AllowAnyMethod() // .AllowAnyHeader() // //.AllowCredentials() // ); // }); _log.LogDebug("BOOT: init MVC service"); _log.LogDebug("BOOT: init Metrics service"); // services.AddMvc(config => // { // //was this but needed logging, not certain about the new way of adding so keeping this in case it all goes sideways in testing // //config.Filters.Add(typeof(AyaNova.Api.ControllerHelpers.ApiCustomExceptionFilter)); // config.Filters.Add(new AyaNova.Api.ControllerHelpers.ApiCustomExceptionFilter(AyaNova.Util.ApplicationLogging.LoggerFactory)); // }).AddMetrics().AddJsonOptions(options => // { // //2019-10-15 - removed this due to fuckery after update, is it required??? Not sure at this point // //if metrics have wrong dates then it's important I guess // //options.JsonSerializerOptions.DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Utc; // }); #region Swagger //https://docs.microsoft.com/en-us/aspnet/core/tutorials/web-api-help-pages-using-swagger?tabs=visual-studio-code //https://swagger.io/ //https://github.com/domaindrivendev/Swashbuckle.AspNetCore _log.LogDebug("BOOT: init API explorer service"); // services.AddSwaggerGen(c => // { // c.SwaggerDoc("v8", new OpenApiInfo { Title = "My API", Version = "v8" }); // c.OperationFilter(); // }); // services.AddSwaggerGen( // c => // { // c.DocInclusionPredicate((docName, apiDesc) => // { // if (!apiDesc.TryGetMethodInfo(out MethodInfo methodInfo)) return false; // // Get the MapToApiVersion attributes of the action // var mapApiVersions = methodInfo // .GetCustomAttributes(true) // .OfType() // .SelectMany(attr => attr.Versions); // //if it contains MapToApiVersion attributes, then we should check those as the ApiVersion ones are ignored // if (mapApiVersions.Any() && mapApiVersions.Any(v => $"v{v.ToString()}" == docName)) // return true; // // Get the ApiVersion attributes of the controller // var versions = methodInfo.DeclaringType // .GetCustomAttributes(true) // .OfType() // .SelectMany(attr => attr.Versions); // return versions.Any(v => $"v{v.ToString()}" == docName); // }); // // resolve the IApiVersionDescriptionProvider service // // note: that we have to build a temporary service provider here because one has not been created yet // var provider = services.BuildServiceProvider().GetRequiredService(); // // add a swagger document for each discovered API version // // note: you might choose to skip or document deprecated API versions differently // foreach (var description in provider.ApiVersionDescriptions) // { // c.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description)); // } // // add a custom operation filter which sets default values // // c.OperationFilter(); // //test filter WTF? // //c.OperationFilter(); // // integrate xml comments // c.IncludeXmlComments(XmlCommentsFilePath); // // //2019-10-15 - Removed this because apikeyscheme is no longer recognized and it appears it *may* not be necessary... TWT // // //If I have any issues with bearer tokens in swagger then this is probably necessary but in a new way // // //this is required to allow authentication when testing secure routes via swagger UI // // c.AddSecurityDefinition("Bearer", new ApiKeyScheme // // { // // Description = "JWT Authorization header using the Bearer scheme. Get your token by logging in via the Auth route then enter it here with the \"Bearer \" prefix. Example: \"Bearer {token}\"", // // Name = "Authorization", // // In = "header", // // Type = "apiKey" // // }); // //Obsolete way // // c.AddSecurityRequirement(new System.Collections.Generic.Dictionary> // // { // // { "Bearer", new string[] { } } // // }); // //https://stackoverflow.com/questions/56234504/migrating-to-swashbuckle-aspnetcore-version-5 // //First we define the security scheme // c.AddSecurityDefinition("Bearer", //Name the security scheme // new OpenApiSecurityScheme // { // Description = "JWT Authorization header using the Bearer scheme.", // Type = SecuritySchemeType.Http, //We set the scheme type to http since we're using bearer authentication // Scheme = "bearer" //The name of the HTTP Authorization scheme to be used in the Authorization header. In this case "bearer". // }); // c.AddSecurityRequirement(new OpenApiSecurityRequirement{ // { // new OpenApiSecurityScheme{ // Reference = new OpenApiReference{ // Id = "Bearer", //The name of the previously defined security scheme. // Type = ReferenceType.SecurityScheme // } // },new List() // } // }); // //https://github.com/domaindrivendev/Swashbuckle.AspNetCore // //ARGGHHHHHHHH!!!!! // }); #endregion #region JWT AUTHENTICATION //get the key if specified var secretKey = ServerBootConfig.AYANOVA_JWT_SECRET; //If no key specified make a unique one //This means the jwt creds won't survive a server reboot //so in that case users need to specify an AyaNova_JWT_SECRET environment variable if (string.IsNullOrWhiteSpace(secretKey)) { secretKey = Util.Hasher.GenerateSalt(); } //WAS "UNLICENSED5G*QQJ8#bQ7$Xr_@sXfHq4" //If secretKey is less than 32 characters, pad it if (secretKey.Length < 32) { secretKey = secretKey.PadRight(32, '-'); } ServerBootConfig.AYANOVA_JWT_SECRET = secretKey; var signingKey = new SymmetricSecurityKey(System.Text.Encoding.ASCII.GetBytes(ServerBootConfig.AYANOVA_JWT_SECRET)); _log.LogDebug("BOOT: init Authorization service"); services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(options => { // options.AutomaticAuthenticate = true; // options.AutomaticChallenge = true; options.TokenValidationParameters = new TokenValidationParameters { // Token signature will be verified using a private key. ValidateIssuerSigningKey = true, RequireSignedTokens = true, IssuerSigningKey = signingKey, ValidateIssuer = true, ValidIssuer = "ayanova.com", ValidateAudience = false, //ValidAudience = "http://localhost:7575/" // Token will only be valid if not expired yet, with 5 minutes clock skew. ValidateLifetime = true, RequireExpirationTime = true, ClockSkew = new TimeSpan(0, 5, 0), }; }); #endregion _log.LogDebug("BOOT: init Generator service"); services.AddSingleton(); } //////////////////////////////////////////////////////////// // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IWebHostEnvironment env, AyContext dbContext, AyaNova.Api.ControllerHelpers.ApiServerState apiServerState, IServiceProvider serviceProvider) { //This was in constructor right after dbcontext: IApiVersionDescriptionProvider provider, _log.LogDebug("BOOT: configuring request pipeline..."); //dotnet3 test added if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } //This is in new templates generated for webapi but it errors out for me so removing it for now // app.UseHttpsRedirection(); //Store a reference to the dependency injection service for static classes ServiceProviderProvider.Provider = app.ApplicationServices; //Enable ability to handle reverse proxy app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedFor | Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedProto }); #region STATIC FILES _log.LogDebug("BOOT: pipeline - static files"); app.UseDefaultFiles(); app.UseStaticFiles(); #endregion _log.LogDebug("BOOT: pipeline - ROUTING"); app.UseRouting();//this wasn't here for 2.2 but added for 3.0, needs to come before the stuff after //v3test // _log.LogDebug("BOOT: pipeline - CORS"); // app.UseCors("CorsPolicy"); #region AUTH / ROLES _log.LogDebug("BOOT: pipeline - authentication"); //Use authentication middleware app.UseAuthentication(); _log.LogDebug("BOOT: pipeline - authorization"); app.UseAuthorization(); //Custom middleware to get user roles and put them into the request so //they can be authorized in routes. app.Use(async (context, next) => { if (!context.User.Identity.IsAuthenticated) { context.Request.HttpContext.Items["AY_ROLES"] = 0; } else { //Get user ID from claims long userId = Convert.ToInt64(context.User.FindFirst(c => c.Type == "id").Value); //Get the database context var ct = context.RequestServices.GetService(); //get the user record var u = ct.User.AsNoTracking().Where(a => a.Id == userId).Select(m => new { roles = m.Roles, name = m.Name, id = m.Id, localeId = m.LocaleId }).First(); context.Request.HttpContext.Items["AY_ROLES"] = u.roles; context.Request.HttpContext.Items["AY_USERNAME"] = u.name; context.Request.HttpContext.Items["AY_USER_ID"] = u.id; context.Request.HttpContext.Items["AY_LOCALE_ID"] = u.localeId; } await next.Invoke(); }); #endregion //According to https://docs.microsoft.com/en-us/aspnet/core/migration/22-to-30?view=aspnetcore-2.2&tabs=visual-studio#migrate-startupconfigure _log.LogDebug("BOOT: pipeline - ENDPOINTS"); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); #region SWAGGER _log.LogDebug("BOOT: pipeline - api explorer"); // // Enable middleware to serve generated Swagger as a JSON endpoint. // app.UseSwagger(); // app.UseSwaggerUI(c => // { // // build a swagger endpoint for each discovered API version // foreach (var description in provider.ApiVersionDescriptions) // { // c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant()); // } // //clean up the swagger explorer UI page and remove the branding // //via our own css // //NOTE: this broke when updated to v2.x of swagger and it can be fixed according to docs: // //https://github.com/domaindrivendev/Swashbuckle.AspNetCore#inject-custom-css // // c.InjectStylesheet("/api/sw.css"); // c.DefaultModelsExpandDepth(-1); // c.DocumentTitle = "AyaNova API explorer"; // c.RoutePrefix = "api-docs"; // }); app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); c.DefaultModelsExpandDepth(-1); c.DocumentTitle = "AyaNova API explorer"; c.RoutePrefix = "api-docs"; }); #endregion swagger // ****************************************************************** // ******************** TESTING WIPE DB ***************************** // //Set this to true to wipe the db and reinstall a trial license and re-seed the data var TESTING_REFRESH_DB = false;//####################################################################################### #if (DEBUG) //TESTING if (TESTING_REFRESH_DB) ServerBootConfig.AYANOVA_PERMANENTLY_ERASE_DATABASE = TESTING_REFRESH_DB; //TESTING #endif if (ServerBootConfig.AYANOVA_PERMANENTLY_ERASE_DATABASE) { _log.LogWarning("BOOT: AYANOVA_PERMANENTLY_ERASE_DATABASE is true, dropping and recreating database"); Util.DbUtil.DropAndRecreateDb(_log); AySchema.CheckAndUpdate(dbContext, _log); } //Check schema _log.LogDebug("BOOT: db schema check"); AySchema.CheckAndUpdate(dbContext, _log); //Check database integrity _log.LogDebug("BOOT: db integrity check"); DbUtil.CheckFingerPrint(AySchema.EXPECTED_COLUMN_COUNT, AySchema.EXPECTED_INDEX_COUNT, _log); //Initialize license AyaNova.Core.License.Initialize(apiServerState, dbContext, _log); //Ensure locales are present, not missing any keys and that there is a server default locale that exists LocaleBiz lb = new LocaleBiz(dbContext, 1, ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID, AuthorizationRoles.OpsAdminFull); lb.ValidateLocales(); #if (DEBUG) //TESTING if (TESTING_REFRESH_DB) { AyaNova.Core.License.Fetch(apiServerState, dbContext, _log); Util.Seeder.SeedDatabase(Util.Seeder.SeedLevel.SmallOneManShopTrialDataSet, -7);//############################################################################################# } //TESTING #endif //AUTOID VALUES INITIALIZATION ServerBootConfig.SetMostRecentAutoIdValuesFromDatabase(dbContext); //SPA FALLBACK ROUTE app.Use(async (context, next) => { //to support html5 pushstate routing in spa //this ensures that a refresh at the client will not 404 but rather force back to the index.html app page and then handled internally by the client await next(); if (!context.Response.HasStarted && context.Request.Path.Value != "/docs" && context.Response.StatusCode == 404 && !Path.HasExtension(context.Request.Path.Value)) { context.Request.Path = "/index.html"; context.Response.StatusCode = 200; context.Response.ContentType = "text/html"; await context.Response.SendFileAsync(Path.Combine(env.WebRootPath, "index.html")); } }); //Log the active user count so it's in the log record _log.LogInformation($"BOOT: Active techs - {UserBiz.ActiveCount}"); //Log the license info so it's on the record _log.LogInformation($"BOOT: License -\r\n=-=-=-=-=-=-=-=-=-=-\r\n{AyaNova.Core.License.LicenseInfo}=-=-=-=-=-=-=-=-=-=-"); //Open up the server for visitors apiServerState.SetOpen(); //final startup log _log.LogInformation("BOOT: COMPLETED - SERVER IS NOW OPEN"); } #region Swagger and API Versioning utilities // static string XmlCommentsFilePath // { // get // { // //Obsolete, used new method: https://developers.de/blogs/holger_vetter/archive/2017/06/30/swagger-includexmlcomments-platformservices-obsolete-replacement.aspx // //var basePath = PlatformServices.Default.Application.ApplicationBasePath; // var basePath = AppContext.BaseDirectory; // var fileName = typeof(Startup).GetTypeInfo().Assembly.GetName().Name + ".xml"; // return Path.Combine(basePath, fileName); // } // } // static Microsoft.OpenApi.Models.OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description) // { // var info = new Microsoft.OpenApi.Models.OpenApiInfo() // { // Title = $"AyaNova API {description.ApiVersion}", // Version = description.ApiVersion.ToString() // }; // if (description.IsDeprecated) // { // info.Description += " This API version has been deprecated."; // } // return info; // } #endregion } }