This commit is contained in:
439
server/AyaNova/Startup.cs
Normal file
439
server/AyaNova/Startup.cs
Normal file
@@ -0,0 +1,439 @@
|
||||
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 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;
|
||||
|
||||
|
||||
|
||||
namespace AyaNova
|
||||
{
|
||||
|
||||
public class Startup
|
||||
{
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
public Startup(ILogger<Startup> logger, ILoggerFactory logFactory, Microsoft.AspNetCore.Hosting.IHostingEnvironment 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<Startup> _log;
|
||||
private string _connectionString = "";
|
||||
private readonly Microsoft.AspNetCore.Hosting.IHostingEnvironment _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...");
|
||||
|
||||
//Server state service for shutting people out of api
|
||||
_log.LogDebug("BOOT: init ApiServerState service");
|
||||
services.AddSingleton(new AyaNova.Api.ControllerHelpers.ApiServerState());
|
||||
|
||||
// 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.AddMvcCore().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);
|
||||
|
||||
//Test for server
|
||||
//Will retry 10 times every 3 seconds for a total of 30 seconds
|
||||
if (!DbUtil.DatabaseServerExists(_log, "BOOT: waiting for db server"))
|
||||
{
|
||||
var err = $"BOOT: E1000 - AyaNova can't connect to the database server after trying for 30 seconds (connection string is:\"{DbUtil.DisplayableConnectionString}\")";
|
||||
_log.LogCritical(err);
|
||||
throw new System.ApplicationException(err);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//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<AyContext>(
|
||||
options => options.UseNpgsql(_connectionString,
|
||||
opt => opt.EnableRetryOnFailure())//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(o => o.ReportApiVersions = true);
|
||||
services
|
||||
.AddApiVersioning(options =>
|
||||
{
|
||||
options.AssumeDefaultVersionWhenUnspecified = true;
|
||||
options.DefaultApiVersion = Microsoft.AspNetCore.Mvc.ApiVersion.Parse("8.0");
|
||||
options.ReportApiVersions = true;
|
||||
});
|
||||
|
||||
|
||||
_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();
|
||||
|
||||
|
||||
#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 =>
|
||||
{
|
||||
// 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<IApiVersionDescriptionProvider>();
|
||||
|
||||
// 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<SwaggerDefaultValues>();
|
||||
|
||||
// integrate xml comments
|
||||
c.IncludeXmlComments(XmlCommentsFilePath);
|
||||
|
||||
|
||||
|
||||
//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"
|
||||
|
||||
});
|
||||
|
||||
|
||||
c.AddSecurityRequirement(new System.Collections.Generic.Dictionary<string, System.Collections.Generic.IEnumerable<string>>
|
||||
{
|
||||
{ "Bearer", new string[] { } }
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region JWT AUTHENTICATION
|
||||
//get the key if specified
|
||||
var secretKey = ServerBootConfig.AYANOVA_JWT_SECRET;
|
||||
|
||||
//If no key specified make a unique one based on license ID which is unique to each customer or trialler
|
||||
if (string.IsNullOrWhiteSpace(secretKey))
|
||||
{
|
||||
// This ensures a key can't work on another AyaNova installation
|
||||
if (AyaNova.Core.License.ActiveKey.TrialLicense)
|
||||
secretKey = AyaNova.Core.License.ActiveKey.Id + "5G*QQJ8#bQ7$Xr_@sXfHq4";
|
||||
else
|
||||
secretKey = AyaNova.Core.License.ActiveKey.RegisteredTo + "5G*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",
|
||||
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<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.IHostingEnvironment env,
|
||||
AyContext dbContext, IApiVersionDescriptionProvider provider, AyaNova.Api.ControllerHelpers.ApiServerState apiServerState, IServiceProvider serviceProvider)
|
||||
{
|
||||
_log.LogDebug("BOOT: configuring request pipeline...");
|
||||
|
||||
|
||||
//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 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";
|
||||
});
|
||||
#endregion swagger
|
||||
|
||||
#region STATIC FILES
|
||||
_log.LogDebug("BOOT: pipeline - static files");
|
||||
app.UseDefaultFiles();
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
OnPrepareResponse = context =>
|
||||
{
|
||||
if (context.File.Name == "default.htm")
|
||||
{
|
||||
context.Context.Response.Headers.Add("Cache-Control", "no-cache, no-store");
|
||||
context.Context.Response.Headers.Add("Expires", "-1");
|
||||
}
|
||||
}
|
||||
});
|
||||
#endregion
|
||||
|
||||
#region AUTH / ROLES
|
||||
_log.LogDebug("BOOT: pipeline - authentication");
|
||||
//Use authentication middleware
|
||||
app.UseAuthentication();
|
||||
|
||||
//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 = ct.User.AsNoTracking().Where(a => a.Id == userId).Select(m => new { roles = m.Roles, name = m.Name, Id = m.Id }).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;
|
||||
}
|
||||
await next.Invoke();
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
//USE MVC
|
||||
_log.LogDebug("BOOT: pipeline - MVC");
|
||||
app.UseMvc();
|
||||
|
||||
|
||||
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, AuthorizationRoles.OpsAdminFull);
|
||||
lb.ValidateLocales();
|
||||
|
||||
#if (DEBUG)
|
||||
// Util.DbUtil.DropAndRecreateDb(_log);
|
||||
// AySchema.CheckAndUpdate(dbContext, _log);
|
||||
// lb.ValidateLocales();
|
||||
// AyaNova.Core.License.Initialize(apiServerState, dbContext, _log);
|
||||
// AyaNova.Core.License.Fetch(apiServerState, dbContext, _log);
|
||||
// Util.Seeder.SeedDatabase(dbContext, Util.Seeder.SeedLevel.SmallOneManShopTrialDataSet);
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//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 Info CreateInfoForApiVersion(ApiVersionDescription description)
|
||||
{
|
||||
var info = new Info()
|
||||
{
|
||||
Title = $"AyaNova API {description.ApiVersion}",
|
||||
Version = description.ApiVersion.ToString()
|
||||
};
|
||||
|
||||
if (description.IsDeprecated)
|
||||
{
|
||||
info.Description += " This API version has been deprecated.";
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user