add UserRendererService and improve errors
This commit is contained in:
parent
6114f384a0
commit
f674d059fd
14 changed files with 607 additions and 25 deletions
2
.editorconfig
Normal file
2
.editorconfig
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[*.cs]
|
||||||
|
resharper_entity_framework_model_validation_unlimited_string_length_highlighting = none
|
1
.idea/.idea.Foxnouns.NET/.idea/.gitignore
vendored
1
.idea/.idea.Foxnouns.NET/.idea/.gitignore
vendored
|
@ -11,3 +11,4 @@
|
||||||
# Datasource local storage ignored files
|
# Datasource local storage ignored files
|
||||||
/dataSources/
|
/dataSources/
|
||||||
/dataSources.local.xml
|
/dataSources.local.xml
|
||||||
|
discord.xml
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using Foxnouns.Backend.Database;
|
using Foxnouns.Backend.Database;
|
||||||
using Foxnouns.Backend.Middleware;
|
using Foxnouns.Backend.Middleware;
|
||||||
|
using Foxnouns.Backend.Services;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Foxnouns.Backend.Controllers;
|
namespace Foxnouns.Backend.Controllers;
|
||||||
|
|
||||||
[Route("/api/v2/users")]
|
[Route("/api/v2/users")]
|
||||||
public class UsersController(DatabaseContext db) : ApiControllerBase
|
public class UsersController(DatabaseContext db, UserRendererService userRendererService) : ApiControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet("{userRef}")]
|
[HttpGet("{userRef}")]
|
||||||
public async Task<IActionResult> GetUser(string userRef)
|
public async Task<IActionResult> GetUser(string userRef)
|
||||||
{
|
{
|
||||||
var user = await db.ResolveUserAsync(userRef);
|
var user = await db.ResolveUserAsync(userRef);
|
||||||
return Ok(user);
|
return Ok(await userRendererService.RenderUserAsync(user));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("@me")]
|
[HttpGet("@me")]
|
||||||
|
|
|
@ -13,16 +13,14 @@ public static class DatabaseQueryExtensions
|
||||||
if (Snowflake.TryParse(userRef, out var snowflake))
|
if (Snowflake.TryParse(userRef, out var snowflake))
|
||||||
{
|
{
|
||||||
user = await context.Users
|
user = await context.Users
|
||||||
.Include(u => u.Members)
|
|
||||||
.FirstOrDefaultAsync(u => u.Id == snowflake);
|
.FirstOrDefaultAsync(u => u.Id == snowflake);
|
||||||
if (user != null) return user;
|
if (user != null) return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
user = await context.Users
|
user = await context.Users
|
||||||
.Include(u => u.Members)
|
|
||||||
.FirstOrDefaultAsync(u => u.Username == userRef);
|
.FirstOrDefaultAsync(u => u.Username == userRef);
|
||||||
if (user != null) return user;
|
if (user != null) return user;
|
||||||
throw new FoxnounsError.UnknownEntityError(typeof(User));
|
throw new ApiError.NotFound("No user with that ID or username found.", code: ErrorCode.UserNotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<Application> GetFrontendApplicationAsync(this DatabaseContext context)
|
public static async Task<Application> GetFrontendApplicationAsync(this DatabaseContext context)
|
||||||
|
|
474
Foxnouns.Backend/Database/Migrations/20240528145744_AddListHidden.Designer.cs
generated
Normal file
474
Foxnouns.Backend/Database/Migrations/20240528145744_AddListHidden.Designer.cs
generated
Normal file
|
@ -0,0 +1,474 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using Foxnouns.Backend.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using NodaTime;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Foxnouns.Backend.Database.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(DatabaseContext))]
|
||||||
|
[Migration("20240528145744_AddListHidden")]
|
||||||
|
partial class AddListHidden
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.5")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Application", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("ClientId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("client_id");
|
||||||
|
|
||||||
|
b.Property<string>("ClientSecret")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("client_secret");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<string[]>("RedirectUris")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text[]")
|
||||||
|
.HasColumnName("redirect_uris");
|
||||||
|
|
||||||
|
b.Property<string[]>("Scopes")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text[]")
|
||||||
|
.HasColumnName("scopes");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_applications");
|
||||||
|
|
||||||
|
b.ToTable("applications", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<int>("AuthType")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("auth_type");
|
||||||
|
|
||||||
|
b.Property<long?>("FediverseApplicationId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("fediverse_application_id");
|
||||||
|
|
||||||
|
b.Property<string>("RemoteId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("remote_id");
|
||||||
|
|
||||||
|
b.Property<string>("RemoteUsername")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("remote_username");
|
||||||
|
|
||||||
|
b.Property<long>("UserId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_auth_methods");
|
||||||
|
|
||||||
|
b.HasIndex("FediverseApplicationId")
|
||||||
|
.HasDatabaseName("ix_auth_methods_fediverse_application_id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("ix_auth_methods_user_id");
|
||||||
|
|
||||||
|
b.ToTable("auth_methods", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Foxnouns.Backend.Database.Models.FediverseApplication", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("ClientId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("client_id");
|
||||||
|
|
||||||
|
b.Property<string>("ClientSecret")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("client_secret");
|
||||||
|
|
||||||
|
b.Property<string>("Domain")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("domain");
|
||||||
|
|
||||||
|
b.Property<int>("InstanceType")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("instance_type");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_fediverse_applications");
|
||||||
|
|
||||||
|
b.ToTable("fediverse_applications", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("Avatar")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("avatar");
|
||||||
|
|
||||||
|
b.Property<string>("Bio")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("bio");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("display_name");
|
||||||
|
|
||||||
|
b.Property<string[]>("Links")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text[]")
|
||||||
|
.HasColumnName("links");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<bool>("Unlisted")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("unlisted");
|
||||||
|
|
||||||
|
b.Property<long>("UserId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_members");
|
||||||
|
|
||||||
|
b.HasIndex("UserId", "Name")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("ix_members_user_id_name");
|
||||||
|
|
||||||
|
b.ToTable("members", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<long>("ApplicationId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("application_id");
|
||||||
|
|
||||||
|
b.Property<Instant>("ExpiresAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("expires_at");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Hash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("bytea")
|
||||||
|
.HasColumnName("hash");
|
||||||
|
|
||||||
|
b.Property<bool>("ManuallyExpired")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("manually_expired");
|
||||||
|
|
||||||
|
b.Property<string[]>("Scopes")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text[]")
|
||||||
|
.HasColumnName("scopes");
|
||||||
|
|
||||||
|
b.Property<long>("UserId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_tokens");
|
||||||
|
|
||||||
|
b.HasIndex("ApplicationId")
|
||||||
|
.HasDatabaseName("ix_tokens_application_id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("ix_tokens_user_id");
|
||||||
|
|
||||||
|
b.ToTable("tokens", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("Avatar")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("avatar");
|
||||||
|
|
||||||
|
b.Property<string>("Bio")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("bio");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("display_name");
|
||||||
|
|
||||||
|
b.Property<string[]>("Links")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text[]")
|
||||||
|
.HasColumnName("links");
|
||||||
|
|
||||||
|
b.Property<bool>("ListHidden")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("list_hidden");
|
||||||
|
|
||||||
|
b.Property<string>("MemberTitle")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("member_title");
|
||||||
|
|
||||||
|
b.Property<int>("Role")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("role");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("username");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_users");
|
||||||
|
|
||||||
|
b.HasIndex("Username")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("ix_users_username");
|
||||||
|
|
||||||
|
b.ToTable("users", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Foxnouns.Backend.Database.Models.FediverseApplication", "FediverseApplication")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("FediverseApplicationId")
|
||||||
|
.HasConstraintName("fk_auth_methods_fediverse_applications_fediverse_application_id");
|
||||||
|
|
||||||
|
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||||
|
.WithMany("AuthMethods")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_auth_methods_users_user_id");
|
||||||
|
|
||||||
|
b.Navigation("FediverseApplication");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||||
|
.WithMany("Members")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_members_users_user_id");
|
||||||
|
|
||||||
|
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Field>", "Fields", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<long>("MemberId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b1.Property<int>("Capacity")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b1.HasKey("MemberId");
|
||||||
|
|
||||||
|
b1.ToTable("members");
|
||||||
|
|
||||||
|
b1.ToJson("fields");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("MemberId")
|
||||||
|
.HasConstraintName("fk_members_members_id");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.FieldEntry>", "Names", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<long>("MemberId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b1.Property<int>("Capacity")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b1.HasKey("MemberId");
|
||||||
|
|
||||||
|
b1.ToTable("members");
|
||||||
|
|
||||||
|
b1.ToJson("names");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("MemberId")
|
||||||
|
.HasConstraintName("fk_members_members_id");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Pronoun>", "Pronouns", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<long>("MemberId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b1.Property<int>("Capacity")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b1.HasKey("MemberId");
|
||||||
|
|
||||||
|
b1.ToTable("members");
|
||||||
|
|
||||||
|
b1.ToJson("pronouns");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("MemberId")
|
||||||
|
.HasConstraintName("fk_members_members_id");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("Fields")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Names")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Pronouns")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Foxnouns.Backend.Database.Models.Application", "Application")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ApplicationId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_tokens_applications_application_id");
|
||||||
|
|
||||||
|
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_tokens_users_user_id");
|
||||||
|
|
||||||
|
b.Navigation("Application");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Fields#List", "Fields", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<long>("UserId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b1.Property<int>("Capacity")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b1.HasKey("UserId")
|
||||||
|
.HasName("pk_users");
|
||||||
|
|
||||||
|
b1.ToTable("users");
|
||||||
|
|
||||||
|
b1.ToJson("fields");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.HasConstraintName("fk_users_users_user_id");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Names#List", "Names", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<long>("UserId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b1.Property<int>("Capacity")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b1.HasKey("UserId")
|
||||||
|
.HasName("pk_users");
|
||||||
|
|
||||||
|
b1.ToTable("users");
|
||||||
|
|
||||||
|
b1.ToJson("names");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.HasConstraintName("fk_users_users_user_id");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Pronouns#List", "Pronouns", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<long>("UserId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b1.Property<int>("Capacity")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b1.HasKey("UserId")
|
||||||
|
.HasName("pk_users");
|
||||||
|
|
||||||
|
b1.ToTable("users");
|
||||||
|
|
||||||
|
b1.ToJson("pronouns");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.HasConstraintName("fk_users_users_user_id");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("Fields")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Names")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Pronouns")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("AuthMethods");
|
||||||
|
|
||||||
|
b.Navigation("Members");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Foxnouns.Backend.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddListHidden : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "list_hidden",
|
||||||
|
table: "users",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "list_hidden",
|
||||||
|
table: "users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -242,6 +242,10 @@ namespace Foxnouns.Backend.Database.Migrations
|
||||||
.HasColumnType("text[]")
|
.HasColumnType("text[]")
|
||||||
.HasColumnName("links");
|
.HasColumnName("links");
|
||||||
|
|
||||||
|
b.Property<bool>("ListHidden")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("list_hidden");
|
||||||
|
|
||||||
b.Property<string>("MemberTitle")
|
b.Property<string>("MemberTitle")
|
||||||
.HasColumnType("text")
|
.HasColumnType("text")
|
||||||
.HasColumnName("member_title");
|
.HasColumnName("member_title");
|
||||||
|
|
|
@ -8,6 +8,7 @@ public class User : BaseModel
|
||||||
public string? MemberTitle { get; set; }
|
public string? MemberTitle { get; set; }
|
||||||
public string? Avatar { get; set; }
|
public string? Avatar { get; set; }
|
||||||
public string[] Links { get; set; } = [];
|
public string[] Links { get; set; } = [];
|
||||||
|
public bool ListHidden { get; set; }
|
||||||
|
|
||||||
public List<FieldEntry> Names { get; set; } = [];
|
public List<FieldEntry> Names { get; set; } = [];
|
||||||
public List<Pronoun> Pronouns { get; set; } = [];
|
public List<Pronoun> Pronouns { get; set; } = [];
|
||||||
|
|
|
@ -15,9 +15,11 @@ public class FoxnounsError(string message, Exception? inner = null) : Exception(
|
||||||
: DatabaseError($"Entity of type {entityType.Name} not found", inner);
|
: DatabaseError($"Entity of type {entityType.Name} not found", inner);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ApiError(string message, HttpStatusCode? statusCode = null) : FoxnounsError(message)
|
public class ApiError(string message, HttpStatusCode? statusCode = null, ErrorCode? errorCode = null)
|
||||||
|
: FoxnounsError(message)
|
||||||
{
|
{
|
||||||
public readonly HttpStatusCode StatusCode = statusCode ?? HttpStatusCode.InternalServerError;
|
public readonly HttpStatusCode StatusCode = statusCode ?? HttpStatusCode.InternalServerError;
|
||||||
|
public readonly ErrorCode ErrorCode = errorCode ?? ErrorCode.InternalServerError;
|
||||||
|
|
||||||
public class Unauthorized(string message) : ApiError(message, statusCode: HttpStatusCode.Unauthorized);
|
public class Unauthorized(string message) : ApiError(message, statusCode: HttpStatusCode.Unauthorized);
|
||||||
|
|
||||||
|
@ -30,8 +32,6 @@ public class ApiError(string message, HttpStatusCode? statusCode = null) : Foxno
|
||||||
public class BadRequest(string message, ModelStateDictionary? modelState = null)
|
public class BadRequest(string message, ModelStateDictionary? modelState = null)
|
||||||
: ApiError(message, statusCode: HttpStatusCode.BadRequest)
|
: ApiError(message, statusCode: HttpStatusCode.BadRequest)
|
||||||
{
|
{
|
||||||
public readonly ModelStateDictionary? Errors = modelState;
|
|
||||||
|
|
||||||
public JObject ToJson()
|
public JObject ToJson()
|
||||||
{
|
{
|
||||||
var o = new JObject
|
var o = new JObject
|
||||||
|
@ -39,10 +39,10 @@ public class ApiError(string message, HttpStatusCode? statusCode = null) : Foxno
|
||||||
{ "status", (int)HttpStatusCode.BadRequest },
|
{ "status", (int)HttpStatusCode.BadRequest },
|
||||||
{ "code", ErrorCode.BadRequest.ToString() }
|
{ "code", ErrorCode.BadRequest.ToString() }
|
||||||
};
|
};
|
||||||
if (Errors == null) return o;
|
if (modelState == null) return o;
|
||||||
|
|
||||||
var a = new JArray();
|
var a = new JArray();
|
||||||
foreach (var error in Errors.Where(e => e.Value is { Errors.Count: > 0 }))
|
foreach (var error in modelState.Where(e => e.Value is { Errors.Count: > 0 }))
|
||||||
{
|
{
|
||||||
var errorObj = new JObject
|
var errorObj = new JObject
|
||||||
{
|
{
|
||||||
|
@ -61,7 +61,18 @@ public class ApiError(string message, HttpStatusCode? statusCode = null) : Foxno
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NotFound(string message) : ApiError(message, statusCode: HttpStatusCode.NotFound);
|
public class NotFound(string message, ErrorCode? code = null)
|
||||||
|
: ApiError(message, statusCode: HttpStatusCode.NotFound, errorCode: code);
|
||||||
|
|
||||||
public class AuthenticationError(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest);
|
public class AuthenticationError(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ErrorCode
|
||||||
|
{
|
||||||
|
InternalServerError,
|
||||||
|
Forbidden,
|
||||||
|
BadRequest,
|
||||||
|
AuthenticationError,
|
||||||
|
GenericApiError,
|
||||||
|
UserNotFound,
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
using Foxnouns.Backend.Database;
|
using Foxnouns.Backend.Database;
|
||||||
using Foxnouns.Backend.Middleware;
|
using Foxnouns.Backend.Middleware;
|
||||||
|
using Foxnouns.Backend.Services;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
@ -12,13 +13,13 @@ public static class WebApplicationExtensions
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds Serilog to this service collection. This method also initializes Serilog, so it should be called as early as possible, before any log calls.
|
/// Adds Serilog to this service collection. This method also initializes Serilog, so it should be called as early as possible, before any log calls.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static WebApplicationBuilder AddSerilog(this WebApplicationBuilder builder, LogEventLevel level)
|
public static WebApplicationBuilder AddSerilog(this WebApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
var config = builder.Configuration.Get<Config>() ?? new();
|
var config = builder.Configuration.Get<Config>() ?? new();
|
||||||
|
|
||||||
var logCfg = new LoggerConfiguration()
|
var logCfg = new LoggerConfiguration()
|
||||||
.Enrich.FromLogContext()
|
.Enrich.FromLogContext()
|
||||||
.MinimumLevel.Is(level)
|
.MinimumLevel.Is(config.LogEventLevel)
|
||||||
// ASP.NET's built in request logs are extremely verbose, so we use Serilog's instead.
|
// ASP.NET's built in request logs are extremely verbose, so we use Serilog's instead.
|
||||||
// Serilog doesn't disable the built-in logs, so we do it here.
|
// Serilog doesn't disable the built-in logs, so we do it here.
|
||||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
|
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
|
||||||
|
@ -62,7 +63,9 @@ public static class WebApplicationExtensions
|
||||||
|
|
||||||
public static IServiceCollection AddCustomServices(this IServiceCollection services) => services
|
public static IServiceCollection AddCustomServices(this IServiceCollection services) => services
|
||||||
.AddSingleton<IClock>(SystemClock.Instance)
|
.AddSingleton<IClock>(SystemClock.Instance)
|
||||||
.AddSnowflakeGenerator();
|
.AddSnowflakeGenerator()
|
||||||
|
.AddScoped<UserRendererService>()
|
||||||
|
.AddScoped<MemberRendererService>();
|
||||||
|
|
||||||
public static IServiceCollection AddCustomMiddleware(this IServiceCollection services) => services
|
public static IServiceCollection AddCustomMiddleware(this IServiceCollection services) => services
|
||||||
.AddScoped<ErrorHandlerMiddleware>()
|
.AddScoped<ErrorHandlerMiddleware>()
|
||||||
|
|
|
@ -45,7 +45,7 @@ public class ErrorHandlerMiddleware(ILogger baseLogger) : IMiddleware
|
||||||
await ctx.Response.WriteAsync(JsonConvert.SerializeObject(new HttpApiError
|
await ctx.Response.WriteAsync(JsonConvert.SerializeObject(new HttpApiError
|
||||||
{
|
{
|
||||||
Status = (int)ae.StatusCode,
|
Status = (int)ae.StatusCode,
|
||||||
Code = ErrorCode.GenericApiError,
|
Code = ae.ErrorCode,
|
||||||
Message = ae.Message,
|
Message = ae.Message,
|
||||||
}));
|
}));
|
||||||
return;
|
return;
|
||||||
|
@ -76,18 +76,12 @@ public class ErrorHandlerMiddleware(ILogger baseLogger) : IMiddleware
|
||||||
public record HttpApiError
|
public record HttpApiError
|
||||||
{
|
{
|
||||||
public required int Status { get; init; }
|
public required int Status { get; init; }
|
||||||
|
|
||||||
[JsonConverter(typeof(StringEnumConverter))]
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
public required ErrorCode Code { get; init; }
|
public required ErrorCode Code { get; init; }
|
||||||
|
|
||||||
public required string Message { get; init; }
|
public required string Message { get; init; }
|
||||||
|
|
||||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||||
public string[]? Scopes { get; init; }
|
public string[]? Scopes { get; init; }
|
||||||
}
|
|
||||||
|
|
||||||
public enum ErrorCode
|
|
||||||
{
|
|
||||||
InternalServerError,
|
|
||||||
Forbidden,
|
|
||||||
BadRequest,
|
|
||||||
AuthenticationError,
|
|
||||||
GenericApiError,
|
|
||||||
}
|
}
|
|
@ -3,13 +3,14 @@ using Foxnouns.Backend.Database;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Foxnouns.Backend.Extensions;
|
using Foxnouns.Backend.Extensions;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Serialization;
|
using Newtonsoft.Json.Serialization;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
var config = builder.AddConfiguration();
|
var config = builder.AddConfiguration();
|
||||||
|
|
||||||
builder.AddSerilog(config.LogEventLevel);
|
builder.AddSerilog();
|
||||||
|
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddControllers()
|
.AddControllers()
|
||||||
|
@ -25,6 +26,15 @@ builder.Services
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set the default converter to snake case as we use it in a couple places.
|
||||||
|
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
ContractResolver = new DefaultContractResolver
|
||||||
|
{
|
||||||
|
NamingStrategy = new SnakeCaseNamingStrategy()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddDbContext<DatabaseContext>()
|
.AddDbContext<DatabaseContext>()
|
||||||
.AddCustomServices()
|
.AddCustomServices()
|
||||||
|
|
18
Foxnouns.Backend/Services/MemberRendererService.cs
Normal file
18
Foxnouns.Backend/Services/MemberRendererService.cs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
using Foxnouns.Backend.Database;
|
||||||
|
using Foxnouns.Backend.Database.Models;
|
||||||
|
|
||||||
|
namespace Foxnouns.Backend.Services;
|
||||||
|
|
||||||
|
public class MemberRendererService(DatabaseContext db)
|
||||||
|
{
|
||||||
|
public PartialMember RenderPartialMember(Member member) => new(member.Id, member.Name,
|
||||||
|
member.DisplayName, member.Bio, member.Names, member.Pronouns);
|
||||||
|
|
||||||
|
public record PartialMember(
|
||||||
|
Snowflake Id,
|
||||||
|
string Name,
|
||||||
|
string? DisplayName,
|
||||||
|
string? Bio,
|
||||||
|
IEnumerable<FieldEntry> Names,
|
||||||
|
IEnumerable<Pronoun> Pronouns);
|
||||||
|
}
|
36
Foxnouns.Backend/Services/UserRendererService.cs
Normal file
36
Foxnouns.Backend/Services/UserRendererService.cs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
using Foxnouns.Backend.Database;
|
||||||
|
using Foxnouns.Backend.Database.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Foxnouns.Backend.Services;
|
||||||
|
|
||||||
|
public class UserRendererService(DatabaseContext db, MemberRendererService memberRendererService)
|
||||||
|
{
|
||||||
|
public async Task<object> RenderUserAsync(User user, User? selfUser = null, bool renderMembers = true)
|
||||||
|
{
|
||||||
|
renderMembers = renderMembers && (!user.ListHidden || selfUser?.Id == user.Id);
|
||||||
|
|
||||||
|
var members = renderMembers ? await db.Members.Where(m => m.UserId == user.Id).ToListAsync() : [];
|
||||||
|
|
||||||
|
return new UserResponse(
|
||||||
|
user.Id, user.Username, user.DisplayName, user.Bio, user.MemberTitle, user.Avatar, user.Links, user.Names,
|
||||||
|
user.Pronouns, user.Fields,
|
||||||
|
renderMembers ? members.Select(memberRendererService.RenderPartialMember) : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record UserResponse(
|
||||||
|
Snowflake Id,
|
||||||
|
string Username,
|
||||||
|
string? DisplayName,
|
||||||
|
string? Bio,
|
||||||
|
string? MemberTitle,
|
||||||
|
string? AvatarUrl,
|
||||||
|
string[] Links,
|
||||||
|
IEnumerable<FieldEntry> Names,
|
||||||
|
IEnumerable<Pronoun> Pronouns,
|
||||||
|
IEnumerable<Field> Fields,
|
||||||
|
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||||
|
IEnumerable<MemberRendererService.PartialMember>? Members
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in a new issue