♻️ New error page
This commit is contained in:
@@ -1,26 +1,23 @@
|
|||||||
@page
|
@page "/Error/{statusCode?}"
|
||||||
@model ErrorModel
|
@model ErrorModel
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Error";
|
ViewData["Title"] = "Error";
|
||||||
|
ViewData["SiteName"] = Model.SiteName ?? "main";
|
||||||
}
|
}
|
||||||
|
|
||||||
<h1 class="text-danger">Error.</h1>
|
<div class="h-screen flex justify-center items-center">
|
||||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
<div class="text-center max-w-120">
|
||||||
|
<img src="~/favicon.png" width="80" height="80" alt="Logo" class="mb-1 mx-auto"/>
|
||||||
@if (Model.ShowRequestId)
|
<h1 class="text-2xl">@(Model.StatusCode?.ToString() ?? "Error")</h1>
|
||||||
{
|
<p>Something went wrong...</p>
|
||||||
<p>
|
<p class="text-sm opacity-80 mt-1">Powered by the Solar Network Pages</p>
|
||||||
<strong>Request ID:</strong> <code>@Model.RequestId</code>
|
<div class="text-xs opacity-70 mt-1">
|
||||||
</p>
|
<p>Path: <b>@Model.CurrentPath</b></p>
|
||||||
}
|
<p>Site ID: <b>@(Model.Site?.Id.ToString() ?? "none")</b></p>
|
||||||
|
@if (Model.ShowRequestId)
|
||||||
<h3>Development Mode</h3>
|
{
|
||||||
<p>
|
<p>Request ID: <b>@Model.RequestId</b></p>
|
||||||
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
|
}
|
||||||
</p>
|
</div>
|
||||||
<p>
|
</div>
|
||||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
</div>
|
||||||
It can result in displaying sensitive information from exceptions to end users.
|
|
||||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
|
||||||
and restarting the app.
|
|
||||||
</p>
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using DysonNetwork.Shared.Models;
|
||||||
|
using DysonNetwork.Zone.Publication;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
@@ -12,8 +14,20 @@ public class ErrorModel : PageModel
|
|||||||
|
|
||||||
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
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()
|
public void OnGet()
|
||||||
{
|
{
|
||||||
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,22 +2,12 @@
|
|||||||
@model IndexModel
|
@model IndexModel
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Solar Network Pages";
|
ViewData["Title"] = "Solar Network Pages";
|
||||||
ViewData["SiteName"] = Model.SiteName ?? "main";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="h-screen flex justify-center items-center">
|
<div class="h-screen flex justify-center items-center">
|
||||||
<div class="text-center max-w-96">
|
<div class="text-center max-w-96">
|
||||||
<img src="~/favicon.png" width="80" height="80" alt="Logo" class="mb-1 mx-auto"/>
|
<img src="~/favicon.png" width="80" height="80" alt="Logo" class="mb-1 mx-auto"/>
|
||||||
<h1 class="text-2xl">404 Not Found</h1>
|
<h1 class="text-2xl">Hello World 👋</h1>
|
||||||
<p>Here are the Solar Network Pages</p>
|
<p>Here are the Solar Network Pages</p>
|
||||||
<p>And you're accessing the site <b>@(Model.SiteName ?? "main")</b></p>
|
|
||||||
<p class="text-sm opacity-80 mt-1">
|
|
||||||
The reason you're seeing this is the author of the site
|
|
||||||
haven't configure anything that match this route.
|
|
||||||
</p>
|
|
||||||
<div class="text-xs opacity-70 mt-1">
|
|
||||||
<p>Path: <b>@Model.CurrentPath</b></p>
|
|
||||||
<p>Site ID: <b>@(Model.Site?.Id.ToString() ?? "none")</b></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,70 +1,10 @@
|
|||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using DysonNetwork.Zone.Publication;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
using Microsoft.AspNetCore.StaticFiles;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Zone.Pages;
|
namespace DysonNetwork.Zone.Pages;
|
||||||
|
|
||||||
public class IndexModel(AppDatabase db, PublicationSiteManager psm) : PageModel
|
public class IndexModel : PageModel
|
||||||
{
|
{
|
||||||
public SnPublicationSite? Site { get; set; }
|
public void OnGet()
|
||||||
public string? SiteName { get; set; }
|
|
||||||
public string? CurrentPath { get; set; }
|
|
||||||
|
|
||||||
public async Task<IActionResult> OnGet(string? path = null)
|
|
||||||
{
|
{
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ using DysonNetwork.Zone.Startup;
|
|||||||
using DysonNetwork.Shared.Auth;
|
using DysonNetwork.Shared.Auth;
|
||||||
using DysonNetwork.Shared.Http;
|
using DysonNetwork.Shared.Http;
|
||||||
using DysonNetwork.Shared.Registry;
|
using DysonNetwork.Shared.Registry;
|
||||||
|
using DysonNetwork.Zone.Publication;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
@@ -11,7 +12,7 @@ builder.AddServiceDefaults();
|
|||||||
|
|
||||||
builder.ConfigureAppKestrel(builder.Configuration);
|
builder.ConfigureAppKestrel(builder.Configuration);
|
||||||
|
|
||||||
builder.Services.AddRazorPages(options => options.Conventions.AddPageRoute("/Index", "{**path}"));
|
builder.Services.AddRazorPages();
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
|
|
||||||
builder.Services.AddAppServices();
|
builder.Services.AddAppServices();
|
||||||
@@ -46,8 +47,11 @@ using (var scope = app.Services.CreateScope())
|
|||||||
if (!app.Environment.IsDevelopment())
|
if (!app.Environment.IsDevelopment())
|
||||||
app.UseExceptionHandler("/Error");
|
app.UseExceptionHandler("/Error");
|
||||||
|
|
||||||
|
app.UseMiddleware<PublicationSiteMiddleware>();
|
||||||
|
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
app.UseStatusCodePagesWithReExecute("/Error/{0}");
|
||||||
app.ConfigureAppMiddleware(builder.Configuration);
|
app.ConfigureAppMiddleware(builder.Configuration);
|
||||||
app.MapRazorPages();
|
app.MapRazorPages();
|
||||||
|
|
||||||
|
|||||||
77
DysonNetwork.Zone/Publication/PublicationSiteMiddleware.cs
Normal file
77
DysonNetwork.Zone/Publication/PublicationSiteMiddleware.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ public class PublicationSiteService(
|
|||||||
if (pub == null) throw new InvalidOperationException("Publisher not found.");
|
if (pub == null) throw new InvalidOperationException("Publisher not found.");
|
||||||
pubId = pub.Id;
|
pubId = pub.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await db.PublicationSites
|
return await db.PublicationSites
|
||||||
.If(pubId.HasValue, q => q.Where(s => s.PublisherId == pubId.Value))
|
.If(pubId.HasValue, q => q.Where(s => s.PublisherId == pubId.Value))
|
||||||
.Include(s => s.Pages)
|
.Include(s => s.Pages)
|
||||||
@@ -47,21 +47,31 @@ public class PublicationSiteService(
|
|||||||
|
|
||||||
public async Task<SnPublicationSite> CreateSite(SnPublicationSite site, Guid accountId)
|
public async Task<SnPublicationSite> CreateSite(SnPublicationSite site, Guid accountId)
|
||||||
{
|
{
|
||||||
var perk = (await remoteAccounts.GetAccount(accountId)).PerkSubscription;
|
var existingSlugSite = await db.PublicationSites
|
||||||
var perkLevel = perk is not null ? PerkSubscriptionPrivilege.GetPrivilegeFromIdentifier(perk.Identifier) : 0;
|
.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,
|
var perk = account.PerkSubscription;
|
||||||
2 => 3,
|
var perkLevel = perk is not null
|
||||||
3 => 5,
|
? PerkSubscriptionPrivilege.GetPrivilegeFromIdentifier(perk.Identifier)
|
||||||
_ => 1
|
: 0;
|
||||||
};
|
|
||||||
|
|
||||||
// Check if account has reached the maximum number of sites
|
var maxSite = perkLevel switch
|
||||||
var existingSitesCount = await db.PublicationSites.CountAsync(s => s.AccountId == accountId);
|
{
|
||||||
if (existingSitesCount >= maxSite)
|
1 => 2,
|
||||||
throw new InvalidOperationException("Account has reached the maximum number of sites allowed.");
|
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
|
// Check if account is member of the publisher
|
||||||
var isMember = await publisherService.IsMemberWithRole(site.PublisherId, accountId, PublisherMemberRole.Editor);
|
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 == "/");
|
var defaultPage = site.Pages.FirstOrDefault(p => p.Path == "/");
|
||||||
return defaultPage;
|
return defaultPage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user