diff --git a/DysonNetwork.Zone/Pages/Error.cshtml b/DysonNetwork.Zone/Pages/Error.cshtml index b5105b6..0aa49e0 100644 --- a/DysonNetwork.Zone/Pages/Error.cshtml +++ b/DysonNetwork.Zone/Pages/Error.cshtml @@ -1,26 +1,23 @@ -@page +@page "/Error/{statusCode?}" @model ErrorModel @{ ViewData["Title"] = "Error"; + ViewData["SiteName"] = Model.SiteName ?? "main"; } -

Error.

-

An error occurred while processing your request.

- -@if (Model.ShowRequestId) -{ -

- Request ID: @Model.RequestId -

-} - -

Development Mode

-

- Swapping to the Development environment displays detailed information about the error that occurred. -

-

- The Development environment shouldn't be enabled for deployed applications. - It can result in displaying sensitive information from exceptions to end users. - For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development - and restarting the app. -

\ No newline at end of file +
+
+ Logo +

@(Model.StatusCode?.ToString() ?? "Error")

+

Something went wrong...

+

Powered by the Solar Network Pages

+
+

Path: @Model.CurrentPath

+

Site ID: @(Model.Site?.Id.ToString() ?? "none")

+ @if (Model.ShowRequestId) + { +

Request ID: @Model.RequestId

+ } +
+
+
\ No newline at end of file diff --git a/DysonNetwork.Zone/Pages/Error.cshtml.cs b/DysonNetwork.Zone/Pages/Error.cshtml.cs index b52a6d8..5bab05b 100644 --- a/DysonNetwork.Zone/Pages/Error.cshtml.cs +++ b/DysonNetwork.Zone/Pages/Error.cshtml.cs @@ -1,4 +1,6 @@ using System.Diagnostics; +using DysonNetwork.Shared.Models; +using DysonNetwork.Zone.Publication; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; @@ -12,8 +14,20 @@ public class ErrorModel : PageModel public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + [FromRoute] public new int? StatusCode { get; set; } + + public SnPublicationSite? Site { get; set; } + public string? SiteName { get; set; } + public string? CurrentPath { get; set; } + public void OnGet() { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; + + if (HttpContext.Items.TryGetValue(PublicationSiteMiddleware.SiteContextKey, out var site)) + Site = site as SnPublicationSite; + + SiteName = Request.Headers["X-SiteName"].ToString(); + CurrentPath = Request.Path; } } \ No newline at end of file diff --git a/DysonNetwork.Zone/Pages/Index.cshtml b/DysonNetwork.Zone/Pages/Index.cshtml index 9235908..8c2a406 100644 --- a/DysonNetwork.Zone/Pages/Index.cshtml +++ b/DysonNetwork.Zone/Pages/Index.cshtml @@ -2,22 +2,12 @@ @model IndexModel @{ ViewData["Title"] = "Solar Network Pages"; - ViewData["SiteName"] = Model.SiteName ?? "main"; }
Logo -

404 Not Found

+

Hello World 👋

Here are the Solar Network Pages

-

And you're accessing the site @(Model.SiteName ?? "main")

-

- The reason you're seeing this is the author of the site - haven't configure anything that match this route. -

-
-

Path: @Model.CurrentPath

-

Site ID: @(Model.Site?.Id.ToString() ?? "none")

