From 54dc041a203532c2309100bf2986c5df054ceae3 Mon Sep 17 00:00:00 2001 From: Tobias Klika Date: Wed, 2 Dec 2020 15:45:44 +0100 Subject: [PATCH] fix setup --- zero.Core/Api/SetupApi.cs | 180 +++++++++++++------- zero.Core/Constants.cs | 1 + zero.Core/Options/ZeroOptions.cs | 8 + zero.Core/Routing/Routes.cs | 2 +- zero.Core/ZeroContext.cs | 7 + zero.Web/Controllers/ZeroSetupController.cs | 8 +- zero.Web/ZeroBuilder.cs | 7 +- 7 files changed, 141 insertions(+), 72 deletions(-) diff --git a/zero.Core/Api/SetupApi.cs b/zero.Core/Api/SetupApi.cs index 7a0775f0..73aad742 100644 --- a/zero.Core/Api/SetupApi.cs +++ b/zero.Core/Api/SetupApi.cs @@ -8,12 +8,15 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Security.Cryptography; using System.Threading.Tasks; +using zero.Core.Database; using zero.Core.Entities; using zero.Core.Entities.Setup; using zero.Core.Extensions; using zero.Core.Identity; using zero.Core.Options; +using zero.Core.Utils; using zero.Core.Validation; @@ -23,13 +26,13 @@ namespace zero.Core.Api { protected IZeroOptions Options { get; private set; } - protected UserManager UserManager { get; private set; } + protected IPasswordHasher PasswordHasher { get; private set; } - public SetupApi(IZeroOptions options, UserManager userManager) + public SetupApi(IZeroOptions options, IPasswordHasher passwordHasher) { Options = options; - UserManager = userManager; + PasswordHasher = passwordHasher; } @@ -59,29 +62,35 @@ namespace zero.Core.Api raven.Setup(Options).Initialize(); // create application - Application app = new Application() + Application app = Prepare(new Application() { + Id = "app." + Safenames.Alias(model.AppName), CreatedDate = DateTimeOffset.Now, IsActive = true, Name = model.AppName, - Alias = Safenames.Alias(model.AppName) - }; + Alias = Safenames.Alias(model.AppName), + Database = model.Database.Name + }); // create user - BackofficeUser user = new BackofficeUser() + BackofficeUser user = Prepare(new BackofficeUser() { IsSuper = true, CreatedDate = DateTimeOffset.Now, Email = model.User.Email, + Username = model.User.Email, Name = model.User.Name, IsActive = true, LanguageId = Options.DefaultLanguage, Alias = Safenames.Alias(model.User.Name), - IsEmailConfirmed = true - }; + IsEmailConfirmed = true, + SecurityStamp = NewSecurityStamp() + }); + + user.PasswordHash = PasswordHasher.HashPassword(user, model.User.Password); // create default language - Language language = new Language() // TODO get default language selection from setup UI + Language language = Prepare(new Language() // TODO get default language selection from setup UI { Name = "English", Alias = Safenames.Alias("English"), @@ -89,62 +98,60 @@ namespace zero.Core.Api Code = "en-US", IsActive = true, IsDefault = true - }; + }); - // TODO UserManager uses the DI resolved IDocumentStore instance which should not be available at this point?? - IdentityResult result = await UserManager.CreateAsync(user, model.User.Password); + using IAsyncDocumentSession session = raven.OpenAsyncSession(); + + await session.StoreAsync(user); // user creation failed - if (!result.Succeeded) + //if (!result.Succeeded) + //{ + // EntityResult entityResult = EntityResult.Fail(); + + // foreach (IdentityError error in result.Errors) + // { + // entityResult.AddError(error.Code, error.Description); + // } + + // return entityResult; + //} + + + await session.StoreAsync(app); + + // save default user roles + IList roles = GetRoles(model); + + foreach (BackofficeUserRole role in roles) { - EntityResult entityResult = EntityResult.Fail(); - - foreach (IdentityError error in result.Errors) - { - entityResult.AddError(error.Code, error.Description); - } - - return entityResult; + await session.StoreAsync(role); } - // save entities - using (IAsyncDocumentSession session = raven.OpenAsyncSession()) + // add admin role to super user + // set app-id for user and store it + user.AppId = session.Advanced.GetDocumentId(app); + user.CurrentAppId = user.AppId; + user.RoleIds.Add(roles.First(role => role.Name == "Standard").Id); + user.RoleIds.Add(roles.First(role => role.Name == "Administrator").Id); + await session.StoreAsync(user); + + // create language + await session.StoreAsync(language); + + // set countries + using (Raven.Client.Documents.BulkInsert.BulkInsertOperation bulkInsert = raven.BulkInsert()) { - await session.StoreAsync(app); - - // set app-id for user and store it - user.AppId = session.Advanced.GetDocumentId(app); - await session.StoreAsync(user); - - // save default user roles - IList roles = GetRoles(model); - - foreach (BackofficeUserRole role in roles) + foreach (Country country in GetCountries(model, language)) { - await session.StoreAsync(role); + await bulkInsert.StoreAsync(country); } - - // add admin role to super user - user.RoleIds.Add(roles.First(role => role.Name == "Standard").Alias); - user.RoleIds.Add(roles.First(role => role.Name == "Administrator").Alias); - - // create language - await session.StoreAsync(language); - - // set countries - using (Raven.Client.Documents.BulkInsert.BulkInsertOperation bulkInsert = raven.BulkInsert()) - { - foreach (Country country in GetCountries(model, language)) - { - await bulkInsert.StoreAsync(country); - } - } - - // update settings file. if this fails the changes won't be stored - UpdateSettingsFile(model); - - await session.SaveChangesAsync(); } + + // update settings file. if this fails the changes won't be stored + UpdateSettingsFile(model); + + await session.SaveChangesAsync(); } catch (Exception) { @@ -204,7 +211,7 @@ namespace zero.Core.Api string json = File.ReadAllText(filePath); - countries.AddRange(JsonConvert.DeserializeObject>(json).Select(country => new Country() + countries.AddRange(JsonConvert.DeserializeObject>(json).Select(country => Prepare(new Country() { CreatedDate = DateTimeOffset.Now, IsActive = true, @@ -213,7 +220,7 @@ namespace zero.Core.Api //LanguageId = defaultLanguage.Id, Code = country.Key.ToLowerInvariant(), Name = country.Value - }).ToList()); + })).ToList()); } return countries; @@ -227,12 +234,11 @@ namespace zero.Core.Api { string type = Constants.Auth.Claims.Permission; - BackofficeUserRole adminRole = new BackofficeUserRole() + BackofficeUserRole adminRole = Prepare(new BackofficeUserRole() { Name = "Administrator", Alias = Safenames.Alias("Administrator"), Sort = 0, - //AppId = Constants.Database.SharedAppId, // TODO appx fix Icon = "fth-award", CreatedDate = DateTimeOffset.Now, IsActive = true, @@ -251,9 +257,9 @@ namespace zero.Core.Api new UserClaim(type, Permissions.Settings.Updates, PermissionsValue.Update), new UserClaim(type, Permissions.Settings.Users, PermissionsValue.Update), }, - }; + }); - BackofficeUserRole editorRole = new BackofficeUserRole() + BackofficeUserRole editorRole = Prepare(new BackofficeUserRole() { Name = "Editor", Alias = Safenames.Alias("Editor"), @@ -270,9 +276,9 @@ namespace zero.Core.Api new UserClaim(type, Permissions.Sections.Settings, PermissionsValue.True), new UserClaim(type, Permissions.Settings.Translations, PermissionsValue.True) } - }; + }); - BackofficeUserRole defaultRole = new BackofficeUserRole() + BackofficeUserRole defaultRole = Prepare(new BackofficeUserRole() { Name = "Standard", Alias = Safenames.Alias("Standard"), @@ -284,10 +290,54 @@ namespace zero.Core.Api { new UserClaim(type, Permissions.Sections.Dashboard, PermissionsValue.True) } - }; + }); return new List() { adminRole, editorRole, defaultRole }; } + + + T Prepare(T model, string languageId = null) where T : IZeroIdEntity + { + IZeroEntity zeroEntity = model as IZeroEntity; + + // set default properties + if (zeroEntity != null && zeroEntity.CreatedDate == default) + { + zeroEntity.CreatedDate = DateTimeOffset.Now; + } + if (zeroEntity != null && zeroEntity.CreatedById == default) + { + zeroEntity.CreatedById = Constants.Auth.SystemUser; + } + + if (model is ILanguageAwareEntity && (model as ILanguageAwareEntity).LanguageId == null) + { + (model as ILanguageAwareEntity).LanguageId = languageId; + } + + // update name alias and last modified + if (zeroEntity != null) + { + zeroEntity.Alias = Safenames.Alias(zeroEntity.Name); + zeroEntity.LastModifiedById = Constants.Auth.SystemUser; + zeroEntity.LastModifiedDate = DateTimeOffset.Now; + zeroEntity.Hash ??= IdGenerator.Create(); + zeroEntity.IsActive = true; + } + + return model; + } + + + /// + /// Creates a new security stamp + /// + string NewSecurityStamp() + { + byte[] bytes = new byte[20]; + RandomNumberGenerator.Fill(bytes); + return Base32.ToBase32(bytes); + } } diff --git a/zero.Core/Constants.cs b/zero.Core/Constants.cs index cd031b41..e4658a8d 100644 --- a/zero.Core/Constants.cs +++ b/zero.Core/Constants.cs @@ -11,6 +11,7 @@ public static class Auth { + public const string SystemUser = "system"; public const string DefaultScheme = "zeroScheme"; public const string BackofficeDisplayName = "Zero Bacckoffice"; public const string BackofficeScheme = "zeroBackoffice"; diff --git a/zero.Core/Options/ZeroOptions.cs b/zero.Core/Options/ZeroOptions.cs index d9d93cc7..4a77c4bd 100644 --- a/zero.Core/Options/ZeroOptions.cs +++ b/zero.Core/Options/ZeroOptions.cs @@ -25,6 +25,9 @@ namespace zero.Core.Options Spaces = new SpaceOptions(); } + /// + public bool SetupCompleted => !String.IsNullOrEmpty(Raven?.Database); + /// public string ZeroVersion { get; set; } @@ -86,6 +89,11 @@ namespace zero.Core.Options public interface IZeroOptions { + /// + /// Whether the zero setup has already been completed and the instance is ready for use + /// + bool SetupCompleted { get; } + /// /// The currently active version /// This should not be set manually, as it is used for setup and migrations and incremented automatically diff --git a/zero.Core/Routing/Routes.cs b/zero.Core/Routing/Routes.cs index 47219c9b..7f78eda8 100644 --- a/zero.Core/Routing/Routes.cs +++ b/zero.Core/Routing/Routes.cs @@ -131,7 +131,7 @@ namespace zero.Core.Routing /// public async Task ResolveUrl(HttpContext context) { - if (context.IsBackofficeRequest(Options.BackofficePath)) + if (!Options.SetupCompleted || context.IsBackofficeRequest(Options.BackofficePath)) { return null; } diff --git a/zero.Core/ZeroContext.cs b/zero.Core/ZeroContext.cs index 2efbbc5d..9ba499df 100644 --- a/zero.Core/ZeroContext.cs +++ b/zero.Core/ZeroContext.cs @@ -1,6 +1,8 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Raven.Client.ServerWide; +using Raven.Client.ServerWide.Operations; using System.Security.Claims; using System.Threading.Tasks; using zero.Core.Database; @@ -69,6 +71,11 @@ namespace zero.Core return; } + if (!Options.SetupCompleted) + { + return; + } + _resolved = true; // check if the current request is a backoffice request diff --git a/zero.Web/Controllers/ZeroSetupController.cs b/zero.Web/Controllers/ZeroSetupController.cs index c8e7ece8..126ebe25 100644 --- a/zero.Web/Controllers/ZeroSetupController.cs +++ b/zero.Web/Controllers/ZeroSetupController.cs @@ -30,10 +30,10 @@ namespace zero.Web.Setup public IActionResult Index() { - if (!Options.ZeroVersion.IsNullOrEmpty()) - { - return Redirect(Options.BackofficePath); - } + //if (!Options.ZeroVersion.IsNullOrEmpty()) + //{ + // return Redirect(Options.BackofficePath); + //} return View("/Views/Zero/Setup.cshtml"); } diff --git a/zero.Web/ZeroBuilder.cs b/zero.Web/ZeroBuilder.cs index 070d72c1..fc94bae3 100644 --- a/zero.Web/ZeroBuilder.cs +++ b/zero.Web/ZeroBuilder.cs @@ -157,9 +157,12 @@ namespace zero.Web 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) + if (options.SetupCompleted) { - IndexCreation.CreateIndexes(assembly, store, database: options.Raven.Database); + foreach (Assembly assembly in assemblies) + { + IndexCreation.CreateIndexes(assembly, store, database: options.Raven.Database); + } } return (ZeroStore)raven;