From fa3c1ccaa7a924b28eb230b590247848f926bd83 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 5 Sep 2024 22:17:10 +0200 Subject: [PATCH] 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