diff --git a/zero.Api/ApiApplicationResolverHandler.cs b/zero.Api/ApiApplicationResolverHandler.cs new file mode 100644 index 00000000..17916930 --- /dev/null +++ b/zero.Api/ApiApplicationResolverHandler.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; + +namespace zero.Api; + +public class ApiApplicationResolverHandler : IBackofficeApplicationResolverHandler +{ + public bool TryResolve(HttpContext context, IEnumerable applications, ZeroUser user, out Application resolved) + { + var routeValues = context.Features.Get().RouteValues; + + if (routeValues.ContainsKey("zero_app_key")) + { + string appKey = routeValues["zero_app_key"] as string; + resolved = applications.FirstOrDefault(x => x.Alias == appKey); + return resolved != null; + } + + resolved = null; + return false; + } +} \ No newline at end of file diff --git a/zero.Api/Endpoints/NotFound/NotFoundController.cs b/zero.Api/Endpoints/NotFound/NotFoundController.cs new file mode 100644 index 00000000..1abeae49 --- /dev/null +++ b/zero.Api/Endpoints/NotFound/NotFoundController.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc; + +namespace zero.Api.Endpoints.NotFound; + +public class NotFoundZeroApiController : Controller +{ + public virtual ActionResult Index() + { + return NotFound(); + } +} \ No newline at end of file diff --git a/zero.Api/Plugin.cs b/zero.Api/Plugin.cs index bf24db0e..3c0e0d54 100644 --- a/zero.Api/Plugin.cs +++ b/zero.Api/Plugin.cs @@ -23,6 +23,7 @@ public class ZeroApiPlugin : ZeroPlugin { services.AddOptions().Bind(configuration.GetSection("Zero:Api")).Configure(ConfigureOptions); services.TryAddEnumerable(ServiceDescriptor.Transient, ZeroApiMvcOptions>()); + services.AddTransient(); ZeroModuleCollection modules = new(); diff --git a/zero.Api/ZeroApiControllerModelConvention.cs b/zero.Api/ZeroApiControllerModelConvention.cs index abcd3b3d..0cbb7075 100644 --- a/zero.Api/ZeroApiControllerModelConvention.cs +++ b/zero.Api/ZeroApiControllerModelConvention.cs @@ -57,7 +57,9 @@ public class ZeroApiControllerModelConvention : IControllerModelConvention if (appAware) { - path.Append("{zero_app_slug}/"); + path.Append("{zero_app_key}/"); + // TODO add route constraint which only allows registered app-ids + // see https://nemi-chand.github.io/creating-custom-routing-constraint-in-aspnet-core-mvc/ } path.Append("[controller]"); diff --git a/zero.Api/ZeroEnpointRouteBuilderExtensions.cs b/zero.Api/ZeroEnpointRouteBuilderExtensions.cs new file mode 100644 index 00000000..cd2942bc --- /dev/null +++ b/zero.Api/ZeroEnpointRouteBuilderExtensions.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Builder; + +namespace zero.Backoffice; + +public static class ZeroEndpointRouteBuilderExtensions +{ + public static void MapZeroApi(this IZeroEndpointRouteBuilder endpoints, string path = "/zero/api") + { + endpoints.MapFallbackToController(path.EnsureEndsWith('/') + "{**path}", "Index", "NotFoundZeroApi"); + //app.UseWhen(ctx => ctx.Request.Path.StartsWithSegments(path + "/api"), app1 => + //{ + // app1.UseEndpoints(endpoints => + // { + // //IZeroOptions options = app.ApplicationServices.GetService(); // TODO oO + // // see https://our.umbraco.com/documentation/reference/routing/custom-routes#where-to-put-your-routing-logic + // //string path = options.BackofficePath.EnsureStartsWith('/').TrimEnd('/'); + // //endpoints.MapFallbackToController(path + "/{**path}", "Index", "ZeroIndex"); + + // //endpoints.MapControllers(); + // }); + //}); + } +} diff --git a/zero.Backoffice/ZeroEnpointRouteBuilderExtensions.cs b/zero.Backoffice/ZeroEnpointRouteBuilderExtensions.cs new file mode 100644 index 00000000..71f55ec8 --- /dev/null +++ b/zero.Backoffice/ZeroEnpointRouteBuilderExtensions.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Builder; + +namespace zero.Backoffice; + +public static class ZeroEndpointRouteBuilderExtensions +{ + public static void MapZeroBackoffice(this IZeroEndpointRouteBuilder endpoints, string path = "/zero") + { + endpoints.MapFallbackToController(path.EnsureEndsWith('/') + "{**path}", "Index", "ZeroIndex"); + endpoints.MapFallbackToController(path.EnsureEndsWith('/') + "backoffice/{**path}", "Index", "NotFoundZeroApi"); + //app.UseWhen(ctx => ctx.Request.Path.StartsWithSegments(path + "/api"), app1 => + //{ + // app1.UseEndpoints(endpoints => + // { + // //IZeroOptions options = app.ApplicationServices.GetService(); // TODO oO + // // see https://our.umbraco.com/documentation/reference/routing/custom-routes#where-to-put-your-routing-logic + // //string path = options.BackofficePath.EnsureStartsWith('/').TrimEnd('/'); + // //endpoints.MapFallbackToController(path + "/{**path}", "Index", "ZeroIndex"); + + // //endpoints.MapControllers(); + // }); + //}); + } +} diff --git a/zero.Core/Applications/ApplicationResolver.cs b/zero.Core/Applications/ApplicationResolver.cs index e920fc08..1e3be088 100644 --- a/zero.Core/Applications/ApplicationResolver.cs +++ b/zero.Core/Applications/ApplicationResolver.cs @@ -39,29 +39,65 @@ public class ApplicationResolver : IApplicationResolver return null; } - Application app; + Application app = null; - //if (context.IsBackofficeRequest(Options.ZeroPath)) - //{ - // app = await ResolveFromUser(user); - //} - //else - //{ - // app = await ResolveFromRequest(context); - //} - app = await ResolveFromRequest(context); + if (context.IsBackofficeRequest(Options.ZeroPath)) + { + ZeroUser userEntity = await GetBackofficeUser(user); + app = await ResolveFromBackofficeHandlers(context, userEntity) ?? await ResolveFromUser(userEntity); + } if (app == null) { - //Logger.LogWarning("Could not resolve application for host {host}", context.Request.Host); - IList apps = await GetApplications(); - app = apps.FirstOrDefault(); + app = await ResolveFromHandlers(context) ?? await ResolveFromRequest(context); + } + + if (app == null) + { + Logger.LogWarning("Could not resolve application for host {host}", context.Request.Host); + return null; } return app; } + /// + public async Task ResolveFromHandlers(HttpContext context) + { + IList apps = await GetApplications(); + IEnumerable handlers = Handler.GetAll(); + + foreach (IApplicationResolverHandler handler in handlers) + { + if (handler.TryResolve(context, apps, out Application resolved)) + { + return resolved; + } + } + + return null; + } + + + /// + public async Task ResolveFromBackofficeHandlers(HttpContext context, ZeroUser user) + { + IList apps = await GetApplications(); + IEnumerable handlers = Handler.GetAll(); + + foreach (IBackofficeApplicationResolverHandler handler in handlers) + { + if (handler.TryResolve(context, apps, user, out Application resolved)) + { + return resolved; + } + } + + return null; + } + + /// public async Task ResolveFromUser(ClaimsPrincipal user) { @@ -108,37 +144,15 @@ public class ApplicationResolver : IApplicationResolver /// - public async Task ResolveFromRequest(HttpContext context) - { - //if (Options.Applications.Count < 2) - //{ - // return (await GetApplications()).FirstOrDefault(); - //} - - IApplicationResolverHandler handler = Handler.Get(); - Application app = handler?.Resolve(context.Request, await GetApplications()); - - if (app == null) - { - app = await ResolveFromUri(context.Request.GetEncodedUrl()); - } - - return app; - } + public Task ResolveFromRequest(HttpContext context) => ResolveFromUri(context.Request.GetEncodedUrl()); /// - public async Task ResolveFromUri(string uriString) - { - return ResolveFromUriInternal(new Uri(uriString, UriKind.Absolute), await GetApplications()); - } + public async Task ResolveFromUri(string uriString) => ResolveFromUriInternal(new Uri(uriString, UriKind.Absolute), await GetApplications()); /// - public async Task ResolveFromUri(Uri uri) - { - return ResolveFromUriInternal(uri, await GetApplications()); - } + public async Task ResolveFromUri(Uri uri) => ResolveFromUriInternal(uri, await GetApplications()); /// diff --git a/zero.Core/Applications/IApplicationResolverHandler.cs b/zero.Core/Applications/IApplicationResolverHandler.cs index 8c2a1d50..26800c07 100644 --- a/zero.Core/Applications/IApplicationResolverHandler.cs +++ b/zero.Core/Applications/IApplicationResolverHandler.cs @@ -4,5 +4,5 @@ namespace zero.Applications; public interface IApplicationResolverHandler : IHandler { - Application Resolve(HttpRequest request, IEnumerable applications); + bool TryResolve(HttpContext context, IEnumerable applications, out Application resolved); } \ No newline at end of file diff --git a/zero.Core/Applications/IBackofficeApplicationResolverHandler.cs b/zero.Core/Applications/IBackofficeApplicationResolverHandler.cs new file mode 100644 index 00000000..3946fc5c --- /dev/null +++ b/zero.Core/Applications/IBackofficeApplicationResolverHandler.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Http; + +namespace zero.Applications; + +public interface IBackofficeApplicationResolverHandler : IHandler +{ + bool TryResolve(HttpContext context, IEnumerable applications, ZeroUser user, out Application resolved); +} \ No newline at end of file diff --git a/zero.Core/Context/ZeroContext.cs b/zero.Core/Context/ZeroContext.cs index 10d1b846..ee187e33 100644 --- a/zero.Core/Context/ZeroContext.cs +++ b/zero.Core/Context/ZeroContext.cs @@ -110,7 +110,7 @@ public class ZeroContext : IZeroContext Application = await AppResolver.Resolve(context, BackofficeUser); AppId = Application.Id; - Logger.LogDebug("Resolved {appId} for request {uri}", AppId, context.Request.Host.ToString() + context.Request.Path.Value.EnsureStartsWith('/')); + Logger.LogInformation("Resolved {appId} ({uri})", AppId, context.Request.Host.ToString() + context.Request.Path.Value.EnsureStartsWith('/')); // set default database for document store Store.ResolvedDatabase = Application.Database; diff --git a/zero.Core/ZeroApplicationBuilder.cs b/zero.Core/ZeroApplicationBuilder.cs index 13cbc150..83a285ce 100644 --- a/zero.Core/ZeroApplicationBuilder.cs +++ b/zero.Core/ZeroApplicationBuilder.cs @@ -14,8 +14,8 @@ public class ZeroApplicationBuilder : IZeroApplicationBuilder App = app; App.UseStaticFiles(); //App.UseExceptionHandler("/zero/api/error"); - App.UseMiddleware(); App.UseRouting(); + App.UseMiddleware(); App.UseAuthentication(); App.UseAuthorization(); diff --git a/zero.Demo/DevApplicationResolverHandler.cs b/zero.Demo/DevApplicationResolverHandler.cs index f69ed5ea..35abadfb 100644 --- a/zero.Demo/DevApplicationResolverHandler.cs +++ b/zero.Demo/DevApplicationResolverHandler.cs @@ -20,22 +20,27 @@ public class DevApplicationResolverHandler : IApplicationResolverHandler } - public Application Resolve(HttpRequest request, IEnumerable applications) + public bool TryResolve(HttpContext context, IEnumerable applications, out Application resolved) { + resolved = null; + //if (!Env.IsDevelopment()) //{ - // return null; + // return false; //} - if (request.Host.Host.Contains("ngrok.io")) + + if (context.Request.Host.Host.Contains("ngrok.io")) { - return applications.First(x => x.Id == "app.hofbauer"); + resolved = applications.First(x => x.Id == "app.hofbauer"); + return true; } - if (request.Host.Port.HasValue && PortMap.TryGetValue(request.Host.Port.Value, out string appId)) + if (context.Request.Host.Port.HasValue && PortMap.TryGetValue(context.Request.Host.Port.Value, out string appId)) { - return applications.FirstOrDefault(x => x.Id == appId); + resolved = applications.FirstOrDefault(x => x.Id == appId); + return true; } - return null; + return false; } } \ No newline at end of file diff --git a/zero.Demo/Pages/Index.cshtml b/zero.Demo/Pages/Index.cshtml index b5f0c15f..7894f49d 100644 --- a/zero.Demo/Pages/Index.cshtml +++ b/zero.Demo/Pages/Index.cshtml @@ -1,10 +1,11 @@ @page @model IndexModel +@inject zero.Context.IZeroContext context @{ ViewData["Title"] = "Home page"; }
-

Welcome

+

Welcome to @context.AppId

Learn about building Web apps with ASP.NET Core.

diff --git a/zero.Demo/Program.cs b/zero.Demo/Program.cs index cadd1cd6..6933d182 100644 --- a/zero.Demo/Program.cs +++ b/zero.Demo/Program.cs @@ -41,6 +41,8 @@ app.UseZero().WithEndpoints(x => //x.MapControllerRoute("default", "api/{controller}/{action=Index}/{id?}"); x.MapControllers(); x.MapRazorPages(); + x.MapZeroApi(); + x.MapZeroBackoffice(); x.MapZeroRoutes(); });