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
									
										
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.idea/.idea.Foxnouns.NET/.idea/.gitignore
									
										
									
										generated
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -11,3 +11,4 @@
 | 
			
		|||
# Datasource local storage ignored files
 | 
			
		||||
/dataSources/
 | 
			
		||||
/dataSources.local.xml
 | 
			
		||||
discord.xml
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,18 +1,19 @@
 | 
			
		|||
using System.Diagnostics;
 | 
			
		||||
using Foxnouns.Backend.Database;
 | 
			
		||||
using Foxnouns.Backend.Middleware;
 | 
			
		||||
using Foxnouns.Backend.Services;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
 | 
			
		||||
namespace Foxnouns.Backend.Controllers;
 | 
			
		||||
 | 
			
		||||
[Route("/api/v2/users")]
 | 
			
		||||
public class UsersController(DatabaseContext db) : ApiControllerBase
 | 
			
		||||
public class UsersController(DatabaseContext db, UserRendererService userRendererService) : ApiControllerBase
 | 
			
		||||
{
 | 
			
		||||
    [HttpGet("{userRef}")]
 | 
			
		||||
    public async Task<IActionResult> GetUser(string userRef)
 | 
			
		||||
    {
 | 
			
		||||
        var user = await db.ResolveUserAsync(userRef);
 | 
			
		||||
        return Ok(user);
 | 
			
		||||
        return Ok(await userRendererService.RenderUserAsync(user));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpGet("@me")]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,16 +13,14 @@ public static class DatabaseQueryExtensions
 | 
			
		|||
        if (Snowflake.TryParse(userRef, out var snowflake))
 | 
			
		||||
        {
 | 
			
		||||
            user = await context.Users
 | 
			
		||||
                .Include(u => u.Members)
 | 
			
		||||
                .FirstOrDefaultAsync(u => u.Id == snowflake);
 | 
			
		||||
            if (user != null) return user;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        user = await context.Users
 | 
			
		||||
            .Include(u => u.Members)
 | 
			
		||||
            .FirstOrDefaultAsync(u => u.Username == userRef);
 | 
			
		||||
        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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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[]")
 | 
			
		||||
                        .HasColumnName("links");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("ListHidden")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("list_hidden");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("MemberTitle")
 | 
			
		||||
                        .HasColumnType("text")
 | 
			
		||||
                        .HasColumnName("member_title");
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,7 @@ public class User : BaseModel
 | 
			
		|||
    public string? MemberTitle { get; set; }
 | 
			
		||||
    public string? Avatar { get; set; }
 | 
			
		||||
    public string[] Links { get; set; } = [];
 | 
			
		||||
    public bool ListHidden { get; set; }
 | 
			
		||||
 | 
			
		||||
    public List<FieldEntry> Names { 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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 ErrorCode ErrorCode = errorCode ?? ErrorCode.InternalServerError;
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
        : ApiError(message, statusCode: HttpStatusCode.BadRequest)
 | 
			
		||||
    {
 | 
			
		||||
        public readonly ModelStateDictionary? Errors = modelState;
 | 
			
		||||
 | 
			
		||||
        public JObject ToJson()
 | 
			
		||||
        {
 | 
			
		||||
            var o = new JObject
 | 
			
		||||
| 
						 | 
				
			
			@ -39,10 +39,10 @@ public class ApiError(string message, HttpStatusCode? statusCode = null) : Foxno
 | 
			
		|||
                { "status", (int)HttpStatusCode.BadRequest },
 | 
			
		||||
                { "code", ErrorCode.BadRequest.ToString() }
 | 
			
		||||
            };
 | 
			
		||||
            if (Errors == null) return o;
 | 
			
		||||
            if (modelState == null) return o;
 | 
			
		||||
 | 
			
		||||
            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
 | 
			
		||||
                {
 | 
			
		||||
| 
						 | 
				
			
			@ -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 enum ErrorCode
 | 
			
		||||
{
 | 
			
		||||
    InternalServerError,
 | 
			
		||||
    Forbidden,
 | 
			
		||||
    BadRequest,
 | 
			
		||||
    AuthenticationError,
 | 
			
		||||
    GenericApiError,
 | 
			
		||||
    UserNotFound,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
using Foxnouns.Backend.Database;
 | 
			
		||||
using Foxnouns.Backend.Middleware;
 | 
			
		||||
using Foxnouns.Backend.Services;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
using Serilog;
 | 
			
		||||
| 
						 | 
				
			
			@ -12,13 +13,13 @@ public static class WebApplicationExtensions
 | 
			
		|||
    /// <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.
 | 
			
		||||
    /// </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 logCfg = new LoggerConfiguration()
 | 
			
		||||
            .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.
 | 
			
		||||
            // Serilog doesn't disable the built-in logs, so we do it here.
 | 
			
		||||
            .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
 | 
			
		||||
| 
						 | 
				
			
			@ -62,7 +63,9 @@ public static class WebApplicationExtensions
 | 
			
		|||
 | 
			
		||||
    public static IServiceCollection AddCustomServices(this IServiceCollection services) => services
 | 
			
		||||
        .AddSingleton<IClock>(SystemClock.Instance)
 | 
			
		||||
        .AddSnowflakeGenerator();
 | 
			
		||||
        .AddSnowflakeGenerator()
 | 
			
		||||
        .AddScoped<UserRendererService>()
 | 
			
		||||
        .AddScoped<MemberRendererService>();
 | 
			
		||||
 | 
			
		||||
    public static IServiceCollection AddCustomMiddleware(this IServiceCollection services) => services
 | 
			
		||||
        .AddScoped<ErrorHandlerMiddleware>()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,7 +45,7 @@ public class ErrorHandlerMiddleware(ILogger baseLogger) : IMiddleware
 | 
			
		|||
                await ctx.Response.WriteAsync(JsonConvert.SerializeObject(new HttpApiError
 | 
			
		||||
                {
 | 
			
		||||
                    Status = (int)ae.StatusCode,
 | 
			
		||||
                    Code = ErrorCode.GenericApiError,
 | 
			
		||||
                    Code = ae.ErrorCode,
 | 
			
		||||
                    Message = ae.Message,
 | 
			
		||||
                }));
 | 
			
		||||
                return;
 | 
			
		||||
| 
						 | 
				
			
			@ -76,18 +76,12 @@ public class ErrorHandlerMiddleware(ILogger baseLogger) : IMiddleware
 | 
			
		|||
public record HttpApiError
 | 
			
		||||
{
 | 
			
		||||
    public required int Status { get; init; }
 | 
			
		||||
 | 
			
		||||
    [JsonConverter(typeof(StringEnumConverter))]
 | 
			
		||||
    public required ErrorCode Code { get; init; }
 | 
			
		||||
 | 
			
		||||
    public required string Message { get; init; }
 | 
			
		||||
 | 
			
		||||
    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
 | 
			
		||||
    public string[]? Scopes { get; init; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public enum ErrorCode
 | 
			
		||||
{
 | 
			
		||||
    InternalServerError,
 | 
			
		||||
    Forbidden,
 | 
			
		||||
    BadRequest,
 | 
			
		||||
    AuthenticationError,
 | 
			
		||||
    GenericApiError,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -3,13 +3,14 @@ using Foxnouns.Backend.Database;
 | 
			
		|||
using Serilog;
 | 
			
		||||
using Foxnouns.Backend.Extensions;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using Newtonsoft.Json;
 | 
			
		||||
using Newtonsoft.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
var builder = WebApplication.CreateBuilder(args);
 | 
			
		||||
 | 
			
		||||
var config = builder.AddConfiguration();
 | 
			
		||||
 | 
			
		||||
builder.AddSerilog(config.LogEventLevel);
 | 
			
		||||
builder.AddSerilog();
 | 
			
		||||
 | 
			
		||||
builder.Services
 | 
			
		||||
    .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
 | 
			
		||||
    .AddDbContext<DatabaseContext>()
 | 
			
		||||
    .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…
	
	Add table
		Add a link
		
	
		Reference in a new issue