From a394091023af70dada171fc9959ddd679b43d71e Mon Sep 17 00:00:00 2001 From: Tobias Klika Date: Mon, 7 Mar 2022 17:13:10 +0100 Subject: [PATCH] preview service --- zero.Backoffice.UI/app/modules/preview/api.ts | 7 ++ .../app/modules/preview/preview.vue | 7 +- .../Endpoints/UI/PreviewRequestTokenModel.cs | 13 +++ zero.Backoffice/Endpoints/UI/UIController.cs | 20 ++++- zero.Core/Context/ZeroContext.cs | 11 ++- .../Persistence/Tokens/ZeroTokenProvider.cs | 21 +++++ zero.Core/Preview/PreviewOptions.cs | 10 ++- zero.Core/Preview/PreviewService.cs | 87 +++++++++++++++++++ .../Preview/Routing/PreviewRouteProvider.cs | 2 +- zero.Core/Preview/ZeroPreviewModule.cs | 6 +- zero.Core/Usings.cs | 1 + zero.Core/ZeroBuilder.cs | 1 + 12 files changed, 173 insertions(+), 13 deletions(-) create mode 100644 zero.Backoffice.UI/app/modules/preview/api.ts create mode 100644 zero.Backoffice/Endpoints/UI/PreviewRequestTokenModel.cs create mode 100644 zero.Core/Preview/PreviewService.cs diff --git a/zero.Backoffice.UI/app/modules/preview/api.ts b/zero.Backoffice.UI/app/modules/preview/api.ts new file mode 100644 index 00000000..45f2245e --- /dev/null +++ b/zero.Backoffice.UI/app/modules/preview/api.ts @@ -0,0 +1,7 @@ +import { post, ApiResponse, ApiRequestConfig } from '../../services/request'; + +export default { + + createPreviewToken: (key: string, config?: ApiRequestConfig): Promise> => post('backoffice/ui/previews/token', { key }, { ...config }), + +}; \ No newline at end of file diff --git a/zero.Backoffice.UI/app/modules/preview/preview.vue b/zero.Backoffice.UI/app/modules/preview/preview.vue index a984a1e2..2d5a2356 100644 --- a/zero.Backoffice.UI/app/modules/preview/preview.vue +++ b/zero.Backoffice.UI/app/modules/preview/preview.vue @@ -10,6 +10,7 @@ diff --git a/zero.Backoffice/Endpoints/UI/PreviewRequestTokenModel.cs b/zero.Backoffice/Endpoints/UI/PreviewRequestTokenModel.cs new file mode 100644 index 00000000..9c585507 --- /dev/null +++ b/zero.Backoffice/Endpoints/UI/PreviewRequestTokenModel.cs @@ -0,0 +1,13 @@ +namespace zero.Backoffice.Endpoints.UI; + +public class PreviewRequestTokenModel +{ + public string Key { get; set; } + + public class Response + { + public string Token { get; set; } + + public string QueryParameter { get; set; } + } +} \ No newline at end of file diff --git a/zero.Backoffice/Endpoints/UI/UIController.cs b/zero.Backoffice/Endpoints/UI/UIController.cs index f715d20f..da4e4231 100644 --- a/zero.Backoffice/Endpoints/UI/UIController.cs +++ b/zero.Backoffice/Endpoints/UI/UIController.cs @@ -6,6 +6,7 @@ using System.IO; using System.Net.Http; using System.Reflection; using zero.Backoffice.Services; +using zero.Preview; namespace zero.Backoffice.Endpoints.UI; @@ -19,9 +20,12 @@ public class UIController : ZeroBackofficeController readonly ICultureService CultureService; readonly IUserService UserService; readonly IRequestUrlResolver RequestUrlResolver; + readonly IPreviewService PreviewService; + readonly IZeroContext ZeroContext; readonly HttpClient HttpClient; - public UIController(IUserService userService, IIconService iconService, IResourceService resourceService, ISectionService sectionService, IZeroOptions options, IMediaManagement media, ICultureService cultureService, IRequestUrlResolver requestUrlResolver) + public UIController(IUserService userService, IIconService iconService, IResourceService resourceService, ISectionService sectionService, IZeroOptions options, IMediaManagement media, + ICultureService cultureService, IRequestUrlResolver requestUrlResolver, IPreviewService previewService, IZeroContext zeroContext) { UserService = userService; IconService = iconService; @@ -31,6 +35,8 @@ public class UIController : ZeroBackofficeController Options = options; Media = media; RequestUrlResolver = requestUrlResolver; + PreviewService = previewService; + ZeroContext = zeroContext; HttpClient = new HttpClient(); } @@ -97,6 +103,18 @@ public class UIController : ZeroBackofficeController } + [HttpPost("previews/token")] + public async Task> GetPreviewToken(PreviewRequestTokenModel model) + { + string token = await PreviewService.CreateAccessToken(model.Key, ZeroContext.BackofficeUser); + return new PreviewRequestTokenModel.Response() + { + Token = token, + QueryParameter = Options.For().QueryParameter + }; + } + + [HttpGet("thumbnail/{id}-{size}.tmp")] public async Task GetThumbnail(string id, string size) { diff --git a/zero.Core/Context/ZeroContext.cs b/zero.Core/Context/ZeroContext.cs index b5c6b173..11fafa3c 100644 --- a/zero.Core/Context/ZeroContext.cs +++ b/zero.Core/Context/ZeroContext.cs @@ -56,12 +56,14 @@ public class ZeroContext : IZeroContext protected IPrimitiveTypeCollection ValueCollection { get; private set; } + protected IPreviewService PreviewService { get; private set; } + bool _resolved = false; public ZeroContext(IZeroOptions options, IHttpContextAccessor httpContextAccessor, IApplicationResolver appResolver, ICultureResolver cultureResolver, - ILogger logger, IZeroStore store, IHandlerHolder handler, IServiceProvider services) + ILogger logger, IZeroStore store, IHandlerHolder handler, IServiceProvider services, IPreviewService previewService) { Options = options; AppResolver = appResolver; @@ -72,6 +74,7 @@ public class ZeroContext : IZeroContext ValueCollection = new PrimitiveTypeCollection(); HttpContextAccessor = httpContextAccessor; Services = services; + PreviewService = previewService; } @@ -96,9 +99,6 @@ public class ZeroContext : IZeroContext _resolved = true; - // check whether the current request is a preview request - IsPreview = context.Request.Query.ContainsKey("zero_preview"); - // check if the current request is a backoffice request IsBackofficeRequest = context.IsBackofficeRequest(Options.ZeroPath); @@ -112,6 +112,9 @@ public class ZeroContext : IZeroContext IsLoggedIntoBackoffice = true; } + // check whether the current request is a preview request + IsPreview = await PreviewService.IsAccessGranted(context.Request, BackofficeUser); + // resolve current application Application = await AppResolver.Resolve(context, BackofficeUser); AppId = Application.Id; diff --git a/zero.Core/Persistence/Tokens/ZeroTokenProvider.cs b/zero.Core/Persistence/Tokens/ZeroTokenProvider.cs index 221e2ca9..ff1e37a0 100644 --- a/zero.Core/Persistence/Tokens/ZeroTokenProvider.cs +++ b/zero.Core/Persistence/Tokens/ZeroTokenProvider.cs @@ -127,6 +127,22 @@ public class ZeroTokenProvider : IZeroTokenProvider } + /// + public virtual async Task Exists(string token) + { + if (token.IsNullOrWhiteSpace()) + { + return false; + } + + IZeroDocumentSession session = Store.Session(); + + // try to find a valid token + SecurityToken securityToken = await session.LoadAsync(TokenToId(token)); + return securityToken != null; + } + + /// /// Converts the token to a database id /// @@ -271,4 +287,9 @@ public interface IZeroTokenProvider /// This method won't store the token in the database and can't be used for verification. Use instead. /// string Random(int length); + + /// + /// Determines whether a token exists in the database. + /// + Task Exists(string token); } diff --git a/zero.Core/Preview/PreviewOptions.cs b/zero.Core/Preview/PreviewOptions.cs index e5b9c6b1..6c3d4a3a 100644 --- a/zero.Core/Preview/PreviewOptions.cs +++ b/zero.Core/Preview/PreviewOptions.cs @@ -4,8 +4,14 @@ public class PreviewOptions { public PreviewOptions() { - PreviewPath = "/preview"; + Path = "/preview"; + QueryParameter = "zero_preview"; + TokenExpirationInMinutes = 10; } - public string PreviewPath { get; set; } + public string Path { get; set; } + + public string QueryParameter { get; set; } + + public double TokenExpirationInMinutes { get; set; } } \ No newline at end of file diff --git a/zero.Core/Preview/PreviewService.cs b/zero.Core/Preview/PreviewService.cs new file mode 100644 index 00000000..d2ed3c90 --- /dev/null +++ b/zero.Core/Preview/PreviewService.cs @@ -0,0 +1,87 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using System.Security.Claims; + +namespace zero.Preview; + +public class PreviewService : IPreviewService +{ + protected IZeroTokenProvider TokenProvider { get; private set; } + + protected IZeroOptions Options { get; private set; } + + + public PreviewService(IZeroTokenProvider tokenProvider, IZeroOptions options) + { + TokenProvider = tokenProvider; + Options = options; + } + + + /// + public async Task CreateAccessToken(string key, ClaimsPrincipal backofficeUser) + { + string fullKey = GetKey(key, backofficeUser); + return await TokenProvider.Create(fullKey, TimeSpan.FromMinutes(Options.For().TokenExpirationInMinutes), 32); + } + + + /// + public async Task IsAccessGranted(HttpRequest request, ClaimsPrincipal backofficeUser) + { + string token = request.Query[Options.For().QueryParameter]; + + if (!token.HasValue()) + { + return false; + } + + return await IsAccessGranted(token, backofficeUser); + } + + + /// + public async Task IsAccessGranted(string token, ClaimsPrincipal backofficeUser) + { + return await TokenProvider.Exists(token); + } + + + /// + /// Generate the key for a new preview access + /// + protected string GetKey(string keyPrefix, ClaimsPrincipal backofficeUser) + { + //string userId = UserManager.GetUserId(backofficeUser); + + //if (userId.IsNullOrEmpty()) + //{ + // throw new InvalidOperationException("Preview access is only granted for authenticated users"); + //} + + if (keyPrefix.IsNullOrEmpty()) + { + throw new ArgumentNullException(nameof(keyPrefix), "Please define a key for generating preview access"); + } + + return "zero:preview:" + /*userId + ":" + */ keyPrefix; + } +} + +public interface IPreviewService +{ + /// + /// Create a token for a new preview access. + /// + Task CreateAccessToken(string key, ClaimsPrincipal backofficeUser); + + // + /// Determine whether the access to the preview is granted based on an HTTP request + /// + Task IsAccessGranted(HttpRequest request, ClaimsPrincipal backofficeUser); + + /// + /// Determine whether the access to the preview is granted based on the passed token + /// + Task IsAccessGranted(string token, ClaimsPrincipal backofficeUser); +} \ No newline at end of file diff --git a/zero.Core/Preview/Routing/PreviewRouteProvider.cs b/zero.Core/Preview/Routing/PreviewRouteProvider.cs index 928f3483..7bd6faac 100644 --- a/zero.Core/Preview/Routing/PreviewRouteProvider.cs +++ b/zero.Core/Preview/Routing/PreviewRouteProvider.cs @@ -12,7 +12,7 @@ public class PreviewRouteProvider : ZeroRouteProvider public override async Task Create(RoutingContext context, PreviewRouteModel model) { Route route = await base.Create(context, model); - route.Url = context.Context.Options.For().PreviewPath.EnsureStartsWith('/').TrimEnd('/'); + route.Url = context.Context.Options.For().Path.EnsureStartsWith('/').TrimEnd('/'); return route; } diff --git a/zero.Core/Preview/ZeroPreviewModule.cs b/zero.Core/Preview/ZeroPreviewModule.cs index 39f5d84e..c9166e24 100644 --- a/zero.Core/Preview/ZeroPreviewModule.cs +++ b/zero.Core/Preview/ZeroPreviewModule.cs @@ -1,7 +1,5 @@ -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; namespace zero.Preview; @@ -9,7 +7,7 @@ internal class ZeroPreviewModule : ZeroModule { public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) { - //services.AddScoped(); + services.AddScoped(); services.AddOptions().Bind(configuration.GetSection("Zero:Preview")); } } \ No newline at end of file diff --git a/zero.Core/Usings.cs b/zero.Core/Usings.cs index 34318fee..a1685b06 100644 --- a/zero.Core/Usings.cs +++ b/zero.Core/Usings.cs @@ -21,6 +21,7 @@ global using zero.Media; global using zero.Models; global using zero.Pages; global using zero.Persistence; +global using zero.Preview; global using zero.Rendering; global using zero.Routing; global using zero.Stores; diff --git a/zero.Core/ZeroBuilder.cs b/zero.Core/ZeroBuilder.cs index e4e4cc1c..620b28ca 100644 --- a/zero.Core/ZeroBuilder.cs +++ b/zero.Core/ZeroBuilder.cs @@ -52,6 +52,7 @@ public class ZeroBuilder Modules.Add(); Modules.Add(); Modules.Add(); + Modules.Add(); Modules.Add(); Modules.Add(); Modules.Add();