[backend/razor] Add basic user page public preview, handle user id redirects correctly

This commit is contained in:
Laura Hausmann 2024-09-15 22:53:29 +02:00
parent 80c4e9dd88
commit 9f2cb34d0e
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
6 changed files with 194 additions and 1 deletions

View file

@ -0,0 +1,42 @@
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Mime;
using Iceshrimp.Backend.Controllers.Shared.Attributes;
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Middleware;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
namespace Iceshrimp.Backend.Controllers.Razor;
[ApiController]
[Authenticate]
[EnableRateLimiting("sliding")]
[Produces(MediaTypeNames.Application.Json)]
public class RedirectController(IOptionsSnapshot<Config.SecuritySection> config, DatabaseContext db) : ControllerBase
{
// @formatter:off
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataUsage", Justification = "IncludeCommonProperties")]
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataQuery", Justification = "IncludeCommonProperties")]
// @formatter:on
[HttpGet("/users/{id}")]
[ProducesResults(HttpStatusCode.OK)]
[ProducesErrors(HttpStatusCode.Forbidden, HttpStatusCode.NotFound)]
public async Task<IActionResult> GetUser(string id)
{
var localUser = HttpContext.GetUser();
if (config.Value.PublicPreview == Enums.PublicPreview.Lockdown && localUser == null)
throw GracefulException.Forbidden("Public preview is disabled on this instance");
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id) ??
throw GracefulException.NotFound("User not found");
return user.IsLocalUser
? Redirect($"/@{user.Username}")
: Redirect(user.UserProfile?.Url ?? user.Uri ?? throw new Exception("Remote user must have an URI"));
}
}

View file

@ -101,6 +101,11 @@ public static class MvcBuilderExtensions
return builder;
}
public static IMvcBuilder AddRouteOverrides(this IMvcBuilder builder)
{
return builder.AddRazorPagesOptions(o => { o.Conventions.AddPageRoute("/User", "@{user}@{host}"); });
}
}
/// <summary>

View file

@ -0,0 +1,41 @@
@page
@attribute [RazorCompiledItemMetadata("RouteTemplate", "/@{user}")]
@using Microsoft.AspNetCore.Razor.Hosting
@model UserModel
@if (Model.User == null)
{
Response.StatusCode = 404;
<div>
<h2>Not found</h2>
<p>This user doesn't appear to exist on this server</p>
</div>
}
else
{
ViewData["title"] = $"@{Model.User.Username} - Iceshrimp.NET";
@section head
{
<meta name="twitter:card" content="summary">
<meta name="og:site_name" content="Iceshrimp.NET">
<meta name="og:title" content="@@@Model.User.Username">
@if (Model.User.UserProfile?.Description is { } bio)
{
<meta name="og:description" content="@bio)">
}
<meta name="og:image" content="@(Model.User.AvatarUrl ?? $"/identicon/{Model.User.Id}")">
}
<div class="user">
<div class="header">
<img src="@(Model.User.AvatarUrl ?? $"/identicon/{Model.User.Id}")" class="avatar" alt="User avatar"/>
<div class="title">
<span>@(Model.User.DisplayName ?? Model.User.Username)</span>
</div>
<span class="acct">@@@Model.User.Acct</span>
</div>
<div class="bio">
@Html.Raw(Model.Bio ?? "<i>This user hasn't added a bio yet.</i>")
</div>
</div>
}

View file

@ -0,0 +1,61 @@
using System.Diagnostics.CodeAnalysis;
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
using Iceshrimp.Backend.Core.Middleware;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
namespace Iceshrimp.Backend.Pages;
public class UserModel(
DatabaseContext db,
IOptions<Config.SecuritySection> security,
IOptions<Config.InstanceSection> instance,
MfmConverter mfm
) : PageModel
{
public new User? User;
public string? Bio;
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataQuery",
Justification = "IncludeCommonProperties")]
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataUsage", Justification = "Same as above")]
public async Task<IActionResult> OnGet(string user, string? host)
{
if (Request.Cookies.ContainsKey("session") || Request.Cookies.ContainsKey("sessions"))
return Partial("Shared/FrontendSPA");
if (security.Value.PublicPreview == Enums.PublicPreview.Lockdown)
throw GracefulException.Forbidden("Public preview is disabled on this instance.",
"The instance administrator has intentionally disabled this feature for privacy reasons.");
//TODO: login button
//TODO: user note view (respect public preview settings - don't show renotes of remote notes if set to restricted or lower)
//TODO: emoji
user = user.ToLowerInvariant();
host = host?.ToLowerInvariant();
if (host == instance.Value.AccountDomain || host == instance.Value.WebDomain)
host = null;
User = await db.Users.IncludeCommonProperties()
.Where(p => p.UsernameLower == user && p.Host == host)
.FirstOrDefaultAsync();
if (User is { IsRemoteUser: true })
return RedirectPermanent(User.UserProfile?.Url ??
User.Uri ??
throw new Exception("User is remote but has no uri"));
if (User?.UserProfile?.Description is { } bio)
Bio = await mfm.ToHtmlAsync(bio, User.UserProfile.Mentions, User.Host, divAsRoot: true);
return Page();
}
}

View file

@ -0,0 +1,44 @@
.bio {
display: flex;
flex-direction: column;
align-items: flex-start;
padding-top: inherit;
}
.user {
background-color: #f7f7f7;
background-color: var(--background-alt);
padding: 12px;
margin: 1em 0;
border-radius: 6px;
overflow: hidden;
}
.header {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(3em, 3em));
grid-template-rows: repeat(2, 1.5em);
}
.avatar {
grid-column: 1;
grid-row: 1/3;
border-radius: 5px;
object-fit: cover;
width: 3em;
max-height: 3em;
}
.title {
grid-column: 2/-1;
grid-row: 1;
padding-left: 10px;
color: var(--text-bright);
font-weight: 600;
}
.acct {
grid-column: 2/-1;
grid-row: 2;
padding-left: 10px;
}

View file

@ -30,7 +30,7 @@ builder.Services.AddAuthorizationPolicies();
builder.Services.AddAuthenticationServices();
builder.Services.AddSignalR().AddMessagePackProtocol();
builder.Services.AddResponseCompression();
builder.Services.AddRazorPages();
builder.Services.AddRazorPages().AddRouteOverrides();
builder.Services.AddServices(builder.Configuration);
builder.Services.ConfigureServices(builder.Configuration);