From 92894f982bdf91a85018459af7b3d4ae1cb74299 Mon Sep 17 00:00:00 2001 From: Tobias Klika Date: Fri, 3 Apr 2020 16:45:47 +0200 Subject: [PATCH] basic setup flow works --- zero.Core/Api/Alias.cs | 106 ++++++ zero.Core/Api/Setup/SetupApi.cs | 74 +++++ zero.Core/Constants.cs | 2 + zero.Core/Entities/DatabaseEntity.cs | 11 +- zero.Core/Entities/Setup/SetupModel.cs | 28 ++ zero.Core/Entities/Setup/SetupSave.cs | 41 --- zero.Core/Extensions/CharExtensions.cs | 60 ++++ zero.Core/Validation/SetupModelValidator.cs | 22 ++ zero.Core/Validation/SetupSaveValidator.cs | 32 -- zero.Web/Controllers/SetupController.cs | 14 +- zero.Web/Sass/Core/_settings.scss | 2 +- zero.Web/Sass/Core/_typography.scss | 2 +- zero.Web/Sass/Setup/_canvas.scss | 18 +- zero.Web/Setup/Steps/step-application.vue | 2 +- zero.Web/Setup/Steps/step-database.vue | 4 +- zero.Web/Setup/Steps/step-install.vue | 145 ++++++++- zero.Web/Setup/Steps/step-user.vue | 6 +- zero.Web/Setup/setup.vue | 60 ++-- zero.Web/Startup.cs | 4 + zero.Web/config.js | 16 +- zero.Web/wwwroot/Assets/setup.js | 341 +++++++++++++++++++- zero.Web/zero.Web.csproj | 1 + 22 files changed, 839 insertions(+), 152 deletions(-) create mode 100644 zero.Core/Api/Alias.cs create mode 100644 zero.Core/Api/Setup/SetupApi.cs create mode 100644 zero.Core/Entities/Setup/SetupModel.cs delete mode 100644 zero.Core/Entities/Setup/SetupSave.cs create mode 100644 zero.Core/Extensions/CharExtensions.cs create mode 100644 zero.Core/Validation/SetupModelValidator.cs delete mode 100644 zero.Core/Validation/SetupSaveValidator.cs diff --git a/zero.Core/Api/Alias.cs b/zero.Core/Api/Alias.cs new file mode 100644 index 00000000..98370e33 --- /dev/null +++ b/zero.Core/Api/Alias.cs @@ -0,0 +1,106 @@ +using System; +using System.Text; +using zero.Core.Extensions; + +namespace zero.Core.Api +{ + public class Alias + { + private const char HYPHEN = '-'; + + private const char PLUS = '+'; + + private const char AMPERSAND = '&'; + + + /// + /// Converts a term to a safe alias (suitable for URLs) + /// + public static string Generate(string value) + { + if (String.IsNullOrWhiteSpace(value)) + { + return String.Empty; + } + + char previous = default; + StringBuilder output = new StringBuilder(); + + for (int i = 0; i < value.Length; i++) + { + // get character in lower case + char character = char.ToLower(value[i]); + char target; + + // do not handle surrogates + if (char.IsSurrogate(character)) + { + continue; + } + + // special replacements accents + umlauts + if (character.TryReplaceAccent(out char[] replacement)) + { + if (replacement.Length > 1) + { + output.Append(replacement); + output.Remove(output.Length - 1, 1); + } + target = replacement[replacement.Length - 1]; + } + // append character a-z, 0-9 + else if (character.IsAZor09()) + { + target = character; + } + // + sign for + and & + else if (character == PLUS || character == AMPERSAND) + { + target = PLUS; + } + // add hyphen for all other characters + else + { + target = HYPHEN; + } + + // add default characters + if (target != HYPHEN && target != PLUS) + { + output.Append(target); + } + // add hyphen if it isn't first and previous char is not + or - + else if (target == HYPHEN && previous != default && previous != PLUS && previous != HYPHEN) + { + output.Append(target); + } + // add plus. do remove hyphen it is the previous character + else if (target == PLUS) + { + if (previous == HYPHEN) + { + output.Remove(output.Length - 1, 1); + } + output.Append(target); + } + + if (output.Length > 0) + { + previous = output[output.Length - 1]; + } + } + + if (output.Length > 0 && !output[output.Length - 1].IsAZor09()) + { + output.Remove(output.Length - 1, 1); + } + + if (output.Length == 0) + { + output.Append(HYPHEN); + } + + return output.ToString(); + } + } +} \ No newline at end of file diff --git a/zero.Core/Api/Setup/SetupApi.cs b/zero.Core/Api/Setup/SetupApi.cs new file mode 100644 index 00000000..5a23e01f --- /dev/null +++ b/zero.Core/Api/Setup/SetupApi.cs @@ -0,0 +1,74 @@ +using FluentValidation.Results; +using Raven.Client.Documents; +using Raven.Client.Documents.Conventions; +using Raven.Client.Documents.Session; +using System; +using System.Threading.Tasks; +using zero.Core.Entities; +using zero.Core.Entities.Setup; +using zero.Core.Validation; + +namespace zero.Core.Api +{ + public class SetupApi : ISetupApi + { + public SetupApi() + { + //Raven = + } + + + public async Task> Install(SetupModel model) + { + ValidationResult validation = await new SetupModelValidator().ValidateAsync(model); + + if (!validation.IsValid) + { + return EntityChangeResult.Fail(validation); + } + + // test read & write permissions on folders + // TODO + + // create temporary instance of database + DocumentStore raven = new DocumentStore() + { + Urls = model.Database.Url.Split(','), + Database = model.Database.Name + }; + + raven.Conventions.FindCollectionName = type => + { + return Constants.Database.CollectionPrefix + DocumentConventions.DefaultGetCollectionName(type); + }; + + raven.Initialize(); + + // create application + Application app = new Application() + { + CreatedDate = DateTimeOffset.Now, + IsActive = true, + Name = model.AppName, + Alias = Alias.Generate(model.AppName) + }; + + using (IAsyncDocumentSession session = raven.OpenAsyncSession()) + { + await session.StoreAsync(app); + await session.SaveChangesAsync(); + } + + raven.Dispose(); + + return EntityChangeResult.Success(model); + } + } + + + + public interface ISetupApi + { + Task> Install(SetupModel model); + } +} diff --git a/zero.Core/Constants.cs b/zero.Core/Constants.cs index 45872961..44e13f21 100644 --- a/zero.Core/Constants.cs +++ b/zero.Core/Constants.cs @@ -5,6 +5,8 @@ public static class Database { public const string SharedAppId = "shared"; + + public const string CollectionPrefix = "zero."; } public static class Sections diff --git a/zero.Core/Entities/DatabaseEntity.cs b/zero.Core/Entities/DatabaseEntity.cs index 460aabe8..0bed1903 100644 --- a/zero.Core/Entities/DatabaseEntity.cs +++ b/zero.Core/Entities/DatabaseEntity.cs @@ -4,7 +4,7 @@ using zero.Core.Attributes; namespace zero.Core.Entities { - [DebuggerDisplay("Id = {Id,nq}, Name = {Name}")] + [DebuggerDisplay("Id = {Id,nq}, Name = {Name}, Alias = {Alias}")] public abstract class DatabaseEntity : IDatabaseEntity { /// @@ -18,6 +18,9 @@ namespace zero.Core.Entities /// public string Name { get; set; } + /// + public string Alias { get; set; } + /// public uint Sort { get; set; } @@ -46,6 +49,12 @@ namespace zero.Core.Entities /// string Name { get; set; } + /// + /// Unique alias which can be used in the frontend and URLs + /// As generating aliases from the name would not be unique we append an incremental number if the desired alias is not available anymore + /// + string Alias { get; set; } + /// /// Sort order /// diff --git a/zero.Core/Entities/Setup/SetupModel.cs b/zero.Core/Entities/Setup/SetupModel.cs new file mode 100644 index 00000000..aa632e4c --- /dev/null +++ b/zero.Core/Entities/Setup/SetupModel.cs @@ -0,0 +1,28 @@ +namespace zero.Core.Entities.Setup +{ + public sealed class SetupModel + { + public string AppName { get; set; } + + public SetupUserModel User { get; set; } + + public SetupDatabaseModel Database { get; set; } + } + + + public sealed class SetupUserModel + { + public string Name { get; set; } + + public string Email { get; set; } + + public string Password { get; set; } + } + + public sealed class SetupDatabaseModel + { + public string Url { get; set; } + + public string Name { get; set; } + } +} diff --git a/zero.Core/Entities/Setup/SetupSave.cs b/zero.Core/Entities/Setup/SetupSave.cs deleted file mode 100644 index 920c01d2..00000000 --- a/zero.Core/Entities/Setup/SetupSave.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace zero.Core.Entities.Setup -{ - public sealed class SetupSave - { - public string AppName { get; set; } - - public SetupUserSave User { get; set; } - - public SetupDatabaseSave Database { get; set; } - - public SetupSavePart Part { get; set; } - } - - - public sealed class SetupUserSave - { - public string Name { get; set; } - - public string Email { get; set; } - - public string Password { get; set; } - } - - public sealed class SetupDatabaseSave - { - public string Url { get; set; } - - public string Name { get; set; } - } - - - public enum SetupSavePart - { - ValidateUser = 0, - ValidateApplication = 1, - ValidateDatabase = 2, - ValidateFolderAccess = 3, - WriteSettings = 4, - SaveData = 5 - } -} diff --git a/zero.Core/Extensions/CharExtensions.cs b/zero.Core/Extensions/CharExtensions.cs new file mode 100644 index 00000000..c0457b3a --- /dev/null +++ b/zero.Core/Extensions/CharExtensions.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; + +namespace zero.Core.Extensions +{ + public static class CharExtensions + { + private static Dictionary accents { get; } = new Dictionary() + { + { 'ä', new char[2] { 'a', 'e' } }, + { 'á', new char[1] { 'a' } }, + { 'à', new char[1] { 'a' } }, + { 'ó', new char[1] { 'o' } }, + { 'ò', new char[1] { 'o' } }, + { 'é', new char[1] { 'e' } }, + { 'è', new char[1] { 'e' } }, + { 'ú', new char[1] { 'u' } }, + { 'ù', new char[1] { 'u' } }, + { 'í', new char[1] { 'i' } }, + { 'ì', new char[1] { 'i' } }, + { 'ö', new char[2] { 'o', 'e' } }, + { 'ü', new char[2] { 'u', 'e' } }, + { 'ß', new char[2] { 's', 's' } }, + { '&', new char[1] { '+' } } + }; + + + /// + /// Check if a character is from a-z, A-Z or 0-9 + /// + public static bool IsAZor09(this char value) + { + return (value >= 0x41 && value <= 0x5A) || (value >= 0x61 && value <= 0x7a) || (value >= 0x30 && value <= 0x39); + } + + + /// + /// Check if a character is in ASCII range + /// + public static bool IsASCII(this char value) + { + return value < 128; + } + + + /// + /// Replaces an accent or umlaut with the appropriate URL + file ready variant + /// + public static bool TryReplaceAccent(this char value, out char[] result) + { + if (!accents.ContainsKey(value)) + { + result = null; + return false; + } + + result = accents[value]; + return true; + } + } +} diff --git a/zero.Core/Validation/SetupModelValidator.cs b/zero.Core/Validation/SetupModelValidator.cs new file mode 100644 index 00000000..7a767934 --- /dev/null +++ b/zero.Core/Validation/SetupModelValidator.cs @@ -0,0 +1,22 @@ +using FluentValidation; +using zero.Core.Entities.Setup; +using zero.Core.Extensions; + +namespace zero.Core.Validation +{ + public class SetupModelValidator : AbstractValidator + { + public SetupModelValidator() + { + RuleFor(x => x.User).NotNull(); + RuleFor(x => x.User.Email).NotEmpty().Email(); + RuleFor(x => x.User.Name).MaximumLength(40).NotEmpty(); + RuleFor(x => x.User.Password).MaximumLength(1024); // TODO password policy + + RuleFor(x => x.AppName).MaximumLength(40).NotEmpty(); + + RuleFor(x => x.Database.Url).NotEmpty().Url(); + RuleFor(x => x.Database.Name).NotEmpty(); + } + } +} diff --git a/zero.Core/Validation/SetupSaveValidator.cs b/zero.Core/Validation/SetupSaveValidator.cs deleted file mode 100644 index 42227f84..00000000 --- a/zero.Core/Validation/SetupSaveValidator.cs +++ /dev/null @@ -1,32 +0,0 @@ -using FluentValidation; -using zero.Core.Entities.Setup; -using zero.Core.Extensions; - -namespace zero.Core.Validation -{ - public class SetupSaveValidator : AbstractValidator - { - public SetupSaveValidator() - { - When(x => x.Part == SetupSavePart.ValidateUser, () => - { - RuleFor(x => x.User).NotNull(); - RuleFor(x => x.User.Email).NotEmpty().Email(); - RuleFor(x => x.User.Name).MaximumLength(40).NotEmpty(); - RuleFor(x => x.User.Password).MaximumLength(1024); // TODO password policy - }); - - When(x => x.Part == SetupSavePart.ValidateApplication, () => - { - RuleFor(x => x.AppName).MaximumLength(40).NotEmpty(); - }); - - When(x => x.Part == SetupSavePart.ValidateDatabase, () => - { - RuleFor(x => x.Database.Url).NotEmpty().Url(); - RuleFor(x => x.Database.Name).NotEmpty(); - }); - - } - } -} diff --git a/zero.Web/Controllers/SetupController.cs b/zero.Web/Controllers/SetupController.cs index da97d2b1..5d963b55 100644 --- a/zero.Web/Controllers/SetupController.cs +++ b/zero.Web/Controllers/SetupController.cs @@ -2,6 +2,8 @@ using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; using zero.Core; +using zero.Core.Api; +using zero.Core.Entities; using zero.Core.Entities.Setup; using zero.Web.Controllers; @@ -10,9 +12,12 @@ namespace zero.Web.Setup [AllowAnonymous] public class SetupController : BackofficeController { - public SetupController(IZeroConfiguration config) : base(config) - { + protected ISetupApi Api { get; private set; } + + public SetupController(IZeroConfiguration config, ISetupApi api) : base(config) + { + Api = api; } public IActionResult Index() @@ -21,9 +26,10 @@ namespace zero.Web.Setup } [HttpPost] - public async Task Save(SetupSave model) + public async Task Install([FromBody] SetupModel model) { - return Ok(); + EntityChangeResult result = await Api.Install(model); + return Json(result); } } } diff --git a/zero.Web/Sass/Core/_settings.scss b/zero.Web/Sass/Core/_settings.scss index 741f7e72..2aea8336 100644 --- a/zero.Web/Sass/Core/_settings.scss +++ b/zero.Web/Sass/Core/_settings.scss @@ -2,7 +2,7 @@ :root { // accent colors - --color-primary: #00aea2; + --color-primary: #f9c202; --color-secondary: #2f3a43; --color-negative: #d82853; diff --git a/zero.Web/Sass/Core/_typography.scss b/zero.Web/Sass/Core/_typography.scss index fc982a0c..9219d1c5 100644 --- a/zero.Web/Sass/Core/_typography.scss +++ b/zero.Web/Sass/Core/_typography.scss @@ -15,7 +15,7 @@ $font-headline-name: 'TwCenMt'; @include font-face($font-headline-name, '/Assets/Fonts/TwCenMt/twcenmt-regular', 400); -@include font-face($font-headline-name, '/Assets/Fonts/TwCenMt/TwCenMt-bold', 700); +@include font-face($font-headline-name, '/Assets/Fonts/TwCenMt/twcenmt-bold', 700); %font { diff --git a/zero.Web/Sass/Setup/_canvas.scss b/zero.Web/Sass/Setup/_canvas.scss index 27ad876f..7058fdf0 100644 --- a/zero.Web/Sass/Setup/_canvas.scss +++ b/zero.Web/Sass/Setup/_canvas.scss @@ -38,14 +38,16 @@ body .app { - width: 100%; - max-width: 520px; - background: var(--color-bg); - border-radius: 3px; - min-height: 600px; - position: relative; - z-index: 2; - padding: 40px; + height: 100%; +} + +.app-headline +{ + text-align: center; + color: white; + font-size: 64px; + font-weight: 400; + margin: -10vh 0 8vh; } body:before diff --git a/zero.Web/Setup/Steps/step-application.vue b/zero.Web/Setup/Steps/step-application.vue index 1e8b0db2..299ca922 100644 --- a/zero.Web/Setup/Steps/step-application.vue +++ b/zero.Web/Setup/Steps/step-application.vue @@ -4,7 +4,7 @@

With zero you can manage multiple applications with one installation. Start by adding your first application.

- + diff --git a/zero.Web/Setup/Steps/step-database.vue b/zero.Web/Setup/Steps/step-database.vue index 4468d80f..df29668c 100644 --- a/zero.Web/Setup/Steps/step-database.vue +++ b/zero.Web/Setup/Steps/step-database.vue @@ -4,11 +4,11 @@

Zero uses RavenDB as it’s database. If you want a more sophisticated setup (i.e. cluster) you can override the IDocumentStore service which is registered as a singleton.

- + - + diff --git a/zero.Web/Setup/Steps/step-install.vue b/zero.Web/Setup/Steps/step-install.vue index 29b5b1fd..84fe4334 100644 --- a/zero.Web/Setup/Steps/step-install.vue +++ b/zero.Web/Setup/Steps/step-install.vue @@ -1,28 +1,97 @@