2020-05-03 17:09:45 +02:00
using FluentValidation ;
2020-05-09 20:05:08 +02:00
using Microsoft.AspNetCore.Hosting ;
2020-05-03 17:09:45 +02:00
using Microsoft.AspNetCore.Identity ;
2020-05-11 15:34:47 +02:00
using Microsoft.Extensions.Configuration ;
2020-04-15 01:42:06 +02:00
using Microsoft.Extensions.DependencyInjection ;
2020-05-11 15:34:47 +02:00
using Microsoft.Extensions.Options ;
2020-08-27 14:22:13 +02:00
using Newtonsoft.Json ;
using Newtonsoft.Json.Converters ;
using Newtonsoft.Json.Serialization ;
2020-05-11 15:34:47 +02:00
using Raven.Client.Documents ;
2020-05-15 14:23:47 +02:00
using Raven.Client.Documents.Indexes ;
2020-04-05 01:19:42 +02:00
using System ;
2020-05-29 14:33:20 +02:00
using System.Linq ;
2020-05-15 14:23:47 +02:00
using System.Reflection ;
2020-04-15 01:42:06 +02:00
using System.Threading.Tasks ;
2020-04-05 01:19:42 +02:00
using zero.Core ;
2020-05-09 20:05:08 +02:00
using zero.Core.Api ;
2020-05-27 16:03:38 +02:00
using zero.Core.Assemblies ;
2020-05-15 14:23:47 +02:00
using zero.Core.Database.Indexes ;
2020-04-05 01:19:42 +02:00
using zero.Core.Entities ;
2020-05-11 15:34:47 +02:00
using zero.Core.Extensions ;
2020-04-15 01:42:06 +02:00
using zero.Core.Identity ;
2020-05-14 13:32:33 +02:00
using zero.Core.Options ;
2020-05-08 18:43:39 +02:00
using zero.Core.Plugins ;
2020-09-14 16:05:41 +02:00
using zero.Core.Utils ;
2020-05-03 17:09:45 +02:00
using zero.Core.Validation ;
2020-05-14 13:32:33 +02:00
using zero.Web.Defaults ;
2020-08-13 12:03:24 +02:00
using zero.Web.Filters ;
2020-04-05 00:39:12 +02:00
namespace zero.Web
{
2020-05-12 01:36:27 +02:00
// TODO maybe use a middleware like Hangfire does: https://github.com/HangfireIO/Hangfire/blob/master/src/Hangfire.AspNetCore/HangfireEndpointRouteBuilderExtensions.cs
2020-04-05 00:39:12 +02:00
public class ZeroBuilder
{
public virtual IServiceCollection Services { get ; }
2020-05-27 16:03:38 +02:00
public virtual IMvcBuilder Mvc { get ; }
IConfiguration Configuration ;
IZeroStartupOptions StartupOptions ;
2020-05-11 15:34:47 +02:00
2020-05-27 18:29:36 +02:00
public ZeroBuilder ( IServiceCollection services , IConfiguration configuration , Action < IZeroStartupOptions > setupAction )
2020-04-05 00:39:12 +02:00
{
2020-05-27 18:29:36 +02:00
Services = services ;
2020-08-27 14:22:13 +02:00
Mvc = services . AddMvc ( opts =>
{
//opts.ModelBinderProviders.Insert(0, new ZeroEntityInterfaceBinderProvider());
});
2020-05-11 15:34:47 +02:00
Configuration = configuration ;
2020-04-15 01:42:06 +02:00
2020-05-27 16:03:38 +02:00
// create startup options
2020-05-27 18:29:36 +02:00
StartupOptions = new ZeroStartupOptions ( Mvc );
2020-05-27 16:03:38 +02:00
StartupOptions . AssemblyDiscoveryRules . Add ( new ZeroAssemblyDiscoveryRule ());
setupAction ?. Invoke ( StartupOptions );
2020-05-11 15:34:47 +02:00
2020-05-27 16:03:38 +02:00
2020-05-28 00:36:52 +02:00
// adds and discovers additional and built-in assemblies
new AssemblyDiscovery ( Mvc ). Execute ( StartupOptions . AssemblyDiscoveryRules );
2020-05-27 16:03:38 +02:00
// add default plugin
AddPlugin < ZeroBackofficePlugin >();
2020-05-27 18:29:36 +02:00
2020-05-27 16:03:38 +02:00
// create and bind zero options
2020-05-14 13:32:33 +02:00
Services . AddOptions < ZeroOptions >()
. Bind ( Configuration . GetSection ( "Zero" ))
. Configure ( opts =>
{
2020-05-27 16:03:38 +02:00
//opts.AssemblyDiscoveryRules.Add(new ZeroAssemblyDiscoveryRule());
2020-05-14 13:32:33 +02:00
opts . ZeroVersion = "0.0.1.0" ; // TODO
});
2020-05-13 14:06:11 +02:00
2020-05-27 16:03:38 +02:00
// add transient options to DI
Services . AddTransient < IZeroOptions >( factory => factory . GetService < IOptionsMonitor < ZeroOptions >>(). CurrentValue );
2020-05-11 15:34:47 +02:00
2020-05-27 16:03:38 +02:00
// configure MVC
2020-08-27 14:22:13 +02:00
Mvc . AddNewtonsoftJson ( x =>
{
// TODO this shall only be configurated for backoffice controllers
2020-10-04 00:52:54 +02:00
BackofficeJsonSerlializerSettings . Setup ( x . SerializerSettings , true );
2020-08-27 14:22:13 +02:00
});
2020-09-18 01:06:21 +02:00
2020-05-11 15:34:47 +02:00
if ( Environment . GetEnvironmentVariable ( "DOTNET_WATCH" ) == "1" )
{
2020-05-27 16:03:38 +02:00
Mvc . AddRazorRuntimeCompilation ();
2020-05-11 15:34:47 +02:00
}
2020-05-22 15:06:17 +02:00
2020-05-27 16:03:38 +02:00
// configure Raven + Identity
ConfigureDatabase ();
ConfigureIdentity ();
// configure FluentValidation
2020-07-09 15:33:58 +02:00
ValidatorOptions . Global . PropertyNameResolver = ValidatorCamelCasePropertyResolver . ResolvePropertyName ;
2020-05-27 16:03:38 +02:00
// add default services
Services . AddScoped < IApplicationContext , ApplicationContext >();
Services . AddTransient < IBackofficeStore , BackofficeStore >();
Services . AddTransient ( typeof ( IAppScope <>), typeof ( AppScope <>));
2020-08-13 12:03:24 +02:00
Services . AddScoped < ModelStateValidationFilterAttribute >();
2020-05-27 16:03:38 +02:00
Services . AddHttpContextAccessor ();
Services . AddTransient < IZeroVue , ZeroVue >();
Services . AddTransient < IPaths >( factory =>
{
IWebHostEnvironment env = factory . GetService < IWebHostEnvironment >();
return new Paths ( env . WebRootPath , true );
});
2020-05-11 15:34:47 +02:00
}
/// <summary>
/// Configures Raven database instance
/// </summary>
void ConfigureDatabase ()
{
// add raven
Services . AddSingleton ( context =>
{
IZeroOptions options = context . GetService < IZeroOptions >();
DocumentStore store = new DocumentStore ()
{
Urls = new string [ 1 ] { options . Raven . Url },
Database = options . Raven . Database
};
2020-05-21 11:18:05 +02:00
IDocumentStore raven = store . Setup ( options ). Initialize ();
2020-05-15 14:23:47 +02:00
2020-05-21 11:18:05 +02:00
// create all indexes
2020-05-29 14:33:20 +02:00
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 );
}
2020-05-15 14:23:47 +02:00
return raven ;
2020-05-11 15:34:47 +02:00
});
}
/// <summary>
/// Configures user + roles
/// </summary>
void ConfigureIdentity ()
{
2020-04-15 01:42:06 +02:00
Services . AddIdentity < User , UserRole >( opts =>
{
2020-04-15 14:09:52 +02:00
opts . ClaimsIdentity . UserIdClaimType = Constants . Auth . Claims . UserId ;
opts . ClaimsIdentity . UserNameClaimType = Constants . Auth . Claims . UserName ;
2020-04-16 12:56:17 +02:00
opts . ClaimsIdentity . RoleClaimType = Constants . Auth . Claims . Role ;
2020-04-15 14:09:52 +02:00
opts . ClaimsIdentity . SecurityStampClaimType = Constants . Auth . Claims . SecurityStamp ;
2020-04-15 01:42:06 +02:00
opts . Password . RequireDigit = false ;
opts . Password . RequireLowercase = false ;
opts . Password . RequireUppercase = false ;
opts . Password . RequireNonAlphanumeric = false ;
2020-04-15 14:09:52 +02:00
opts . Password . RequiredLength = 8 ;
2020-04-15 01:42:06 +02:00
opts . Password . RequiredUniqueChars = 1 ;
opts . Lockout . DefaultLockoutTimeSpan = TimeSpan . FromMinutes ( 5 );
opts . Lockout . MaxFailedAccessAttempts = 5 ;
opts . Lockout . AllowedForNewUsers = true ;
2020-04-15 14:09:52 +02:00
})
. AddClaimsPrincipalFactory < ZeroClaimsPrinicipalFactory >()
. AddDefaultTokenProviders ();
2020-04-15 01:42:06 +02:00
Services . ConfigureApplicationCookie ( opts =>
{
//opts.Cookie.Path = // TODO use backoffice path
opts . Cookie . Name = Constants . Auth . CookieName ;
opts . SlidingExpiration = true ;
2020-05-09 00:29:35 +02:00
opts . ExpireTimeSpan = TimeSpan . FromMinutes ( 60 );
2020-04-15 01:42:06 +02:00
// 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 ;
};
});
2020-04-16 00:56:22 +02:00
Services . AddTransient < IUserStore < User >, ZeroUserStore >();
Services . AddTransient < IRoleStore < UserRole >, ZeroRoleStore >();
2020-04-15 01:42:06 +02:00
Services . AddScoped < UserManager < User >>();
Services . AddScoped < SignInManager < User >>();
Services . AddScoped < RoleManager < UserRole >>();
2020-04-05 01:19:42 +02:00
}
2020-05-11 15:34:47 +02:00
/// <summary>
/// Use specified options
/// </summary>
2020-04-05 01:19:42 +02:00
public ZeroBuilder WithOptions ( Action < ZeroOptions > configureOptions )
{
2020-05-14 13:32:33 +02:00
Services . Configure ( configureOptions );
2020-04-05 01:19:42 +02:00
return this ;
2020-04-05 00:39:12 +02:00
}
2020-05-18 15:55:43 +02:00
/// <summary>
/// Adds a zero plugin
/// </summary>
2020-05-27 18:29:36 +02:00
public ZeroBuilder AddPlugin < T >() where T : class , IZeroPlugin , new ()
2020-05-18 15:55:43 +02:00
{
2020-05-28 00:36:52 +02:00
AssemblyDiscovery . Current . AddAssembly ( typeof ( T ). Assembly );
2020-05-18 15:55:43 +02:00
Services . AddScoped < IZeroPlugin , T >();
AddPluginServices < T >();
2020-05-27 18:29:36 +02:00
return this ;
2020-05-18 15:55:43 +02:00
}
/// <summary>
/// Adds a zero plugin
/// </summary>
2020-05-27 18:29:36 +02:00
public ZeroBuilder AddPlugin < T >( Func < IServiceProvider , T > implementationFactory ) where T : class , IZeroPlugin , new ()
2020-05-18 15:55:43 +02:00
{
2020-05-28 00:36:52 +02:00
AssemblyDiscovery . Current . AddAssembly ( typeof ( T ). Assembly );
2020-05-18 15:55:43 +02:00
Services . AddScoped < IZeroPlugin , T >( implementationFactory );
AddPluginServices < T >();
2020-05-27 18:29:36 +02:00
return this ;
2020-05-18 15:55:43 +02:00
}
/// <summary>
/// Creates a temporary instance of the plugin to add additional services
/// </summary>
/// <typeparam name="T"></typeparam>
void AddPluginServices < T >() where T : class , IZeroPlugin , new ()
{
try
{
T plugin = new T ();
2020-09-10 16:12:06 +02:00
plugin . ConfigureServices ( Services , Configuration );
2020-05-18 15:55:43 +02:00
2020-10-19 15:58:32 +02:00
Services . Configure < ZeroOptions >( opts => plugin . Configure ( opts ));
2020-05-18 15:55:43 +02:00
}
catch
{
throw new Exception ( $"Plugin \" { nameof ( T )} \ " needs an additional parameterless constructor as ConfigureServices() is called before the DI container is built" );
}
}
2020-04-05 00:39:12 +02:00
}
}