From fb2aa4f9d7a1403405877a6088a6564cdda0096e Mon Sep 17 00:00:00 2001 From: Tobias Klika Date: Wed, 22 Apr 2020 15:46:35 +0200 Subject: [PATCH] first draft of change tokens works --- zero.Core/Api/CountriesApi.cs | 46 +++++++- zero.Core/Api/Token.cs | 103 ++++++++++++++++++ zero.Core/Configuration.cs | 8 ++ zero.Core/Constants.cs | 1 + zero.Core/Entities/ChangeToken.cs | 13 +++ zero.Core/Entities/DatabaseEntity.cs | 2 +- zero.Core/Validation/CountryValidator.cs | 16 +++ zero.Web/App/Pages/settings/country.vue | 17 +-- zero.Web/App/components/forms/error-view.vue | 2 +- zero.Web/App/resources/countries.js | 6 + zero.Web/Controllers/BackofficeController.cs | 28 ++++- zero.Web/Controllers/CountriesController.cs | 18 ++- zero.Web/Controllers/UsersController.cs | 4 +- zero.Web/Filters/AddTokenAttribute.cs | 45 ++++++++ zero.Web/Filters/VerifyTokenAttribute.cs | 56 ++++++++++ zero.Web/Models/CountryEditModel.cs | 2 - zero.Web/Models/EditModel.cs | 5 + zero.Web/Models/UserEditModel.cs | 2 - .../Resources/Localization/zero.en-us.json | 2 + zero.Web/Sass/Modules/_box.scss | 4 +- zero.Web/Startup.cs | 1 + zero.Web/zeroSettings.json | 1 + 22 files changed, 353 insertions(+), 29 deletions(-) create mode 100644 zero.Core/Api/Token.cs create mode 100644 zero.Core/Entities/ChangeToken.cs create mode 100644 zero.Core/Validation/CountryValidator.cs create mode 100644 zero.Web/Filters/AddTokenAttribute.cs create mode 100644 zero.Web/Filters/VerifyTokenAttribute.cs diff --git a/zero.Core/Api/CountriesApi.cs b/zero.Core/Api/CountriesApi.cs index bbc511e7..562ca600 100644 --- a/zero.Core/Api/CountriesApi.cs +++ b/zero.Core/Api/CountriesApi.cs @@ -1,10 +1,13 @@ -using Raven.Client.Documents; +using FluentValidation.Results; +using Raven.Client.Documents; using Raven.Client.Documents.Linq; using Raven.Client.Documents.Session; +using System; using System.Collections.Generic; using System.Threading.Tasks; using zero.Core.Entities; using zero.Core.Extensions; +using zero.Core.Validation; namespace zero.Core.Api { @@ -57,6 +60,42 @@ namespace zero.Core.Api .ToQueriedListAsync(query); } } + + + /// + public async Task> Save(Country model) + { + ValidationResult validation = await new CountryValidator().ValidateAsync(model); + + if (!validation.IsValid) + { + return EntityResult.Fail(validation); + } + + if (model.Id.IsNullOrEmpty()) + { + model.AppId = "zero.applications.1-A"; // TODO real app id + model.CreatedDate = DateTimeOffset.Now; + } + + model.Alias = Alias.Generate(model.Name); + + using (IAsyncDocumentSession session = Raven.OpenAsyncSession()) + { + await session.StoreAsync(model); + + string id = session.Advanced.GetDocumentId(model); + + await session.SaveChangesAsync(); + + if (model.Id.IsNullOrEmpty()) + { + model.Id = id; + } + } + + return EntityResult.Success(model); + } } @@ -76,5 +115,10 @@ namespace zero.Core.Api /// Get all available countries (with query) /// Task> GetByQuery(string languageId, ListQuery query); + + /// + /// Creates or updates a country + /// + Task> Save(Country model); } } diff --git a/zero.Core/Api/Token.cs b/zero.Core/Api/Token.cs new file mode 100644 index 00000000..b73b482d --- /dev/null +++ b/zero.Core/Api/Token.cs @@ -0,0 +1,103 @@ +using Raven.Client.Documents; +using Raven.Client.Documents.Session; +using System; +using System.Threading.Tasks; +using zero.Core.Entities; +using zero.Core.Extensions; + +namespace zero.Core.Api +{ + public class Token : IToken + { + protected IDocumentStore Raven { get; private set; } + + protected IZeroConfiguration Config { get; private set; } + + private const string PREFIX = "ChangeTokens."; + + + public Token(IDocumentStore raven, IZeroConfiguration config) + { + Raven = raven; + Config = config; + } + + + /// + public async Task Verify(IZeroEntity entity, string token) + { + return await Verify(entity?.Id, token); + } + + + /// + public async Task Verify(string entityId, string token) + { + if (token.IsNullOrWhiteSpace() || entityId.IsNullOrEmpty()) + { + return false; + } + + using (IAsyncDocumentSession session = Raven.OpenAsyncSession()) + { + return await session.Query().AnyAsync(x => x.Id == token && x.ReferenceId == entityId); + } + } + + + /// + public async Task Get(IZeroEntity entity) + { + return await Get(entity?.Id); + } + + + /// + public async Task Get(string entityId) + { + if (entityId.IsNullOrEmpty()) + { + return null; + } + + ChangeToken token = new ChangeToken() + { + Id = Constants.Database.CollectionPrefix + PREFIX + Guid.NewGuid(), + ReferenceId = entityId + }; + + using (IAsyncDocumentSession session = Raven.OpenAsyncSession()) + { + await session.StoreAsync(token); + session.Advanced.GetMetadataFor(token)[Constants.Database.Expires] = DateTime.UtcNow.AddMinutes(Config.TokenExpiration); + await session.SaveChangesAsync(); + } + + return token.Id; + } + } + + + public interface IToken + { + /// + /// Verifies if the change token is valid for the entity + /// + Task Verify(IZeroEntity entity, string token); + + /// + /// Verifies if the change token is valid for the entity + /// + Task Verify(string entityId, string token); + + /// + /// Get a new change token for the entity + /// + Task Get(IZeroEntity entity); + + /// + /// Get a new change token for the entity + /// + Task Get(string entityId); + } +} diff --git a/zero.Core/Configuration.cs b/zero.Core/Configuration.cs index ea3a6ce6..def426dc 100644 --- a/zero.Core/Configuration.cs +++ b/zero.Core/Configuration.cs @@ -11,6 +11,9 @@ /// public string[] SupportedLanguages { get; private set; } = new string[] { "en-US", "de-DE" }; + /// + public int TokenExpiration { get; set; } + /// public RavenConfig Raven { get; set; } @@ -36,6 +39,11 @@ /// string[] SupportedLanguages { get; } + /// + /// Expiration in minutes of a generated change token for an entity + /// + int TokenExpiration { get; set; } + /// /// RavenDB configuration data /// diff --git a/zero.Core/Constants.cs b/zero.Core/Constants.cs index 870c7db8..4f915dce 100644 --- a/zero.Core/Constants.cs +++ b/zero.Core/Constants.cs @@ -26,6 +26,7 @@ public const string SharedAppId = "shared"; public const string CollectionPrefix = "zero."; public const string ReservationPrefix = "zero."; + public const string Expires = Raven.Client.Constants.Documents.Metadata.Expires; } public static class Sections diff --git a/zero.Core/Entities/ChangeToken.cs b/zero.Core/Entities/ChangeToken.cs new file mode 100644 index 00000000..07596304 --- /dev/null +++ b/zero.Core/Entities/ChangeToken.cs @@ -0,0 +1,13 @@ +namespace zero.Core.Entities +{ + /// + /// A change token holds a reference to a database entity + /// This is used to verify change requests for entities in the zero backoffice + /// + public class ChangeToken + { + public string Id { get; set; } + + public string ReferenceId { get; set; } + } +} diff --git a/zero.Core/Entities/DatabaseEntity.cs b/zero.Core/Entities/DatabaseEntity.cs index 05cc876e..3264847b 100644 --- a/zero.Core/Entities/DatabaseEntity.cs +++ b/zero.Core/Entities/DatabaseEntity.cs @@ -10,7 +10,7 @@ namespace zero.Core.Entities } [DebuggerDisplay("Id = {Id,nq}, Name = {Name}, Alias = {Alias}")] - public abstract class DatabaseEntity : IDatabaseEntity + public abstract class DatabaseEntity : IDatabaseEntity, IZeroEntity { /// [GenerateId] diff --git a/zero.Core/Validation/CountryValidator.cs b/zero.Core/Validation/CountryValidator.cs new file mode 100644 index 00000000..3e621b26 --- /dev/null +++ b/zero.Core/Validation/CountryValidator.cs @@ -0,0 +1,16 @@ +using FluentValidation; +using zero.Core.Entities; +using zero.Core.Entities.Setup; +using zero.Core.Extensions; + +namespace zero.Core.Validation +{ + public class CountryValidator : AbstractValidator + { + public CountryValidator() + { + RuleFor(x => x.Code).Length(2); + RuleFor(x => x.Name).Length(2, 120); + } + } +} diff --git a/zero.Web/App/Pages/settings/country.vue b/zero.Web/App/Pages/settings/country.vue index be51a263..239e93b0 100644 --- a/zero.Web/App/Pages/settings/country.vue +++ b/zero.Web/App/Pages/settings/country.vue @@ -11,10 +11,10 @@ -
+
- + @@ -23,7 +23,7 @@
-