Browse Source

Plan enforcement.

pull/65/head
Sebastian Stehle 9 years ago
parent
commit
fb59474bc3
  1. 4
      src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs
  2. 2
      src/Squidex.Read/Apps/IAppEntity.cs
  3. 21
      src/Squidex.Read/Apps/Services/IAppLimitsPlan.cs
  4. 21
      src/Squidex.Read/Apps/Services/IAppLimitsProvider.cs
  5. 26
      src/Squidex.Read/Apps/Services/Implementations/ConfigAppLimitsPlan.cs
  6. 56
      src/Squidex.Read/Apps/Services/Implementations/ConfigAppLimitsProvider.cs
  7. 24
      src/Squidex.Write/Apps/AppCommandHandler.cs
  8. 5
      src/Squidex.Write/Apps/AppContributors.cs
  9. 10
      src/Squidex.Write/Apps/AppDomainObject.cs
  10. 13
      src/Squidex/Config/Domain/ReadModule.cs
  11. 17
      src/Squidex/Config/MyUsageOptions.cs
  12. 19
      src/Squidex/Controllers/Api/Apps/AppClientsController.cs
  13. 21
      src/Squidex/Controllers/Api/Apps/AppContributorsController.cs
  14. 21
      src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs
  15. 26
      src/Squidex/Controllers/Api/Apps/Models/ContributorsDto.cs
  16. 24
      src/Squidex/Controllers/Api/Assets/AssetsController.cs
  17. 16
      src/Squidex/Controllers/Api/History/HistoryController.cs
  18. 5
      src/Squidex/Controllers/Api/Statistics/Models/CurrentCallsDto.cs
  19. 5
      src/Squidex/Controllers/Api/Statistics/Models/CurrentStorageDto.cs
  20. 17
      src/Squidex/Controllers/Api/Statistics/UsagesController.cs
  21. 23
      src/Squidex/Pipeline/AppApiFilter.cs
  22. 2
      src/Squidex/Pipeline/MustBeAppDeveloperAttribute.cs
  23. 2
      src/Squidex/Pipeline/MustBeAppEditorAttribute.cs
  24. 2
      src/Squidex/Pipeline/MustBeAppOwnerAttribute.cs
  25. 3
      src/Squidex/Startup.cs
  26. 10
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.html
  27. 44
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts
  28. 6
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html
  29. 9
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.scss
  30. 12
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts
  31. 34
      src/Squidex/app/shared/services/app-contributors.service.spec.ts
  32. 24
      src/Squidex/app/shared/services/app-contributors.service.ts
  33. 8
      src/Squidex/app/shared/services/usages.service.spec.ts
  34. 26
      src/Squidex/app/shared/services/usages.service.ts
  35. 18
      tests/Squidex.Infrastructure.Tests/CQRS/Events/CompoundEventConsumerTests.cs
  36. 14
      tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs
  37. 102
      tests/Squidex.Read.Tests/Apps/ConfigAppLimitsProviderTests.cs
  38. 26
      tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs
  39. 8
      tests/Squidex.Write.Tests/Assets/AssetCommandHandlerTests.cs

4
src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs

@ -32,6 +32,10 @@ namespace Squidex.Read.MongoDb.Apps
[BsonElement] [BsonElement]
public long Version { get; set; } public long Version { get; set; }
[BsonIgnoreIfDefault]
[BsonElement]
public int PlanId { get; set; }
[BsonRequired] [BsonRequired]
[BsonElement] [BsonElement]
public string MasterLanguage { get; set; } public string MasterLanguage { get; set; }

2
src/Squidex.Read/Apps/IAppEntity.cs

