using FluentValidation; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; using Raven.Client.Documents; using Raven.Client.Documents.Indexes; using System; using System.Linq; using System.Reflection; using System.Threading.Tasks; using zero.Core; using zero.Core.Api; using zero.Core.Assemblies; using zero.Core.Database.Indexes; using zero.Core.Entities; using zero.Core.Extensions; using zero.Core.Identity; using zero.Core.Options; using zero.Core.Plugins; using zero.Core.Utils; using zero.Core.Validation; using zero.Web.Defaults; using zero.Web.Filters; namespace zero.Web { // TODO maybe use a middleware like Hangfire does: https://github.com/HangfireIO/Hangfire/blob/master/src/Hangfire.AspNetCore/HangfireEndpointRouteBuilderExtensions.cs public class ZeroBuilder { public virtual IServiceCollection Services { get; } public virtual IMvcBuilder Mvc { get; } IConfiguration Configuration; IZeroStartupOptions StartupOptions; public ZeroBuilder(IServiceCollection services, IConfiguration configuration, Action setupAction) { Services = services; Mvc = services.AddMvc(opts => { //opts.ModelBinderProviders.Insert(0, new ZeroEntityInterfaceBinderProvider()); }); Configuration = configuration; // create startup options StartupOptions = new ZeroStartupOptions(Mvc); StartupOptions.AssemblyDiscoveryRules.Add(new ZeroAssemblyDiscoveryRule()); setupAction?.Invoke(StartupOptions); // adds and discovers additional and built-in assemblies new AssemblyDiscovery(Mvc).Execute(StartupOptions.AssemblyDiscoveryRules); // add default plugin AddPlugin(); // create and bind zero options Services.AddOptions() .Bind(Configuration.GetSection("Zero")) .Configure(opts => { //opts.AssemblyDiscoveryRules.Add(new ZeroAssemblyDiscoveryRule()); opts.ZeroVersion = "0.0.1.0"; // TODO }); // add transient options to DI Services.AddTransient(factory => factory.GetService>().CurrentValue); // configure MVC Mvc.AddNewtonsoftJson(x => { // TODO this shall only be configurated for backoffice controllers BackofficeJsonSerlializerSettings.Setup(x.SerializerSettings, true); }); if (Environment.GetEnvironmentVariable("DOTNET_WATCH") == "1") { Mvc.AddRazorRuntimeCompilation(); } // configure Raven + Identity ConfigureDatabase(); ConfigureIdentity(); // configure FluentValidation ValidatorOptions.Global.PropertyNameResolver = ValidatorCamelCasePropertyResolver.ResolvePropertyName; // add default services Services.AddScoped(); Services.AddTransient(); Services.AddTransient(typeof(IAppScope<>), typeof(AppScope<>)); Services.AddScoped(); Services.AddHttpContextAccessor(); Services.AddTransient(); Services.AddTransient(factory => { IWebHostEnvironment env = factory.GetService(); return new Paths(env.WebRootPath, true); }); } /// /// Configures Raven database instance /// void ConfigureDatabase() { // add raven Services.AddSingleton(context => { IZeroOptions options = context.GetService(); DocumentStore store = new DocumentStore() { Urls = new string[1] { options.Raven.Url }, Database = options.Raven.Database }; IDocumentStore raven = store.Setup(options).Initialize(); // create all indexes var assemblies = AssemblyDiscovery.Current.GetAssemblies().ToList(); // TODO maybe we shouldn't use all auto-registered assemblies but specify them directly via options? foreach (Assembly assembly in assemblies) { IndexCreation.CreateIndexes(assembly, store); } return raven; }); } /// /// Configures user + roles /// void ConfigureIdentity() { Services.AddIdentity(opts => { opts.ClaimsIdentity.UserIdClaimType = Constants.Auth.Claims.UserId; opts.ClaimsIdentity.UserNameClaimType = Constants.Auth.Claims.UserName; opts.ClaimsIdentity.RoleClaimType = Constants.Auth.Claims.Role; opts.ClaimsIdentity.SecurityStampClaimType = Constants.Auth.Claims.SecurityStamp; opts.Password.RequireDigit = false; opts.Password.RequireLowercase = false; opts.Password.RequireUppercase = false; opts.Password.RequireNonAlphanumeric = false; opts.Password.RequiredLength = 8; opts.Password.RequiredUniqueChars = 1; opts.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); opts.Lockout.MaxFailedAccessAttempts = 5; opts.Lockout.AllowedForNewUsers = true; }) .AddClaimsPrincipalFactory() .AddDefaultTokenProviders(); Services.ConfigureApplicationCookie(opts => { //opts.Cookie.Path = // TODO use backoffice path opts.Cookie.Name = Constants.Auth.CookieName; opts.SlidingExpiration = true; opts.ExpireTimeSpan = TimeSpan.FromMinutes(60); // override redirect to login page (handled by vue frontend) and return a 401 instead opts.Events.OnRedirectToLogin = (context) => { context.Response.StatusCode = 401; return Task.CompletedTask; }; }); Services.AddTransient, ZeroUserStore>(); Services.AddTransient, ZeroRoleStore>(); Services.AddScoped>(); Services.AddScoped>(); Services.AddScoped>(); } /// /// Use specified options /// public ZeroBuilder WithOptions(Action configureOptions) { Services.Configure(configureOptions); return this; } /// /// Adds a zero plugin /// public ZeroBuilder AddPlugin() where T : class, IZeroPlugin, new() { AssemblyDiscovery.Current.AddAssembly(typeof(T).Assembly); Services.AddScoped(); AddPluginServices(); return this; } /// /// Adds a zero plugin /// public ZeroBuilder AddPlugin(Func implementationFactory) where T : class, IZeroPlugin, new() { AssemblyDiscovery.Current.AddAssembly(typeof(T).Assembly); Services.AddScoped(implementationFactory); AddPluginServices(); return this; } /// /// Creates a temporary instance of the plugin to add additional services /// /// void AddPluginServices() where T : class, IZeroPlugin, new() { try { T plugin = new T(); plugin.ConfigureServices(Services, Configuration); Services.Configure(opts => plugin.Configure(opts)); } catch { throw new Exception($"Plugin \"{nameof(T)}\" needs an additional parameterless constructor as ConfigureServices() is called before the DI container is built"); } } } }