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 @@
-