@ -15,6 +15,8 @@ namespace Squidex.Read.Apps
{ {
string Name { get; } string Name { get; }
int PlanId { get; }
LanguagesConfig LanguagesConfig { get; } LanguagesConfig LanguagesConfig { get; }
IReadOnlyCollection<IAppClientEntity> Clients { get; } IReadOnlyCollection<IAppClientEntity> Clients { get; }

21
src/Squidex.Read/Apps/Services/IAppLimitsPlan.cs

@ -0,0 +1,21 @@
// ==========================================================================
// IAppLimitsPlan.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Read.Apps.Services
{
public interface IAppLimitsPlan
{
string Name { get; }
long MaxApiCalls { get; }
long MaxAssetSize { get; }
int MaxContributors { get; }
}
}

21
src/Squidex.Read/Apps/Services/IAppLimitsProvider.cs

@ -0,0 +1,21 @@
// ==========================================================================
// IAppLimitsProvider.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Read.Apps.Services
{
public interface IAppLimitsProvider
{
IEnumerable<IAppLimitsPlan> GetAvailablePlans();
IAppLimitsPlan GetPlanForApp(IAppEntity entity);
IAppLimitsPlan GetPlan(int planId);
}
}

26
src/Squidex.Read/Apps/Services/Implementations/ConfigAppLimitsPlan.cs

@ -0,0 +1,26 @@
// ==========================================================================
// ConfigAppLimitsPlan.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Read.Apps.Services.Implementations
{
public sealed class ConfigAppLimitsPlan : IAppLimitsPlan
{
public string Name { get; set; }
public long MaxApiCalls { get; set; }
public long MaxAssetSize { get; set; }
public int MaxContributors { get; set; }
public ConfigAppLimitsPlan Clone()
{
return (ConfigAppLimitsPlan)MemberwiseClone();
}
}
}

56
src/Squidex.Read/Apps/Services/Implementations/ConfigAppLimitsProvider.cs

@ -0,0 +1,56 @@
// ==========================================================================
// ConfigAppLimitsProvider.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using Squidex.Infrastructure;
namespace Squidex.Read.Apps.Services.Implementations
{
public sealed class ConfigAppLimitsProvider : IAppLimitsProvider
{
private static readonly ConfigAppLimitsPlan Infinite = new ConfigAppLimitsPlan
{
Name = "Infinite",
MaxApiCalls = -1,
MaxAssetSize = -1,
MaxContributors = -1
};
private readonly List<ConfigAppLimitsPlan> config;
public ConfigAppLimitsProvider(IEnumerable<ConfigAppLimitsPlan> config)
{
Guard.NotNull(config, nameof(config));
this.config = config.Select(c => c.Clone()).OrderBy(x => x.MaxApiCalls).ToList();
}
public IEnumerable<IAppLimitsPlan> GetAvailablePlans()
{
return config;
}
public IAppLimitsPlan GetPlanForApp(IAppEntity app)
{
Guard.NotNull(app, nameof(app));
return GetPlan(app.PlanId);
}
public IAppLimitsPlan GetPlan(int planId)
{
if (planId >= 0 && planId < config.Count)
{
return config[planId];
}
return config.FirstOrDefault() ?? Infinite;
}
}
}

24
src/Squidex.Write/Apps/AppCommandHandler.cs

@ -12,21 +12,26 @@ using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
using Squidex.Read.Apps.Repositories; using Squidex.Read.Apps.Repositories;
using Squidex.Read.Apps.Services;
using Squidex.Read.Users.Repositories; using Squidex.Read.Users.Repositories;
using Squidex.Write.Apps.Commands; using Squidex.Write.Apps.Commands;
// ReSharper disable InvertIf
namespace Squidex.Write.Apps namespace Squidex.Write.Apps
{ {
public class AppCommandHandler : ICommandHandler public class AppCommandHandler : ICommandHandler
{ {
private readonly IAggregateHandler handler; private readonly IAggregateHandler handler;
private readonly IAppRepository appRepository; private readonly IAppRepository appRepository;
private readonly IAppLimitsProvider appLimitsProvider;
private readonly IUserRepository userRepository; private readonly IUserRepository userRepository;
private readonly ClientKeyGenerator keyGenerator; private readonly ClientKeyGenerator keyGenerator;
public AppCommandHandler( public AppCommandHandler(
IAggregateHandler handler, IAggregateHandler handler,
IAppRepository appRepository, IAppRepository appRepository,
IAppLimitsProvider appLimitsProvider,
IUserRepository userRepository, IUserRepository userRepository,
ClientKeyGenerator keyGenerator) ClientKeyGenerator keyGenerator)
{ {
@ -34,11 +39,13 @@ namespace Squidex.Write.Apps
Guard.NotNull(keyGenerator, nameof(keyGenerator)); Guard.NotNull(keyGenerator, nameof(keyGenerator));
Guard.NotNull(appRepository, nameof(appRepository)); Guard.NotNull(appRepository, nameof(appRepository));
Guard.NotNull(userRepository, nameof(userRepository)); Guard.NotNull(userRepository, nameof(userRepository));
Guard.NotNull(appLimitsProvider, nameof(appLimitsProvider));
this.handler = handler; this.handler = handler;
this.keyGenerator = keyGenerator; this.keyGenerator = keyGenerator;
this.appRepository = appRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
this.appRepository = appRepository;
this.appLimitsProvider = appLimitsProvider;
} }
protected async Task On(CreateApp command, CommandContext context) protected async Task On(CreateApp command, CommandContext context)
@ -71,7 +78,20 @@ namespace Squidex.Write.Apps
throw new ValidationException("Cannot assign contributor to app", error); throw new ValidationException("Cannot assign contributor to app", error);
} }
await handler.UpdateAsync<AppDomainObject>(context, a => a.AssignContributor(command)); await handler.UpdateAsync<AppDomainObject>(context, a =>
{
var oldContributors = a.ContributorCount;
var maxContributors = appLimitsProvider.GetPlan(a.PlanId).MaxContributors;
a.AssignContributor(command);
if (a.ContributorCount > oldContributors && a.ContributorCount > maxContributors)
{
var error = new ValidationError("You have reached your max number of contributors");
throw new ValidationException("Cannot assign contributor to app", error);
}
});
} }
protected Task On(AttachClient command, CommandContext context) protected Task On(AttachClient command, CommandContext context)

5
src/Squidex.Write/Apps/AppContributors.cs

@ -20,6 +20,11 @@ namespace Squidex.Write.Apps
{ {
private readonly Dictionary<string, PermissionLevel> contributors = new Dictionary<string, PermissionLevel>(); private readonly Dictionary<string, PermissionLevel> contributors = new Dictionary<string, PermissionLevel>();
public int Count
{
get { return contributors.Count; }
}
public void Assign(string contributorId, PermissionLevel permission) public void Assign(string contributorId, PermissionLevel permission)
{ {
string Message() => "Cannot assign contributor"; string Message() => "Cannot assign contributor";

10
src/Squidex.Write/Apps/AppDomainObject.cs

@ -36,6 +36,16 @@ namespace Squidex.Write.Apps
get { return name; } get { return name; }
} }
public int PlanId
{
get { return 0; }
}
public int ContributorCount
{
get { return contributors.Count; }
}
public IReadOnlyDictionary<string, AppClient> Clients public IReadOnlyDictionary<string, AppClient> Clients
{ {
get { return clients.Clients; } get { return clients.Clients; }

13
src/Squidex/Config/Domain/ReadModule.cs

@ -6,8 +6,11 @@
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using System.Linq;
using Autofac; using Autofac;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Squidex.Read.Apps; using Squidex.Read.Apps;
using Squidex.Read.Apps.Services; using Squidex.Read.Apps.Services;
using Squidex.Read.Apps.Services.Implementations; using Squidex.Read.Apps.Services.Implementations;
@ -33,11 +36,21 @@ namespace Squidex.Config.Domain
protected override void Load(ContainerBuilder builder) protected override void Load(ContainerBuilder builder)
{ {
builder.Register(c => c.Resolve<IOptions<MyUsageOptions>>().Value?.Plans ?? Enumerable.Empty<ConfigAppLimitsPlan>())
.As<IEnumerable<ConfigAppLimitsPlan>>()
.AsSelf()
.SingleInstance();
builder.RegisterType<CachingAppProvider>() builder.RegisterType<CachingAppProvider>()
.As<IAppProvider>() .As<IAppProvider>()
.AsSelf() .AsSelf()
.SingleInstance(); .SingleInstance();
builder.RegisterType<ConfigAppLimitsProvider>()
.As<IAppLimitsProvider>()
.AsSelf()
.SingleInstance();
builder.RegisterType<CachingSchemaProvider>() builder.RegisterType<CachingSchemaProvider>()
.As<ISchemaProvider>() .As<ISchemaProvider>()
.AsSelf() .AsSelf()

17
src/Squidex/Config/MyUsageOptions.cs

@ -0,0 +1,17 @@
// ==========================================================================
// MyUsageOptions.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Read.Apps.Services.Implementations;
namespace Squidex.Config
{
public class MyUsageOptions
{
public ConfigAppLimitsPlan[] Plans { get; set; }
}
}

19
src/Squidex/Controllers/Api/Apps/AppClientsController.cs

@ -15,7 +15,6 @@ using Squidex.Controllers.Api.Apps.Models;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Read.Apps.Services;
using Squidex.Write.Apps; using Squidex.Write.Apps;
using Squidex.Write.Apps.Commands; using Squidex.Write.Apps.Commands;
@ -30,12 +29,9 @@ namespace Squidex.Controllers.Api.Apps
[SwaggerTag("Apps")] [SwaggerTag("Apps")]
public class AppClientsController : ControllerBase public class AppClientsController : ControllerBase
{ {
private readonly IAppProvider appProvider; public AppClientsController(ICommandBus commandBus)
public AppClientsController(ICommandBus commandBus, IAppProvider appProvider)
: base(commandBus) : base(commandBus)
{ {
this.appProvider = appProvider;
} }
/// <summary> /// <summary>
@ -53,18 +49,11 @@ namespace Squidex.Controllers.Api.Apps
[Route("apps/{app}/clients/")] [Route("apps/{app}/clients/")]
[ProducesResponseType(typeof(ClientDto[]), 200)] [ProducesResponseType(typeof(ClientDto[]), 200)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> GetClients(string app) public IActionResult GetClients(string app)
{ {
var entity = await appProvider.FindAppByNameAsync(app); var response = App.Clients.Select(x => SimpleMapper.Map(x, new ClientDto())).ToList();
if (entity == null)
{
return NotFound();
}
var response = entity.Clients.Select(x => SimpleMapper.Map(x, new ClientDto())).ToList();
Response.Headers["ETag"] = new StringValues(entity.Version.ToString()); Response.Headers["ETag"] = new StringValues(App.Version.ToString());
return Ok(response); return Ok(response);
} }

21
src/Squidex/Controllers/Api/Apps/AppContributorsController.cs

@ -29,12 +29,12 @@ namespace Squidex.Controllers.Api.Apps
[SwaggerTag("Apps")] [SwaggerTag("Apps")]
public class AppContributorsController : ControllerBase public class AppContributorsController : ControllerBase
{ {
private readonly IAppProvider appProvider; private readonly IAppLimitsProvider appLimitsProvider;
public AppContributorsController(ICommandBus commandBus, IAppProvider appProvider) public AppContributorsController(ICommandBus commandBus, IAppLimitsProvider appLimitsProvider)
: base(commandBus) : base(commandBus)
{ {
this.appProvider = appProvider; this.appLimitsProvider = appLimitsProvider;
} }
/// <summary> /// <summary>
@ -47,20 +47,15 @@ namespace Squidex.Controllers.Api.Apps
/// </returns> /// </returns>
[HttpGet] [HttpGet]
[Route("apps/{app}/contributors/")] [Route("apps/{app}/contributors/")]
[ProducesResponseType(typeof(ContributorDto[]), 200)] [ProducesResponseType(typeof(ContributorsDto), 200)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> GetContributors(string app) public IActionResult GetContributors(string app)
{ {
var entity = await appProvider.FindAppByNameAsync(app); var contributors = App.Contributors.Select(x => SimpleMapper.Map(x, new ContributorDto())).ToArray();
if (entity == null) var response = new ContributorsDto { Contributors = contributors, MaxContributors = appLimitsProvider.GetPlanForApp(App).MaxContributors };
{
return NotFound();
}
var response = entity.Contributors.Select(x => SimpleMapper.Map(x, new ContributorDto())).ToList(); Response.Headers["ETag"] = new StringValues(App.Version.ToString());
Response.Headers["ETag"] = new StringValues(entity.Version.ToString());
return Ok(response); return Ok(response);
} }

21
src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs

@ -19,7 +19,6 @@ using Squidex.Controllers.Api.Apps.Models;
using Squidex.Core; using Squidex.Core;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Read.Apps.Services;
using Squidex.Write.Apps.Commands; using Squidex.Write.Apps.Commands;
namespace Squidex.Controllers.Api.Apps namespace Squidex.Controllers.Api.Apps
@ -32,12 +31,9 @@ namespace Squidex.Controllers.Api.Apps
[SwaggerTag("Apps")] [SwaggerTag("Apps")]
public class AppLanguagesController : ControllerBase public class AppLanguagesController : ControllerBase
{ {
private readonly IAppProvider appProvider; public AppLanguagesController(ICommandBus commandBus)
public AppLanguagesController(ICommandBus commandBus, IAppProvider appProvider)
: base(commandBus) : base(commandBus)
{ {
this.appProvider = appProvider;
} }
/// <summary> /// <summary>
@ -52,25 +48,18 @@ namespace Squidex.Controllers.Api.Apps
[HttpGet] [HttpGet]
[Route("apps/{app}/languages/")] [Route("apps/{app}/languages/")]
[ProducesResponseType(typeof(LanguageDto[]), 200)] [ProducesResponseType(typeof(LanguageDto[]), 200)]
public async Task<IActionResult> GetLanguages(string app) public IActionResult GetLanguages(string app)
{ {
var entity = await appProvider.FindAppByNameAsync(app); var model = App.LanguagesConfig.OfType<LanguageConfig>().Select(x =>
if (entity == null)
{
return NotFound();
}
var model = entity.LanguagesConfig.OfType<LanguageConfig>().Select(x =>
SimpleMapper.Map(x.Language, SimpleMapper.Map(x.Language,
new AppLanguageDto new AppLanguageDto
{ {
IsMaster = x == entity.LanguagesConfig.Master, IsMaster = x == App.LanguagesConfig.Master,
IsOptional = x.IsOptional, IsOptional = x.IsOptional,
Fallback = x.Fallback.ToList() Fallback = x.Fallback.ToList()
})).OrderByDescending(x => x.IsMaster).ThenBy(x => x.Iso2Code).ToList(); })).OrderByDescending(x => x.IsMaster).ThenBy(x => x.Iso2Code).ToList();
Response.Headers["ETag"] = new StringValues(entity.Version.ToString()); Response.Headers["ETag"] = new StringValues(App.Version.ToString());
return Ok(model); return Ok(model);
} }

26
src/Squidex/Controllers/Api/Apps/Models/ContributorsDto.cs

@ -0,0 +1,26 @@
// ==========================================================================
// ContributorsDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
namespace Squidex.Controllers.Api.Apps.Models
{
public class ContributorsDto
{
/// <summary>
/// The contributors.
/// </summary>
[Required]
public ContributorDto[] Contributors { get; set; }
/// <summary>
/// The maximum number of allowed contributors.
/// </summary>
public int MaxContributors { get; set; }
}
}

24
src/Squidex/Controllers/Api/Assets/AssetsController.cs

@ -21,6 +21,7 @@ using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Read.Apps.Services;
using Squidex.Read.Assets.Repositories; using Squidex.Read.Assets.Repositories;
using Squidex.Write.Assets.Commands; using Squidex.Write.Assets.Commands;
@ -36,16 +37,22 @@ namespace Squidex.Controllers.Api.Assets
public class AssetsController : ControllerBase public class AssetsController : ControllerBase
{ {
private readonly IAssetRepository assetRepository; private readonly IAssetRepository assetRepository;
private readonly IAssetStatsRepository assetStatsRepository;
private readonly IAppLimitsProvider appLimitProvider;
private readonly AssetConfig assetsConfig; private readonly AssetConfig assetsConfig;
public AssetsController( public AssetsController(
ICommandBus commandBus, ICommandBus commandBus,
IAssetRepository assetRepository, IAssetRepository assetRepository,
IAssetStatsRepository assetStatsRepository,
IAppLimitsProvider appLimitProvider,
IOptions<AssetConfig> assetsConfig) IOptions<AssetConfig> assetsConfig)
: base(commandBus) : base(commandBus)
{ {
this.assetsConfig = assetsConfig.Value; this.assetsConfig = assetsConfig.Value;
this.assetRepository = assetRepository; this.assetRepository = assetRepository;
this.assetStatsRepository = assetStatsRepository;
this.appLimitProvider = appLimitProvider;
} }
/// <summary> /// <summary>
@ -149,7 +156,7 @@ namespace Squidex.Controllers.Api.Assets
[ProducesResponseType(typeof(ErrorDto), 400)] [ProducesResponseType(typeof(ErrorDto), 400)]
public async Task<IActionResult> PostAsset(string app, List<IFormFile> file) public async Task<IActionResult> PostAsset(string app, List<IFormFile> file)
{ {
var assetFile = GetAssetFile(file); var assetFile = await CheckAssetFileAsync(file);
var command = new CreateAsset { File = assetFile }; var command = new CreateAsset { File = assetFile };
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
@ -178,7 +185,7 @@ namespace Squidex.Controllers.Api.Assets
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PutAssetContent(string app, Guid id, List<IFormFile> file) public async Task<IActionResult> PutAssetContent(string app, Guid id, List<IFormFile> file)
{ {
var assetFile = GetAssetFile(file); var assetFile = await CheckAssetFileAsync(file);
var command = new UpdateAsset { File = assetFile, AssetId = id }; var command = new UpdateAsset { File = assetFile, AssetId = id };
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
@ -232,7 +239,7 @@ namespace Squidex.Controllers.Api.Assets
return NoContent(); return NoContent();
} }
private AssetFile GetAssetFile(IReadOnlyList<IFormFile> file) private async Task<AssetFile> CheckAssetFileAsync(IReadOnlyList<IFormFile> file)
{ {
if (file.Count != 1) if (file.Count != 1)
{ {
@ -250,6 +257,17 @@ namespace Squidex.Controllers.Api.Assets
throw new ValidationException("Cannot create asset.", error); throw new ValidationException("Cannot create asset.", error);
} }
var plan = appLimitProvider.GetPlanForApp(App);
var currentSize = await assetStatsRepository.GetTotalSizeAsync(App.Id);
if (plan.MaxAssetSize > 0 && plan.MaxAssetSize < currentSize + formFile.Length)
{
var error = new ValidationError("You have reached your max asset size.");
throw new ValidationException("Cannot create asset.", error);
}
var assetFile = new AssetFile(formFile.FileName, formFile.ContentType, formFile.Length, formFile.OpenReadStream); var assetFile = new AssetFile(formFile.FileName, formFile.ContentType, formFile.Length, formFile.OpenReadStream);
return assetFile; return assetFile;

16
src/Squidex/Controllers/Api/History/HistoryController.cs

@ -29,14 +29,11 @@ namespace Squidex.Controllers.Api.History
[SwaggerTag("History")] [SwaggerTag("History")]
public class HistoryController : ControllerBase public class HistoryController : ControllerBase
{ {
private readonly IAppProvider appProvider;
private readonly IHistoryEventRepository historyEventRepository; private readonly IHistoryEventRepository historyEventRepository;
public HistoryController(ICommandBus commandBus, IAppProvider appProvider, IHistoryEventRepository historyEventRepository) public HistoryController(ICommandBus commandBus, IHistoryEventRepository historyEventRepository)
: base(commandBus) : base(commandBus)
{ {
this.appProvider = appProvider;
this.historyEventRepository = historyEventRepository; this.historyEventRepository = historyEventRepository;
} }
@ -55,16 +52,9 @@ namespace Squidex.Controllers.Api.History
[ApiCosts(0.1)] [ApiCosts(0.1)]
public async Task<IActionResult> GetHistory(string app, string channel) public async Task<IActionResult> GetHistory(string app, string channel)
{ {
var entity = await appProvider.FindAppByNameAsync(app); var entities = await historyEventRepository.QueryByChannelAsync(App.Id, channel, 100);
if (entity == null)
{
return NotFound();
}
var schemas = await historyEventRepository.QueryByChannelAsync(entity.Id, channel, 100);
var response = schemas.Select(x => SimpleMapper.Map(x, new HistoryEventDto())).ToList(); var response = entities.Select(x => SimpleMapper.Map(x, new HistoryEventDto())).ToList();
return Ok(response); return Ok(response);
} }

5
src/Squidex/Controllers/Api/Statistics/Models/CurrentCallsDto.cs

@ -14,5 +14,10 @@ namespace Squidex.Controllers.Api.Statistics.Models
/// The number of calls. /// The number of calls.
/// </summary> /// </summary>
public long Count { get; set; } public long Count { get; set; }
/// <summary>
/// The number of maximum allowed calls.
/// </summary>
public long MaxAllowed { get; set; }
} }
} }

5
src/Squidex/Controllers/Api/Statistics/Models/CurrentStorageDto.cs

@ -14,5 +14,10 @@ namespace Squidex.Controllers.Api.Statistics.Models
/// The size in bytes. /// The size in bytes.
/// </summary> /// </summary>
public long Size { get; set; } public long Size { get; set; }
/// <summary>
/// The maximum allowed asset size.
/// </summary>
public long MaxAllowed { get; set; }
} }
} }

17
src/Squidex/Controllers/Api/Statistics/UsagesController.cs

@ -15,6 +15,7 @@ using Squidex.Controllers.Api.Statistics.Models;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.UsageTracking; using Squidex.Infrastructure.UsageTracking;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Read.Apps.Services;
using Squidex.Read.Assets.Repositories; using Squidex.Read.Assets.Repositories;
namespace Squidex.Controllers.Api.Statistics namespace Squidex.Controllers.Api.Statistics
@ -28,13 +29,19 @@ namespace Squidex.Controllers.Api.Statistics
public class UsagesController : ControllerBase public class UsagesController : ControllerBase
{ {
private readonly IUsageTracker usageTracker; private readonly IUsageTracker usageTracker;
private readonly IAppLimitsProvider appLimitProvider;
private readonly IAssetStatsRepository assetStatsRepository; private readonly IAssetStatsRepository assetStatsRepository;
public UsagesController(ICommandBus commandBus, IUsageTracker usageTracker, IAssetStatsRepository assetStatsRepository) public UsagesController(
ICommandBus commandBus,
IUsageTracker usageTracker,
IAppLimitsProvider appLimitProvider,
IAssetStatsRepository assetStatsRepository)
: base(commandBus) : base(commandBus)
{ {
this.usageTracker = usageTracker; this.usageTracker = usageTracker;
this.appLimitProvider = appLimitProvider;
this.assetStatsRepository = assetStatsRepository; this.assetStatsRepository = assetStatsRepository;
} }
@ -55,7 +62,9 @@ namespace Squidex.Controllers.Api.Statistics
{ {
var count = await usageTracker.GetMonthlyCalls(App.Id.ToString(), DateTime.Today); var count = await usageTracker.GetMonthlyCalls(App.Id.ToString(), DateTime.Today);
return Ok(new CurrentCallsDto { Count = count }); var plan = appLimitProvider.GetPlanForApp(App);
return Ok(new CurrentCallsDto { Count = count, MaxAllowed = plan.MaxApiCalls });
} }
/// <summary> /// <summary>
@ -110,7 +119,9 @@ namespace Squidex.Controllers.Api.Statistics
{ {
var size = await assetStatsRepository.GetTotalSizeAsync(App.Id); var size = await assetStatsRepository.GetTotalSizeAsync(App.Id);
return Ok(new CurrentStorageDto { Size = size }); var plan = appLimitProvider.GetPlanForApp(App);
return Ok(new CurrentStorageDto { Size = size, MaxAllowed = plan.MaxAssetSize });
} }
/// <summary> /// <summary>

23
src/Squidex/Pipeline/AppApiFilter.cs

@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Mvc.Filters;
using Squidex.Core.Apps; using Squidex.Core.Apps;
using Squidex.Core.Identity; using Squidex.Core.Identity;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Infrastructure.UsageTracking;
using Squidex.Read.Apps; using Squidex.Read.Apps;
using Squidex.Read.Apps.Services; using Squidex.Read.Apps.Services;
@ -25,10 +26,15 @@ namespace Squidex.Pipeline
public sealed class AppApiFilter : IAsyncAuthorizationFilter public sealed class AppApiFilter : IAsyncAuthorizationFilter
{ {
private readonly IAppProvider appProvider; private readonly IAppProvider appProvider;
private readonly IAppLimitsProvider appLimitProvider;
private readonly IUsageTracker usageTracker;
public AppApiFilter(IAppProvider appProvider) public AppApiFilter(IAppProvider appProvider, IAppLimitsProvider appLimitProvider, IUsageTracker usageTracker)
{ {
this.appProvider = appProvider; this.appProvider = appProvider;
this.appLimitProvider = appLimitProvider;
this.usageTracker = usageTracker;
} }
public async Task OnAuthorizationAsync(AuthorizationFilterContext context) public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
@ -56,6 +62,16 @@ namespace Squidex.Pipeline
context.Result = new NotFoundResult(); context.Result = new NotFoundResult();
return; return;
} }
var plan = appLimitProvider.GetPlanForApp(app);
var usage = await usageTracker.GetMonthlyCalls(app.Id.ToString(), DateTime.Today);
if (plan.MaxApiCalls >= 0 && (usage * 1.1) > plan.MaxApiCalls)
{
context.Result = new StatusCodeResult(429);
return;
}
var defaultIdentity = context.HttpContext.User.Identities.First(); var defaultIdentity = context.HttpContext.User.Identities.First();
@ -63,12 +79,15 @@ namespace Squidex.Pipeline
{ {
case PermissionLevel.Owner: case PermissionLevel.Owner:
defaultIdentity.AddClaim(new Claim(defaultIdentity.RoleClaimType, SquidexRoles.AppOwner)); defaultIdentity.AddClaim(new Claim(defaultIdentity.RoleClaimType, SquidexRoles.AppOwner));
defaultIdentity.AddClaim(new Claim(defaultIdentity.RoleClaimType, SquidexRoles.AppDeveloper));
defaultIdentity.AddClaim(new Claim(defaultIdentity.RoleClaimType, SquidexRoles.AppEditor));
break; break;
case PermissionLevel.Editor: case PermissionLevel.Editor:
defaultIdentity.AddClaim(new Claim(defaultIdentity.RoleClaimType, SquidexRoles.AppDeveloper));
defaultIdentity.AddClaim(new Claim(defaultIdentity.RoleClaimType, SquidexRoles.AppEditor)); defaultIdentity.AddClaim(new Claim(defaultIdentity.RoleClaimType, SquidexRoles.AppEditor));
break; break;
case PermissionLevel.Developer: case PermissionLevel.Developer:
defaultIdentity.AddClaim(new Claim(defaultIdentity.RoleClaimType, SquidexRoles.AppDeveloper)); defaultIdentity.AddClaim(new Claim(defaultIdentity.RoleClaimType, SquidexRoles.AppEditor));
break; break;
} }

2
src/Squidex/Pipeline/MustBeAppDeveloperAttribute.cs

@ -15,7 +15,7 @@ namespace Squidex.Pipeline
{ {
public MustBeAppDeveloperAttribute() public MustBeAppDeveloperAttribute()
{ {
Roles = $"{SquidexRoles.AppOwner},{SquidexRoles.AppDeveloper}"; Roles = SquidexRoles.AppDeveloper;
} }
} }
} }

2
src/Squidex/Pipeline/MustBeAppEditorAttribute.cs

@ -15,7 +15,7 @@ namespace Squidex.Pipeline
{ {
public MustBeAppEditorAttribute() public MustBeAppEditorAttribute()
{ {
Roles = $"{SquidexRoles.AppOwner},{SquidexRoles.AppDeveloper},{SquidexRoles.AppEditor}"; Roles = SquidexRoles.AppEditor;
} }
} }
} }

2
src/Squidex/Pipeline/MustBeAppOwnerAttribute.cs

@ -15,7 +15,7 @@ namespace Squidex.Pipeline
{ {
public MustBeAppOwnerAttribute() public MustBeAppOwnerAttribute()
{ {
Roles = $"{SquidexRoles.AppOwner}"; Roles = SquidexRoles.AppOwner;
} }
} }
} }

3
src/Squidex/Startup.cs

@ -24,6 +24,7 @@ using Squidex.Config.Swagger;
using Squidex.Config.Web; using Squidex.Config.Web;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Log.Adapter; using Squidex.Infrastructure.Log.Adapter;
using Squidex.Read.Apps.Services.Implementations;
// ReSharper disable ConvertClosureToMethodGroup // ReSharper disable ConvertClosureToMethodGroup
// ReSharper disable AccessToModifiedClosure // ReSharper disable AccessToModifiedClosure
@ -75,6 +76,8 @@ namespace Squidex
Configuration.GetSection("urls")); Configuration.GetSection("urls"));
services.Configure<MyIdentityOptions>( services.Configure<MyIdentityOptions>(
Configuration.GetSection("identity")); Configuration.GetSection("identity"));
services.Configure<MyUsageOptions>(
Configuration.GetSection("usage"));
var builder = new ContainerBuilder(); var builder = new ContainerBuilder();
builder.Populate(services); builder.Populate(services);

10
src/Squidex/app/features/dashboard/pages/dashboard-page.component.html

@ -70,9 +70,10 @@
<div class="card card"> <div class="card card">
<div class="card-block"> <div class="card-block">
<div class="aggregation" *ngIf="currentCalls"> <div class="aggregation" *ngIf="callsCurrent">
<div class="aggregation-label">API calls this month</div> <div class="aggregation-label">API calls this month</div>
<div class="aggregation-value">{{currentCalls}}</div> <div class="aggregation-value">{{callsCurrent}}</div>
<div class="aggregation-label" *ngIf="callsMax">Monthly limit: {{callsMax}}</div>
</div> </div>
</div> </div>
</div> </div>
@ -85,9 +86,10 @@
<div class="card card"> <div class="card card">
<div class="card-block"> <div class="card-block">
<div class="aggregation" *ngIf="currentStorage"> <div class="aggregation" *ngIf="assetsCurrent">
<div class="aggregation-label">Asset size today</div> <div class="aggregation-label">Asset size today</div>
<div class="aggregation-value">{{currentStorage}}</div> <div class="aggregation-value">{{assetsCurrent}}</div>
<div class="aggregation-label" *ngIf="callsMax">Total limit: {{assetsMax}}</div>
</div> </div>
</div> </div>
</div> </div>

44
src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts

@ -51,8 +51,11 @@ export class DashboardPageComponent extends AppComponentBase implements OnInit,
maintainAspectRatio: false maintainAspectRatio: false
}; };
public currentStorage: string | null = null; public assetsCurrent: string | null = null;
public currentCalls: string | null = null; public assetsMax: string | null = null;
public callsCurrent: string | null = null;
public callsMax: string | null = null;
constructor(apps: AppsStoreService, notifications: NotificationService, constructor(apps: AppsStoreService, notifications: NotificationService,
private readonly auth: AuthService, private readonly auth: AuthService,
@ -69,26 +72,15 @@ export class DashboardPageComponent extends AppComponentBase implements OnInit,
this.appName() this.appName()
.switchMap(app => this.usagesService.getTodayStorage(app)) .switchMap(app => this.usagesService.getTodayStorage(app))
.subscribe(dto => { .subscribe(dto => {
this.currentStorage = FileHelper.fileSize(dto.size); this.assetsCurrent = FileHelper.fileSize(dto.size);
this.assetsMax = FileHelper.fileSize(dto.maxAllowed);
}); });
this.appName() this.appName()
.switchMap(app => this.usagesService.getMonthCalls(app)) .switchMap(app => this.usagesService.getMonthCalls(app))
.subscribe(dto => { .subscribe(dto => {
let count = dto.count; this.callsCurrent = formatCalls(dto.count);
this.callsMax = formatCalls(dto.maxAllowed);
if (count > 1000) {
count = count / 1000;
if (count < 10) {
count = Math.round(count * 10) / 10;
} else {
count = Math.round(count);
}
this.currentCalls = count + 'k';
} else {
this.currentCalls = count.toString();
}
}); });
this.appName() this.appName()
@ -170,6 +162,24 @@ export class DashboardPageComponent extends AppComponentBase implements OnInit,
} }
} }
function formatCalls(count: number): string {
if (count > 1000) {
count = count / 1000;
if (count < 10) {
count = Math.round(count * 10) / 10;
} else {
count = Math.round(count);
}
return count + 'k';
} else if (count < 0) {
return undefined;
} else {
return count.toString();
}
}
function createLabels(dtos: { date: DateTime }[]): string[] { function createLabels(dtos: { date: DateTime }[]): string[] {
const labels: string[] = []; const labels: string[] = [];

6
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html

@ -13,6 +13,10 @@
<div class="panel-main"> <div class="panel-main">
<div class="panel-content panel-content-scroll"> <div class="panel-content panel-content-scroll">
<div class="contributors-limit" *ngIf="maxContributors > 0">
Your plan allows up to {{maxContributors}} contributors.
</div>
<table class="table table-items table-fixed"> <table class="table table-items table-fixed">
<colgroup> <colgroup>
<col style="width: 70px" /> <col style="width: 70px" />
@ -56,7 +60,7 @@
<sqx-autocomplete [source]="usersDataSource" formControlName="user" [inputName]="'contributor'" placeholder="Find existing user"></sqx-autocomplete> <sqx-autocomplete [source]="usersDataSource" formControlName="user" [inputName]="'contributor'" placeholder="Find existing user"></sqx-autocomplete>
</div> </div>
<button type="submit" class="btn btn-success" [disabled]="!addContributorForm.valid">Add Contributor</button> <button type="submit" class="btn btn-success" [disabled]="!canAddContributor">Add Contributor</button>
</form> </form>
</div> </div>
</div> </div>

9
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.scss

@ -11,4 +11,13 @@
font-style: italic; font-style: italic;
font-size: .8rem; font-size: .8rem;
} }
}
.contributors-limit {
margin: -$panel-padding;
margin-bottom: $panel-padding;
color: $color-dark-foreground;
background: $color-theme-green-dark;
border: 0;
padding: .5 * $panel-padding $panel-padding;
} }

12
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts

@ -64,6 +64,8 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni
public currentUserId: string; public currentUserId: string;
public maxContributors = -1;
public usersDataSource: UsersDataSource; public usersDataSource: UsersDataSource;
public usersPermissions = [ public usersPermissions = [
'Owner', 'Owner',
@ -71,6 +73,10 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni
'Editor' 'Editor'
]; ];
public get canAddContributor() {
return this.addContributorForm.valid && (this.maxContributors < -1 || this.appContributors.length < this.maxContributors);
}
public addContributorForm: FormGroup = public addContributorForm: FormGroup =
this.formBuilder.group({ this.formBuilder.group({
user: [null, user: [null,
@ -99,8 +105,10 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni
public load() { public load() {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.appContributorsService.getContributors(app, this.version).retry(2)) .switchMap(app => this.appContributorsService.getContributors(app, this.version).retry(2))
.subscribe(dtos => { .subscribe(dto => {
this.updateContributors(ImmutableArray.of(dtos)); this.updateContributors(ImmutableArray.of(dto.contributors));
this.maxContributors = dto.maxContributors;
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });

34
src/Squidex/app/shared/services/app-contributors.service.spec.ts

@ -12,6 +12,7 @@ import { IMock, Mock, Times } from 'typemoq';
import { import {
ApiUrlConfig, ApiUrlConfig,
AppContributorDto, AppContributorDto,
AppContributorsDto,
AppContributorsService, AppContributorsService,
AuthService, AuthService,
Version Version
@ -32,32 +33,35 @@ describe('AppContributorsService', () => {
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions({ new ResponseOptions({
body: [ body: {
{ contributors: [
contributorId: '123', {
permission: 'Owner' contributorId: '123',
}, permission: 'Owner'
{ },
contributorId: '456', {
permission: 'Owner' contributorId: '456',
} permission: 'Owner'
] }
],
maxContributors: 100
}
}) })
) )
)) ))
.verifiable(Times.once()); .verifiable(Times.once());
let contributors: AppContributorDto[] | null = null; let contributors: AppContributorsDto | null = null;
appContributorsService.getContributors('my-app', version).subscribe(result => { appContributorsService.getContributors('my-app', version).subscribe(result => {
contributors = result; contributors = result;
}).unsubscribe(); }).unsubscribe();
expect(contributors).toEqual( expect(contributors).toEqual(
[ new AppContributorsDto([
new AppContributorDto('123', 'Owner'), new AppContributorDto('123', 'Owner'),
new AppContributorDto('456', 'Owner') new AppContributorDto('456', 'Owner')
]); ], 100));
authService.verifyAll(); authService.verifyAll();
}); });

24
src/Squidex/app/shared/services/app-contributors.service.ts

@ -21,6 +21,14 @@ export class AppContributorDto {
} }
} }
export class AppContributorsDto {
constructor(
public readonly contributors: AppContributorDto[],
public readonly maxContributors: number
) {
}
}
@Injectable() @Injectable()
export class AppContributorsService { export class AppContributorsService {
constructor( constructor(
@ -29,19 +37,21 @@ export class AppContributorsService {
) { ) {
} }
public getContributors(appName: string, version?: Version): Observable<AppContributorDto[]> { public getContributors(appName: string, version?: Version): Observable<AppContributorsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`);
return this.authService.authGet(url, version) return this.authService.authGet(url, version)
.map(response => response.json()) .map(response => response.json())
.map(response => { .map(response => {
const items: any[] = response; const items: any[] = response.contributors;
return items.map(item => { return new AppContributorsDto(
return new AppContributorDto( items.map(item => {
item.contributorId, return new AppContributorDto(
item.permission); item.contributorId,
}); item.permission);
}),
response.maxContributors);
}) })
.catchError('Failed to load contributors. Please reload.'); .catchError('Failed to load contributors. Please reload.');
} }

8
src/Squidex/app/shared/services/usages.service.spec.ts

@ -71,7 +71,7 @@ describe('UsagesService', () => {
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions({ new ResponseOptions({
body: { count: 130 } body: { count: 130, maxAllowed: 150 }
}) })
) )
)) ))
@ -83,7 +83,7 @@ describe('UsagesService', () => {
usages = result; usages = result;
}).unsubscribe(); }).unsubscribe();
expect(usages).toEqual(new CurrentCallsDto(130)); expect(usages).toEqual(new CurrentCallsDto(130, 150));
authService.verifyAll(); authService.verifyAll();
}); });
@ -130,7 +130,7 @@ describe('UsagesService', () => {
.returns(() => Observable.of( .returns(() => Observable.of(
new Response( new Response(
new ResponseOptions({ new ResponseOptions({
body: { size: 130 } body: { size: 130, maxAllowed: 150 }
}) })
) )
)) ))
@ -142,7 +142,7 @@ describe('UsagesService', () => {
usages = result; usages = result;
}).unsubscribe(); }).unsubscribe();
expect(usages).toEqual(new CurrentStorageDto(130)); expect(usages).toEqual(new CurrentStorageDto(130, 150));
authService.verifyAll(); authService.verifyAll();
}); });

26
src/Squidex/app/shared/services/usages.service.ts

@ -33,14 +33,16 @@ export class StorageUsageDto {
export class CurrentStorageDto { export class CurrentStorageDto {
constructor( constructor(
public readonly size: number public readonly size: number,
public readonly maxAllowed: number
) { ) {
} }
} }
export class CurrentCallsDto { export class CurrentCallsDto {
constructor( constructor(
public readonly count: number public readonly count: number,
public readonly maxAllowed: number
) { ) {
} }
} }
@ -58,10 +60,19 @@ export class UsagesService {
return this.authService.authGet(url) return this.authService.authGet(url)
.map(response => response.json()) .map(response => response.json())
.map(response => new CurrentCallsDto(response.count)) .map(response => new CurrentCallsDto(response.count, response.maxAllowed))
.catchError('Failed to load monthly api calls. Please reload.'); .catchError('Failed to load monthly api calls. Please reload.');
} }
public getTodayStorage(app: string): Observable<CurrentStorageDto> {
const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/storage/today`);
return this.authService.authGet(url)
.map(response => response.json())
.map(response => new CurrentStorageDto(response.size, response.maxAllowed))
.catchError('Failed to load todays storage size. Please reload.');
}
public getCallsUsages(app: string, fromDate: DateTime, toDate: DateTime): Observable<CallsUsageDto[]> { public getCallsUsages(app: string, fromDate: DateTime, toDate: DateTime): Observable<CallsUsageDto[]> {
const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/calls/${fromDate.toStringFormat('YYYY-MM-DD')}/${toDate.toStringFormat('YYYY-MM-DD')}`); const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/calls/${fromDate.toStringFormat('YYYY-MM-DD')}/${toDate.toStringFormat('YYYY-MM-DD')}`);
@ -80,15 +91,6 @@ export class UsagesService {
.catchError('Failed to load calls usage. Please reload.'); .catchError('Failed to load calls usage. Please reload.');
} }
public getTodayStorage(app: string): Observable<CurrentStorageDto> {
const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/storage/today`);
return this.authService.authGet(url)
.map(response => response.json())
.map(response => new CurrentStorageDto(response.size))
.catchError('Failed to load todays storage size. Please reload.');
}
public getStorageUsages(app: string, fromDate: DateTime, toDate: DateTime): Observable<StorageUsageDto[]> { public getStorageUsages(app: string, fromDate: DateTime, toDate: DateTime): Observable<StorageUsageDto[]> {
const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/storage/${fromDate.toStringFormat('YYYY-MM-DD')}/${toDate.toStringFormat('YYYY-MM-DD')}`); const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/storage/${fromDate.toStringFormat('YYYY-MM-DD')}/${toDate.toStringFormat('YYYY-MM-DD')}`);

18
tests/Squidex.Infrastructure.Tests/CQRS/Events/CompoundEventConsumerTests.cs

@ -57,8 +57,13 @@ namespace Squidex.Infrastructure.CQRS.Events
[Fact] [Fact]
public async Task Should_clear_all_consumers() public async Task Should_clear_all_consumers()
{ {
consumer1.Setup(x => x.ClearAsync()).Returns(TaskHelper.Done).Verifiable(); consumer1.Setup(x => x.ClearAsync()).
consumer2.Setup(x => x.ClearAsync()).Returns(TaskHelper.Done).Verifiable(); Returns(TaskHelper.Done)
.Verifiable();
consumer2.Setup(x => x.ClearAsync())
.Returns(TaskHelper.Done)
.Verifiable();
var sut = new CompoundEventConsumer("consumer-name", consumer1.Object, consumer2.Object); var sut = new CompoundEventConsumer("consumer-name", consumer1.Object, consumer2.Object);
@ -73,8 +78,13 @@ namespace Squidex.Infrastructure.CQRS.Events
{ {
var @event = Envelope.Create(new MyEvent()); var @event = Envelope.Create(new MyEvent());
consumer1.Setup(x => x.On(@event)).Returns(TaskHelper.Done).Verifiable(); consumer1.Setup(x => x.On(@event))
consumer2.Setup(x => x.On(@event)).Returns(TaskHelper.Done).Verifiable(); .Returns(TaskHelper.Done)
.Verifiable();
consumer2.Setup(x => x.On(@event))
.Returns(TaskHelper.Done)
.Verifiable();
var sut = new CompoundEventConsumer("consumer-name", consumer1.Object, consumer2.Object); var sut = new CompoundEventConsumer("consumer-name", consumer1.Object, consumer2.Object);

14
tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs

@ -121,9 +121,17 @@ namespace Squidex.Infrastructure.UsageTracking
{ {
var today = DateTime.Today; var today = DateTime.Today;
usageStore.Setup(x => x.TrackUsagesAsync(today, "key1", 1.0, 1000)).Returns(TaskHelper.Done).Verifiable(); usageStore.Setup(x => x.TrackUsagesAsync(today, "key1", 1.0, 1000))
usageStore.Setup(x => x.TrackUsagesAsync(today, "key2", 1.5, 5000)).Returns(TaskHelper.Done).Verifiable(); .Returns(TaskHelper.Done)
usageStore.Setup(x => x.TrackUsagesAsync(today, "key3", 0.9, 15000)).Returns(TaskHelper.Done).Verifiable(); .Verifiable();
usageStore.Setup(x => x.TrackUsagesAsync(today, "key2", 1.5, 5000))
.Returns(TaskHelper.Done)
.Verifiable();
usageStore.Setup(x => x.TrackUsagesAsync(today, "key3", 0.9, 15000))
.Returns(TaskHelper.Done)
.Verifiable();
await sut.TrackAsync("key1", 1, 1000); await sut.TrackAsync("key1", 1, 1000);

102
tests/Squidex.Read.Tests/Apps/ConfigAppLimitsProviderTests.cs

@ -0,0 +1,102 @@
// ==========================================================================
// ConfigAppLimitsProviderTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Linq;
using FluentAssertions;
using Moq;
using Squidex.Read.Apps.Services.Implementations;
using Xunit;
namespace Squidex.Read.Apps
{
public class ConfigAppLimitsProviderTests
{
private static readonly ConfigAppLimitsPlan[] Plans =
{
new ConfigAppLimitsPlan
{
Name = "Basic",
MaxApiCalls = 150000,
MaxAssetSize = 1024 * 1024 * 2,
MaxContributors = 5
},
new ConfigAppLimitsPlan
{
Name = "Free",
MaxApiCalls = 50000,
MaxAssetSize = 1024 * 1024 * 10,
MaxContributors = 2
}
};
[Fact]
public void Should_return_plans()
{
var sut = new ConfigAppLimitsProvider(Plans);
Plans.OrderBy(x => x.MaxApiCalls).ShouldBeEquivalentTo(sut.GetAvailablePlans());
}
[Fact]
public void Should_return_infinite_if_nothing_configured()
{
var sut = new ConfigAppLimitsProvider(Enumerable.Empty<ConfigAppLimitsPlan>());
var plan = sut.GetPlanForApp(CreateApp(0));
plan.ShouldBeEquivalentTo(new ConfigAppLimitsPlan
{
Name = "Infinite",
MaxApiCalls = -1,
MaxAssetSize = -1,
MaxContributors = -1
});
}
[Fact]
public void Should_return_fitting_app_plan()
{
var sut = new ConfigAppLimitsProvider(Plans);
var plan = sut.GetPlanForApp(CreateApp(1));
plan.ShouldBeEquivalentTo(new ConfigAppLimitsPlan
{
Name = "Basic",
MaxApiCalls = 150000,
MaxAssetSize = 1024 * 1024 * 2,
MaxContributors = 5
});
}
[Fact]
public void Should_smallest_plan_if_none_fits()
{
var sut = new ConfigAppLimitsProvider(Plans);
var plan = sut.GetPlanForApp(CreateApp(4));
plan.ShouldBeEquivalentTo(new ConfigAppLimitsPlan
{
Name = "Free",
MaxApiCalls = 50000,
MaxAssetSize = 1024 * 1024 * 10,
MaxContributors = 2
});
}
private static IAppEntity CreateApp(int plan)
{
var app = new Mock<IAppEntity>();
app.Setup(x => x.PlanId).Returns(plan);
return app.Object;
}
}
}

26
tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs

@ -14,6 +14,8 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Read.Apps; using Squidex.Read.Apps;
using Squidex.Read.Apps.Repositories; using Squidex.Read.Apps.Repositories;
using Squidex.Read.Apps.Services;
using Squidex.Read.Apps.Services.Implementations;
using Squidex.Read.Users; using Squidex.Read.Users;
using Squidex.Read.Users.Repositories; using Squidex.Read.Users.Repositories;
using Squidex.Write.Apps.Commands; using Squidex.Write.Apps.Commands;
@ -29,6 +31,7 @@ namespace Squidex.Write.Apps
{ {
private readonly Mock<ClientKeyGenerator> keyGenerator = new Mock<ClientKeyGenerator>(); private readonly Mock<ClientKeyGenerator> keyGenerator = new Mock<ClientKeyGenerator>();
private readonly Mock<IAppRepository> appRepository = new Mock<IAppRepository>(); private readonly Mock<IAppRepository> appRepository = new Mock<IAppRepository>();
private readonly Mock<IAppLimitsProvider> appLimitsProvider = new Mock<IAppLimitsProvider>();
private readonly Mock<IUserRepository> userRepository = new Mock<IUserRepository>(); private readonly Mock<IUserRepository> userRepository = new Mock<IUserRepository>();
private readonly AppCommandHandler sut; private readonly AppCommandHandler sut;
private readonly AppDomainObject app; private readonly AppDomainObject app;
@ -39,9 +42,11 @@ namespace Squidex.Write.Apps
public AppCommandHandlerTests() public AppCommandHandlerTests()
{ {
appLimitsProvider.Setup(x => x.GetPlan(0)).Returns(new ConfigAppLimitsPlan { MaxContributors = 2 });
app = new AppDomainObject(AppId, -1); app = new AppDomainObject(AppId, -1);
sut = new AppCommandHandler(Handler, appRepository.Object, userRepository.Object, keyGenerator.Object); sut = new AppCommandHandler(Handler, appRepository.Object, appLimitsProvider.Object, userRepository.Object, keyGenerator.Object);
} }
[Fact] [Fact]
@ -93,12 +98,29 @@ namespace Squidex.Write.Apps
}, false); }, false);
} }
[Fact]
public async Task AssignContributor_throw_if_reached_max_contributor_size()
{
CreateApp()
.AssignContributor(CreateCommand(new AssignContributor { ContributorId = "1" }))
.AssignContributor(CreateCommand(new AssignContributor { ContributorId = "2" }));
var context = CreateContextForCommand(new AssignContributor { ContributorId = contributorId });
userRepository.Setup(x => x.FindUserByIdAsync(It.IsAny<string>())).Returns(Task.FromResult(new Mock<IUserEntity>().Object));
await TestUpdate(app, async _ =>
{
await Assert.ThrowsAsync<ValidationException>(() => sut.HandleAsync(context));
}, false);
}
[Fact] [Fact]
public async Task AssignContributor_should_throw_if_null_user_not_found() public async Task AssignContributor_should_throw_if_null_user_not_found()
{ {
CreateApp(); CreateApp();
var context = CreateContextForCommand(new AssignContributor { ContributorId = null }); var context = CreateContextForCommand(new AssignContributor { ContributorId = contributorId });
userRepository.Setup(x => x.FindUserByIdAsync(contributorId)).Returns(Task.FromResult<IUserEntity>(null)); userRepository.Setup(x => x.FindUserByIdAsync(contributorId)).Returns(Task.FromResult<IUserEntity>(null));

8
tests/Squidex.Write.Tests/Assets/AssetCommandHandlerTests.cs

@ -112,12 +112,16 @@ namespace Squidex.Write.Assets
private void SetupImageInfo() private void SetupImageInfo()
{ {
assetThumbnailGenerator.Setup(x => x.GetImageInfoAsync(stream)).Returns(Task.FromResult(image)).Verifiable(); assetThumbnailGenerator
.Setup(x => x.GetImageInfoAsync(stream)).Returns(Task.FromResult(image))
.Verifiable();
} }
private void SetupStore(long version) private void SetupStore(long version)
{ {
assetStore.Setup(x => x.UploadAsync(assetId, version, null, stream)).Returns(TaskHelper.Done).Verifiable(); assetStore
.Setup(x => x.UploadAsync(assetId, version, null, stream)).Returns(TaskHelper.Done)
.Verifiable();
} }
} }
} }

Loading…
Cancel
Save