522 lines
22 KiB
C#
522 lines
22 KiB
C#
using System.IO;
|
|
using System.Reflection;
|
|
using System.Linq;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
|
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 Microsoft.Extensions.Options;
|
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
|
|
|
using AyaNova.Models;
|
|
using AyaNova.Util;
|
|
using AyaNova.Generator;
|
|
using AyaNova.Biz;
|
|
using NLog.Extensions.Logging;
|
|
|
|
namespace AyaNova
|
|
{
|
|
public class Startup
|
|
{
|
|
/////////////////////////////////////////////////////////////
|
|
//
|
|
public Startup(Microsoft.AspNetCore.Hosting.IWebHostEnvironment hostingEnvironment)
|
|
{//ILogger<Startup> logger, ILoggerFactory logFactory,
|
|
|
|
// Get the factory for ILogger instances.
|
|
var nlogLoggerProvider = new NLogLoggerProvider();
|
|
|
|
// Create an ILogger.
|
|
_newLog = nlogLoggerProvider.CreateLogger("Server");
|
|
|
|
//x_log = logger;
|
|
_hostingEnvironment = hostingEnvironment;
|
|
//AyaNova.Util.ApplicationLogging.LoggerFactory = logFactory;
|
|
//AyaNova.Util.ApplicationLogging.theLogger = _newLog;
|
|
AyaNova.Util.ApplicationLogging.LoggerProvider = nlogLoggerProvider;
|
|
|
|
//this must be set here
|
|
ServerBootConfig.AYANOVA_CONTENT_ROOT_PATH = hostingEnvironment.ContentRootPath;
|
|
|
|
}
|
|
|
|
private readonly ILogger _newLog;
|
|
|
|
// private readonly ILogger<Startup>x_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)
|
|
{
|
|
_newLog.LogDebug("BOOT: initializing services...");
|
|
|
|
|
|
|
|
//Server state service for shutting people out of api
|
|
_newLog.LogDebug("BOOT: init ApiServerState service");
|
|
services.AddSingleton(new AyaNova.Api.ControllerHelpers.ApiServerState());
|
|
|
|
//Init controllers
|
|
_newLog.LogDebug("BOOT: init controllers");
|
|
var MvcBuilder = services.AddControllers(config =>
|
|
{
|
|
// config.Filters.Add(new AyaNova.Api.ControllerHelpers.ApiCustomExceptionFilter(AyaNova.Util.ApplicationLogging.LoggerFactory));
|
|
config.Filters.Add(new AyaNova.Api.ControllerHelpers.ApiCustomExceptionFilter(_newLog));
|
|
});
|
|
|
|
|
|
|
|
|
|
//Prevent default model binding automatic 400 page so we can consistently show *our* error to our specs
|
|
//https://docs.microsoft.com/en-us/aspnet/core/web-api/index?view=aspnetcore-3.1#automatic-http-400-responses
|
|
MvcBuilder.ConfigureApiBehaviorOptions(options =>
|
|
{
|
|
options.SuppressModelStateInvalidFilter = true;
|
|
});
|
|
|
|
_newLog.LogDebug("BOOT: init JSON");
|
|
|
|
MvcBuilder.AddNewtonsoftJson(options =>
|
|
{
|
|
options.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.None;
|
|
options.SerializerSettings.DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Utc;
|
|
|
|
});
|
|
|
|
|
|
//HTTP CLIENT FACTORY USED BY LICENSE.CS
|
|
//https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-3.1
|
|
_newLog.LogDebug("BOOT: init HTTPClientFactory");
|
|
services.AddHttpClient();
|
|
|
|
|
|
//2019-10-17 METRICS will not work just yet with .netcore 3.1 see here https://github.com/AppMetrics/AppMetrics/issues/480
|
|
//awaiting a new release from them
|
|
_newLog.LogDebug("BOOT: init Metrics service");
|
|
services.AddMetrics();
|
|
|
|
_newLog.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
|
|
_newLog.LogDebug("BOOT: Testing database server connection...");
|
|
|
|
//parse the connection string properly
|
|
DbUtil.ParseConnectionString(_newLog, _connectionString);
|
|
|
|
//Probe for database server
|
|
//Will retry every 10 seconds for up to 5 minutes before bailing
|
|
if (!DbUtil.DatabaseServerExists(_newLog, "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}\")";
|
|
_newLog.LogCritical(err);
|
|
throw new System.ApplicationException(err);
|
|
}
|
|
|
|
|
|
|
|
_newLog.LogInformation("BOOT: Connected to database server - {0}", DbUtil.DisplayableConnectionString);
|
|
|
|
|
|
//ensure database is ready and present
|
|
DbUtil.EnsureDatabaseExists(_newLog);
|
|
|
|
bool LOG_SENSITIVE_DATA = false;
|
|
|
|
#if (DEBUG)
|
|
LOG_SENSITIVE_DATA = false;//############################################################################
|
|
|
|
#endif
|
|
|
|
_newLog.LogDebug("BOOT: init EF service");
|
|
|
|
services.AddEntityFrameworkNpgsql().AddDbContext<AyContext>(
|
|
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
|
|
|
|
|
|
// Add service and create Policy with options
|
|
_newLog.LogDebug("BOOT: init CORS service");
|
|
services.AddCors(options =>
|
|
{
|
|
options.AddPolicy("CorsPolicy",
|
|
builder => builder.AllowAnyOrigin()
|
|
.AllowAnyMethod()
|
|
.AllowAnyHeader()
|
|
//.AllowCredentials()
|
|
);
|
|
});
|
|
|
|
|
|
|
|
|
|
#region Swagger
|
|
|
|
services
|
|
.AddApiVersioning(options =>
|
|
{
|
|
options.AssumeDefaultVersionWhenUnspecified = true;
|
|
options.DefaultApiVersion = Microsoft.AspNetCore.Mvc.ApiVersion.Parse("8.0");
|
|
options.ReportApiVersions = true;
|
|
});
|
|
services.AddVersionedApiExplorer(options =>
|
|
{
|
|
|
|
// add the versioned api explorer, which also adds IApiVersionDescriptionProvider service
|
|
// note: the specified format code will format the version as "'v'major[.minor][-status]"
|
|
options.GroupNameFormat = "'v'VVV";
|
|
|
|
// note: this option is only necessary when versioning by url segment. the SubstitutionFormat
|
|
// can also be used to control the format of the API version in route templates
|
|
|
|
//THIS IS WHAT ADDS THE API version PARAMETER AUTOMATICALLY so you don't need to type an 8 in every swagger-ui route test
|
|
options.SubstituteApiVersionInUrl = true;
|
|
});
|
|
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
|
|
services.AddSwaggerGen(
|
|
c =>
|
|
{
|
|
// integrate xml comments
|
|
c.IncludeXmlComments(XmlCommentsFilePath);
|
|
|
|
//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<string>()
|
|
}
|
|
});
|
|
}
|
|
);
|
|
|
|
|
|
#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));
|
|
|
|
_newLog.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
|
|
|
|
_newLog.LogDebug("BOOT: init Generator service");
|
|
services.AddSingleton<IHostedService, GeneratorService>();
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////
|
|
// 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, IApiVersionDescriptionProvider provider, AyaNova.Api.ControllerHelpers.ApiServerState apiServerState, IServiceProvider serviceProvider)
|
|
{
|
|
_newLog.LogDebug("BOOT: configuring request pipeline...");
|
|
|
|
//this *may* be useful in the event of an issue so uncomment if necessary but errors during dev are handled equally by the logging, I think
|
|
// if (env.IsDevelopment())
|
|
// {
|
|
// app.UseDeveloperExceptionPage();
|
|
// }
|
|
|
|
|
|
//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
|
|
_newLog.LogDebug("BOOT: pipeline - static files");
|
|
app.UseDefaultFiles();
|
|
app.UseStaticFiles();
|
|
//Might need the following if the page doesn't update in the client properly
|
|
//however the vue build process will automatically uniquify each build file names so maybe not required
|
|
// app.UseStaticFiles(new StaticFileOptions
|
|
// {
|
|
// OnPrepareResponse = context =>
|
|
// {
|
|
// if (context.File.Name == "index.html")
|
|
// {
|
|
// context.Context.Response.Headers.Add("Cache-Control", "no-cache, no-store");
|
|
// context.Context.Response.Headers.Add("Expires", "-1");
|
|
// }
|
|
// }
|
|
// });
|
|
#endregion
|
|
|
|
_newLog.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
|
|
|
|
_newLog.LogDebug("BOOT: pipeline - CORS");
|
|
app.UseCors("CorsPolicy");
|
|
|
|
|
|
#region AUTH / ROLES CUSTOM MIDDLEWARE
|
|
_newLog.LogDebug("BOOT: pipeline - authentication");
|
|
//Use authentication middleware
|
|
app.UseAuthentication();
|
|
|
|
_newLog.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<AyContext>();
|
|
|
|
//get the user record
|
|
var u = await ct.User.AsNoTracking().Where(a => a.Id == userId).Select(m => new { roles = m.Roles, name = m.Name, id = m.Id, translationId = m.UserOptions.TranslationId }).FirstAsync();
|
|
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_TRANSLATION_ID"] = u.translationId;
|
|
}
|
|
await next.Invoke();
|
|
});
|
|
|
|
#endregion
|
|
|
|
_newLog.LogDebug("BOOT: pipeline - ENDPOINTS");
|
|
app.UseEndpoints(endpoints =>
|
|
{
|
|
endpoints.MapControllers();
|
|
});
|
|
|
|
#region SWAGGER
|
|
|
|
_newLog.LogDebug("BOOT: pipeline - api explorer");
|
|
// Enable middleware to serve generated Swagger as a JSON endpoint.
|
|
app.UseSwagger();
|
|
app.UseSwaggerUI(
|
|
options =>
|
|
{
|
|
foreach (var description in provider.ApiVersionDescriptions)
|
|
{
|
|
options.SwaggerEndpoint(
|
|
$"/swagger/{description.GroupName}/swagger.json",
|
|
description.GroupName.ToUpperInvariant());
|
|
}
|
|
options.DocExpansion(Swashbuckle.AspNetCore.SwaggerUI.DocExpansion.None);
|
|
options.DefaultModelsExpandDepth(-1);//This is meant to hide the Models section that would appear at the bottom of the swagger ui showing *all* models from the api
|
|
options.DocumentTitle = "AyaNova API explorer";
|
|
options.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 = true;//#######################################################################################
|
|
|
|
#if (DEBUG)
|
|
|
|
//TESTING
|
|
if (TESTING_REFRESH_DB)
|
|
ServerBootConfig.AYANOVA_PERMANENTLY_ERASE_DATABASE = TESTING_REFRESH_DB;
|
|
//TESTING
|
|
#endif
|
|
|
|
|
|
if (ServerBootConfig.AYANOVA_PERMANENTLY_ERASE_DATABASE)
|
|
{
|
|
_newLog.LogWarning("BOOT: AYANOVA_PERMANENTLY_ERASE_DATABASE is true, dropping and recreating database");
|
|
Util.DbUtil.DropAndRecreateDbAsync(_newLog).Wait();
|
|
AySchema.CheckAndUpdateAsync(dbContext, _newLog).Wait();
|
|
}
|
|
|
|
//Check schema
|
|
_newLog.LogDebug("BOOT: db schema check");
|
|
AySchema.CheckAndUpdateAsync(dbContext, _newLog).Wait();
|
|
|
|
//Check database integrity
|
|
_newLog.LogDebug("BOOT: db integrity check");
|
|
DbUtil.CheckFingerPrintAsync(AySchema.EXPECTED_COLUMN_COUNT, AySchema.EXPECTED_INDEX_COUNT, _newLog).Wait();
|
|
|
|
//Initialize license
|
|
AyaNova.Core.License.InitializeAsync(apiServerState, dbContext, _newLog).Wait();
|
|
|
|
//Ensure translations are present, not missing any keys and that there is a server default translation that exists
|
|
TranslationBiz lb = new TranslationBiz(dbContext, 1, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, AuthorizationRoles.OpsAdminFull);
|
|
lb.ValidateTranslationsAsync().Wait();
|
|
|
|
|
|
|
|
#if (DEBUG)
|
|
//TESTING
|
|
if (TESTING_REFRESH_DB)
|
|
{
|
|
AyaNova.Core.License.FetchKeyAsync(apiServerState, dbContext, _newLog).Wait();
|
|
//NOTE: For unit testing make sure the time zone is same as tester to ensure list filter by date tests will work because server is on same page as user in terms of time
|
|
Util.Seeder.SeedDatabaseAsync(Util.Seeder.SeedLevel.SmallOneManShopTrialDataSet, -8).Wait();//#############################################################################################
|
|
}
|
|
//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
|
|
_newLog.LogInformation($"BOOT: Active techs - {UserBiz.ActiveCountAsync().Result}");
|
|
|
|
//Log the license info so it's on the record
|
|
_newLog.LogInformation($"BOOT: License - [{AyaNova.Core.License.LicenseInfoLogFormat}]");
|
|
|
|
|
|
|
|
//Open up the server for visitors
|
|
apiServerState.SetOpen();
|
|
|
|
//final startup log
|
|
_newLog.LogInformation("BOOT: COMPLETED - SERVER IS NOW OPEN");
|
|
|
|
#if (DEBUG)
|
|
//Show in dev console that server is open (so I don't need to look in the log to see it)
|
|
System.Diagnostics.Debugger.Log(1, "BOOT", "Startup.cs -> BOOT: COMPLETED - SERVER IS NOW OPEN");
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
#region Swagger and API Versioning utilities
|
|
|
|
static string XmlCommentsFilePath
|
|
{
|
|
get
|
|
{
|
|
var basePath = AppContext.BaseDirectory;
|
|
var fileName = typeof(Startup).GetTypeInfo().Assembly.GetName().Name + ".xml";
|
|
return Path.Combine(basePath, fileName);
|
|
}
|
|
}
|
|
|
|
|
|
#endregion
|
|
}
|
|
}
|
|
|