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