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
|
||||
/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…
Reference in a new issue