From 22d09ad7a6e2613a48305c4086f5e6c21926bea7 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 5 Sep 2024 21:10:45 +0200 Subject: [PATCH 1/2] fix: return correct error in GET /users/@me --- Foxnouns.Backend/Controllers/UsersController.cs | 13 ++++++++++--- .../Database/DatabaseQueryExtensions.cs | 16 +++++++++++----- Foxnouns.Backend/ExpectedError.cs | 14 ++++++++++---- .../Middleware/AuthorizationMiddleware.cs | 4 ++-- Foxnouns.Backend/Services/UserRendererService.cs | 7 ++++--- 5 files changed, 37 insertions(+), 17 deletions(-) diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index 78967f3..8fdf235 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -20,15 +20,16 @@ public class UsersController( { [HttpGet("{userRef}")] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] - public async Task GetUserAsync(string userRef) + public async Task GetUserAsync(string userRef, CancellationToken ct = default) { - var user = await db.ResolveUserAsync(userRef, CurrentToken); + var user = await db.ResolveUserAsync(userRef, CurrentToken, ct); return Ok(await userRendererService.RenderUserAsync( user, selfUser: CurrentUser, token: CurrentToken, renderMembers: true, - renderAuthMethods: true + renderAuthMethods: true, + ct: ct )); } @@ -59,6 +60,11 @@ public class UsersController( user.Bio = req.Bio; } + if (req.HasProperty(nameof(req.Links))) + { + user.Links = req.Links ?? []; + } + if (req.HasProperty(nameof(req.Avatar))) errors.Add(("avatar", ValidationUtils.ValidateAvatar(req.Avatar))); @@ -150,5 +156,6 @@ public class UsersController( public string? DisplayName { get; init; } public string? Bio { get; init; } public string? Avatar { get; init; } + public string[]? Links { get; init; } } } \ No newline at end of file diff --git a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs index 1d4e851..b6ccaaa 100644 --- a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs +++ b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs @@ -9,23 +9,29 @@ namespace Foxnouns.Backend.Database; public static class DatabaseQueryExtensions { - public static async Task ResolveUserAsync(this DatabaseContext context, string userRef, Token? token) + public static async Task ResolveUserAsync(this DatabaseContext context, string userRef, Token? token, + CancellationToken ct = default) { - if (userRef == "@me" && token != null) - return await context.Users.FirstAsync(u => u.Id == token.UserId); + if (userRef == "@me") + { + return token != null + ? await context.Users.FirstAsync(u => u.Id == token.UserId, ct) + : throw new ApiError.Unauthorized("This endpoint requires an authenticated user.", + ErrorCode.AuthenticationRequired); + } User? user; if (Snowflake.TryParse(userRef, out var snowflake)) { user = await context.Users .Where(u => !u.Deleted) - .FirstOrDefaultAsync(u => u.Id == snowflake); + .FirstOrDefaultAsync(u => u.Id == snowflake, ct); if (user != null) return user; } user = await context.Users .Where(u => !u.Deleted) - .FirstOrDefaultAsync(u => u.Username == userRef); + .FirstOrDefaultAsync(u => u.Username == userRef, ct); if (user != null) return user; throw new ApiError.NotFound("No user with that ID or username found.", code: ErrorCode.UserNotFound); } diff --git a/Foxnouns.Backend/ExpectedError.cs b/Foxnouns.Backend/ExpectedError.cs index 6e39af7..f277265 100644 --- a/Foxnouns.Backend/ExpectedError.cs +++ b/Foxnouns.Backend/ExpectedError.cs @@ -23,11 +23,15 @@ public class ApiError(string message, HttpStatusCode? statusCode = null, ErrorCo public readonly HttpStatusCode StatusCode = statusCode ?? HttpStatusCode.InternalServerError; public readonly ErrorCode ErrorCode = errorCode ?? ErrorCode.InternalServerError; - public class Unauthorized(string message) : ApiError(message, statusCode: HttpStatusCode.Unauthorized, - errorCode: ErrorCode.AuthenticationError); + public class Unauthorized(string message, ErrorCode errorCode = ErrorCode.AuthenticationError) : ApiError(message, + statusCode: HttpStatusCode.Unauthorized, + errorCode: errorCode); - public class Forbidden(string message, IEnumerable? scopes = null) - : ApiError(message, statusCode: HttpStatusCode.Forbidden) + public class Forbidden( + string message, + IEnumerable? scopes = null, + ErrorCode errorCode = ErrorCode.Forbidden) + : ApiError(message, statusCode: HttpStatusCode.Forbidden, errorCode: errorCode) { public readonly string[] Scopes = scopes?.ToArray() ?? []; } @@ -115,6 +119,8 @@ public enum ErrorCode Forbidden, BadRequest, AuthenticationError, + AuthenticationRequired, + MissingScopes, GenericApiError, UserNotFound, MemberNotFound, diff --git a/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs b/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs index dd0d97f..6d4bc49 100644 --- a/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs +++ b/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs @@ -18,10 +18,10 @@ public class AuthorizationMiddleware : IMiddleware var token = ctx.GetToken(); if (token == null) - throw new ApiError.Unauthorized("This endpoint requires an authenticated user."); + throw new ApiError.Unauthorized("This endpoint requires an authenticated user.", ErrorCode.AuthenticationRequired); if (attribute.Scopes.Length > 0 && attribute.Scopes.Except(token.Scopes.ExpandScopes()).Any()) throw new ApiError.Forbidden("This endpoint requires ungranted scopes.", - attribute.Scopes.Except(token.Scopes.ExpandScopes())); + attribute.Scopes.Except(token.Scopes.ExpandScopes()), ErrorCode.MissingScopes); if (attribute.RequireAdmin && token.User.Role != UserRole.Admin) throw new ApiError.Forbidden("This endpoint can only be used by admins."); if (attribute.RequireModerator && token.User.Role != UserRole.Admin && token.User.Role != UserRole.Moderator) diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index 9bb198e..4449488 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -12,7 +12,8 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe public async Task RenderUserAsync(User user, User? selfUser = null, Token? token = null, bool renderMembers = true, - bool renderAuthMethods = false) + bool renderAuthMethods = false, + CancellationToken ct = default) { var isSelfUser = selfUser?.Id == user.Id; var tokenCanReadHiddenMembers = token.HasScope("member.read") && isSelfUser; @@ -24,7 +25,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe renderAuthMethods = renderAuthMethods && tokenPrivileged; IEnumerable members = - renderMembers ? await db.Members.Where(m => m.UserId == user.Id).ToListAsync() : []; + renderMembers ? await db.Members.Where(m => m.UserId == user.Id).ToListAsync(ct) : []; // Unless the user is requesting their own members AND the token can read hidden members, we filter out unlisted members. if (!(isSelfUser && tokenCanReadHiddenMembers)) members = members.Where(m => m.Unlisted); @@ -32,7 +33,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe ? await db.AuthMethods .Where(a => a.UserId == user.Id) .Include(a => a.FediverseApplication) - .ToListAsync() + .ToListAsync(ct) : []; return new UserResponse( From fa3c1ccaa7a924b28eb230b590247848f926bd83 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 5 Sep 2024 22:17:10 +0200 Subject: [PATCH 2/2] feat: add user settings endpoint --- .../Controllers/UsersController.cs | 31 ++ Foxnouns.Backend/Database/DatabaseContext.cs | 1 + ...20240905191709_AddUserSettings.Designer.cs | 432 ++++++++++++++++++ .../20240905191709_AddUserSettings.cs | 30 ++ .../DatabaseContextModelSnapshot.cs | 175 ++----- Foxnouns.Backend/Database/Models/User.cs | 6 + 6 files changed, 536 insertions(+), 139 deletions(-) create mode 100644 Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.Designer.cs create mode 100644 Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.cs diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index 8fdf235..a2c6219 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -158,4 +158,35 @@ public class UsersController( public string? Avatar { get; init; } public string[]? Links { get; init; } } + + + [HttpGet("@me/settings")] + [Authorize("user.read_hidden")] + [ProducesResponseType(statusCode: StatusCodes.Status200OK)] + public async Task GetUserSettingsAsync(CancellationToken ct = default) + { + var user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id, ct); + return Ok(user.Settings); + } + + [HttpPatch("@me/settings")] + [Authorize("user.read_hidden", "user.update")] + [ProducesResponseType(statusCode: StatusCodes.Status200OK)] + public async Task UpdateUserSettingsAsync([FromBody] UpdateUserSettingsRequest req, CancellationToken ct = default) + { + var user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id, ct); + + if (req.HasProperty(nameof(req.DarkMode))) + user.Settings.DarkMode = req.DarkMode; + + db.Update(user); + await db.SaveChangesAsync(ct); + + return Ok(user.Settings); + } + + public class UpdateUserSettingsRequest : PatchRequest + { + public bool? DarkMode { get; init; } + } } \ No newline at end of file diff --git a/Foxnouns.Backend/Database/DatabaseContext.cs b/Foxnouns.Backend/Database/DatabaseContext.cs index 725afb6..26087b8 100644 --- a/Foxnouns.Backend/Database/DatabaseContext.cs +++ b/Foxnouns.Backend/Database/DatabaseContext.cs @@ -63,6 +63,7 @@ public class DatabaseContext : DbContext modelBuilder.Entity().Property(u => u.Names).HasColumnType("jsonb"); modelBuilder.Entity().Property(u => u.Pronouns).HasColumnType("jsonb"); modelBuilder.Entity().Property(u => u.CustomPreferences).HasColumnType("jsonb"); + modelBuilder.Entity().Property(u => u.Settings).HasColumnType("jsonb"); modelBuilder.Entity().Property(m => m.Fields).HasColumnType("jsonb"); modelBuilder.Entity().Property(m => m.Names).HasColumnType("jsonb"); diff --git a/Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.Designer.cs b/Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.Designer.cs new file mode 100644 index 0000000..45b3a53 --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.Designer.cs @@ -0,0 +1,432 @@ +// +using System; +using System.Collections.Generic; +using Foxnouns.Backend.Database; +using Foxnouns.Backend.Database.Models; +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("20240905191709_AddUserSettings")] + partial class AddUserSettings + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Application", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_id"); + + b.Property("ClientSecret") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_secret"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("RedirectUris") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("redirect_uris"); + + b.Property("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("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("AuthType") + .HasColumnType("integer") + .HasColumnName("auth_type"); + + b.Property("FediverseApplicationId") + .HasColumnType("bigint") + .HasColumnName("fediverse_application_id"); + + b.Property("RemoteId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("remote_id"); + + b.Property("RemoteUsername") + .HasColumnType("text") + .HasColumnName("remote_username"); + + b.Property("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("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_id"); + + b.Property("ClientSecret") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_secret"); + + b.Property("Domain") + .IsRequired() + .HasColumnType("text") + .HasColumnName("domain"); + + b.Property("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("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("Avatar") + .HasColumnType("text") + .HasColumnName("avatar"); + + b.Property("Bio") + .HasColumnType("text") + .HasColumnName("bio"); + + b.Property("DisplayName") + .HasColumnType("text") + .HasColumnName("display_name"); + + b.Property>("Fields") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("fields"); + + b.Property("Links") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("links"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property>("Names") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("names"); + + b.Property>("Pronouns") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("pronouns"); + + b.Property("Unlisted") + .HasColumnType("boolean") + .HasColumnName("unlisted"); + + b.Property("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.TemporaryKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Expires") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text") + .HasColumnName("key"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_temporary_keys"); + + b.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_temporary_keys_key"); + + b.ToTable("temporary_keys", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("ApplicationId") + .HasColumnType("bigint") + .HasColumnName("application_id"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hash"); + + b.Property("ManuallyExpired") + .HasColumnType("boolean") + .HasColumnName("manually_expired"); + + b.Property("Scopes") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("scopes"); + + b.Property("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("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("Avatar") + .HasColumnType("text") + .HasColumnName("avatar"); + + b.Property("Bio") + .HasColumnType("text") + .HasColumnName("bio"); + + b.Property>("CustomPreferences") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("custom_preferences"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasColumnName("deleted_by"); + + b.Property("DisplayName") + .HasColumnType("text") + .HasColumnName("display_name"); + + b.Property>("Fields") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("fields"); + + b.Property("LastActive") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_active"); + + b.Property("Links") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("links"); + + b.Property("ListHidden") + .HasColumnType("boolean") + .HasColumnName("list_hidden"); + + b.Property("MemberTitle") + .HasColumnType("text") + .HasColumnName("member_title"); + + b.Property>("Names") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("names"); + + b.Property("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property>("Pronouns") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("pronouns"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("settings"); + + b.Property("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.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.Navigation("AuthMethods"); + + b.Navigation("Members"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.cs b/Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.cs new file mode 100644 index 0000000..49c5bd1 --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.cs @@ -0,0 +1,30 @@ +using Foxnouns.Backend.Database.Models; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Foxnouns.Backend.Database.Migrations +{ + /// + public partial class AddUserSettings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "settings", + table: "users", + type: "jsonb", + nullable: false, + defaultValueSql: "'{}'"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "settings", + table: "users"); + } + } +} diff --git a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs index fc99285..36ca955 100644 --- a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -150,6 +150,11 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("text") .HasColumnName("display_name"); + b.Property>("Fields") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("fields"); + b.Property("Links") .IsRequired() .HasColumnType("text[]") @@ -160,6 +165,16 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("text") .HasColumnName("name"); + b.Property>("Names") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("names"); + + b.Property>("Pronouns") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("pronouns"); + b.Property("Unlisted") .HasColumnType("boolean") .HasColumnName("unlisted"); @@ -269,7 +284,7 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("text") .HasColumnName("bio"); - b.Property>("CustomPreferences") + b.Property>("CustomPreferences") .IsRequired() .HasColumnType("jsonb") .HasColumnName("custom_preferences"); @@ -290,6 +305,11 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("text") .HasColumnName("display_name"); + b.Property>("Fields") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("fields"); + b.Property("LastActive") .HasColumnType("timestamp with time zone") .HasColumnName("last_active"); @@ -307,14 +327,29 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("text") .HasColumnName("member_title"); + b.Property>("Names") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("names"); + b.Property("Password") .HasColumnType("text") .HasColumnName("password"); + b.Property>("Pronouns") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("pronouns"); + b.Property("Role") .HasColumnType("integer") .HasColumnName("role"); + b.Property("Settings") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("settings"); + b.Property("Username") .IsRequired() .HasColumnType("text") @@ -358,72 +393,6 @@ namespace Foxnouns.Backend.Database.Migrations .IsRequired() .HasConstraintName("fk_members_users_user_id"); - b.OwnsOne("System.Collections.Generic.List", "Fields", b1 => - { - b1.Property("MemberId") - .HasColumnType("bigint"); - - b1.Property("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", "Names", b1 => - { - b1.Property("MemberId") - .HasColumnType("bigint"); - - b1.Property("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", "Pronouns", b1 => - { - b1.Property("MemberId") - .HasColumnType("bigint"); - - b1.Property("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"); }); @@ -448,78 +417,6 @@ namespace Foxnouns.Backend.Database.Migrations b.Navigation("User"); }); - modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => - { - b.OwnsOne("Foxnouns.Backend.Database.Models.User.Fields#List", "Fields", b1 => - { - b1.Property("UserId") - .HasColumnType("bigint"); - - b1.Property("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("UserId") - .HasColumnType("bigint"); - - b1.Property("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("UserId") - .HasColumnType("bigint"); - - b1.Property("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"); diff --git a/Foxnouns.Backend/Database/Models/User.cs b/Foxnouns.Backend/Database/Models/User.cs index c152e65..de48944 100644 --- a/Foxnouns.Backend/Database/Models/User.cs +++ b/Foxnouns.Backend/Database/Models/User.cs @@ -25,6 +25,7 @@ public class User : BaseModel public List Members { get; } = []; public List AuthMethods { get; } = []; + public UserSettings Settings { get; set; } = new(); public required Instant LastActive { get; set; } @@ -58,4 +59,9 @@ public enum PreferenceSize Large, Normal, Small, +} + +public class UserSettings +{ + public bool? DarkMode { get; set; } = null; } \ No newline at end of file