-
diff --git a/DysonNetwork.Zone/Pages/Index.cshtml.cs b/DysonNetwork.Zone/Pages/Index.cshtml.cs index 4720b28..7e9b36c 100644 --- a/DysonNetwork.Zone/Pages/Index.cshtml.cs +++ b/DysonNetwork.Zone/Pages/Index.cshtml.cs @@ -1,70 +1,10 @@ -using DysonNetwork.Shared.Models; -using DysonNetwork.Zone.Publication; -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using Microsoft.AspNetCore.StaticFiles; -using Microsoft.EntityFrameworkCore; -using System.Text.Json; namespace DysonNetwork.Zone.Pages; -public class IndexModel(AppDatabase db, PublicationSiteManager psm) : PageModel +public class IndexModel : PageModel { - public SnPublicationSite? Site { get; set; } - public string? SiteName { get; set; } - public string? CurrentPath { get; set; } - - public async Task OnGet(string? path = null) + public void OnGet() { - var siteNameValue = Request.Headers["X-SiteName"].ToString(); - SiteName = siteNameValue; - CurrentPath = Path.Combine("/", path ?? ""); - if (string.IsNullOrEmpty(siteNameValue)) - { - SiteName = null; - return Page(); - } - - var capturedName = siteNameValue; - Site = await db.PublicationSites.FirstOrDefaultAsync(s => s.Slug == capturedName); - if (Site == null) return Page(); - var pagePath = CurrentPath; - - var page = await db.PublicationPages.FirstOrDefaultAsync(p => p.SiteId == Site.Id && p.Path == pagePath); - if (page != null) - { - switch (page.Type) - { - case PublicationPageType.HtmlPage - when page.Config.TryGetValue("html", out var html) && html is JsonElement content: - return Content(content.ToString(), "text/html"); - case PublicationPageType.Redirect - when page.Config.TryGetValue("url", out var url) && url is JsonElement redirectUrl: - return Redirect(redirectUrl.ToString()); - } - } - - // If the site is enabled the self-managed mode, try lookup the files then - if (Site.Mode == PublicationSiteMode.SelfManaged) - { - var provider = new FileExtensionContentTypeProvider(); - var hostedFilePath = psm.GetValidatedFullPath(Site.Id, CurrentPath); - if (System.IO.File.Exists(hostedFilePath)) - { - if (!provider.TryGetContentType(hostedFilePath, out var mimeType)) - mimeType = "text/html"; - var fileStream = new FileStream(hostedFilePath, FileMode.Open, FileAccess.Read); - return new FileStreamResult(fileStream, mimeType); - } - - var hostedNotFoundPath = psm.GetValidatedFullPath(Site.Id, "404.html"); - if (System.IO.File.Exists(hostedNotFoundPath)) - { - var fileStream = new FileStream(hostedNotFoundPath, FileMode.Open, FileAccess.Read); - return new FileStreamResult(fileStream, "text/html"); - } - } - - return Page(); } } \ No newline at end of file diff --git a/DysonNetwork.Zone/Program.cs b/DysonNetwork.Zone/Program.cs index b227cd1..8d987eb 100644 --- a/DysonNetwork.Zone/Program.cs +++ b/DysonNetwork.Zone/Program.cs @@ -3,6 +3,7 @@ using DysonNetwork.Zone.Startup; using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Http; using DysonNetwork.Shared.Registry; +using DysonNetwork.Zone.Publication; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); @@ -11,7 +12,7 @@ builder.AddServiceDefaults(); builder.ConfigureAppKestrel(builder.Configuration); -builder.Services.AddRazorPages(options => options.Conventions.AddPageRoute("/Index", "{**path}")); +builder.Services.AddRazorPages(); builder.Services.AddControllers(); builder.Services.AddAppServices(); @@ -46,8 +47,11 @@ using (var scope = app.Services.CreateScope()) if (!app.Environment.IsDevelopment()) app.UseExceptionHandler("/Error"); +app.UseMiddleware(); + app.UseStaticFiles(); app.UseRouting(); +app.UseStatusCodePagesWithReExecute("/Error/{0}"); app.ConfigureAppMiddleware(builder.Configuration); app.MapRazorPages(); diff --git a/DysonNetwork.Zone/Publication/PublicationSiteMiddleware.cs b/DysonNetwork.Zone/Publication/PublicationSiteMiddleware.cs new file mode 100644 index 0000000..3892e3b --- /dev/null +++ b/DysonNetwork.Zone/Publication/PublicationSiteMiddleware.cs @@ -0,0 +1,77 @@ +using System.Text.Json; +using DysonNetwork.Shared.Models; +using Microsoft.AspNetCore.StaticFiles; +using Microsoft.EntityFrameworkCore; + +namespace DysonNetwork.Zone.Publication; + +public class PublicationSiteMiddleware(RequestDelegate next) +{ + public const string SiteContextKey = "PubSite"; + + public async Task InvokeAsync(HttpContext context, AppDatabase db, PublicationSiteManager psm) + { + var siteNameValue = context.Request.Headers["X-SiteName"].ToString(); + var currentPath = context.Request.Path.Value ?? ""; + + if (string.IsNullOrEmpty(siteNameValue)) + { + await next(context); + return; + } + + var site = await db.PublicationSites + .FirstOrDefaultAsync(s => EF.Functions.ILike(s.Name, siteNameValue)); + if (site == null) + { + await next(context); + return; + } + + context.Items[SiteContextKey] = site; + + var page = await db.PublicationPages + .FirstOrDefaultAsync(p => p.SiteId == site.Id && p.Path == currentPath); + if (page != null) + { + switch (page.Type) + { + case PublicationPageType.HtmlPage + when page.Config.TryGetValue("html", out var html) && html is JsonElement content: + context.Response.ContentType = "text/html"; + await context.Response.WriteAsync(content.ToString()); + return; + case PublicationPageType.Redirect + when page.Config.TryGetValue("url", out var url) && url is JsonElement redirectUrl: + context.Response.Redirect(redirectUrl.ToString()); + return; + } + } + + // If the site is enabled the self-managed mode, try lookup the files then + if (site.Mode == PublicationSiteMode.SelfManaged) + { + var provider = new FileExtensionContentTypeProvider(); + var hostedFilePath = psm.GetValidatedFullPath(site.Id, currentPath); + if (File.Exists(hostedFilePath)) + { + if (!provider.TryGetContentType(hostedFilePath, out var mimeType)) + mimeType = "text/html"; + + context.Response.ContentType = mimeType; + await context.Response.SendFileAsync(hostedFilePath); + return; + } + + var hostedNotFoundPath = psm.GetValidatedFullPath(site.Id, "404.html"); + if (File.Exists(hostedNotFoundPath)) + { + context.Response.ContentType = "text/html"; + await context.Response.SendFileAsync(hostedNotFoundPath); + return; + } + } + + await next(context); + } +} diff --git a/DysonNetwork.Zone/Publication/PublicationSiteService.cs b/DysonNetwork.Zone/Publication/PublicationSiteService.cs index b172e0d..1ace442 100644 --- a/DysonNetwork.Zone/Publication/PublicationSiteService.cs +++ b/DysonNetwork.Zone/Publication/PublicationSiteService.cs @@ -30,7 +30,7 @@ public class PublicationSiteService( if (pub == null) throw new InvalidOperationException("Publisher not found."); pubId = pub.Id; } - + return await db.PublicationSites .If(pubId.HasValue, q => q.Where(s => s.PublisherId == pubId.Value)) .Include(s => s.Pages) @@ -47,21 +47,31 @@ public class PublicationSiteService( public async Task CreateSite(SnPublicationSite site, Guid accountId) { - var perk = (await remoteAccounts.GetAccount(accountId)).PerkSubscription; - var perkLevel = perk is not null ? PerkSubscriptionPrivilege.GetPrivilegeFromIdentifier(perk.Identifier) : 0; + var existingSlugSite = await db.PublicationSites + .AnyAsync(s => EF.Functions.ILike(s.Slug, site.Slug)); + if (existingSlugSite) throw new InvalidOperationException("Site with the slug already exists."); - var maxSite = perkLevel switch + var account = await remoteAccounts.GetAccount(accountId); + if (!account.IsSuperuser) { - 1 => 2, - 2 => 3, - 3 => 5, - _ => 1 - }; + var perk = account.PerkSubscription; + var perkLevel = perk is not null + ? PerkSubscriptionPrivilege.GetPrivilegeFromIdentifier(perk.Identifier) + : 0; - // Check if account has reached the maximum number of sites - var existingSitesCount = await db.PublicationSites.CountAsync(s => s.AccountId == accountId); - if (existingSitesCount >= maxSite) - throw new InvalidOperationException("Account has reached the maximum number of sites allowed."); + var maxSite = perkLevel switch + { + 1 => 2, + 2 => 3, + 3 => 5, + _ => 1 + }; + + // Check if account has reached the maximum number of sites + var existingSitesCount = await db.PublicationSites.CountAsync(s => s.AccountId == accountId); + if (existingSitesCount >= maxSite) + throw new InvalidOperationException("Account has reached the maximum number of sites allowed."); + } // Check if account is member of the publisher var isMember = await publisherService.IsMemberWithRole(site.PublisherId, accountId, PublisherMemberRole.Editor); @@ -205,4 +215,4 @@ public class PublicationSiteService( var defaultPage = site.Pages.FirstOrDefault(p => p.Path == "/"); return defaultPage; } -} +} \ No newline at end of file