From 09cda1a89e6f44549c250cdbefeddfd1c4bf6542 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Wed, 10 Jul 2024 02:42:19 +0200 Subject: [PATCH] [backend/database] Move user_profile columns that only concern local users to user_settings This commit also removes a bunch of obsolete user_profile columns. --- .../Controllers/Mastodon/AccountController.cs | 4 +- .../Controllers/Web/AdminController.cs | 4 +- .../Controllers/Web/AuthController.cs | 18 +- .../Core/Database/DatabaseContext.cs | 39 +-- .../DatabaseContextModelSnapshot.cs | 204 ++++------------ ...1619_RemoveExtraneousUserProfileColumns.cs | 223 ++++++++++++++++++ ...348_MoveUserProfilePropsToSettingsStore.cs | 202 ++++++++++++++++ .../Core/Database/Tables/UserProfile.cs | 109 +-------- .../Core/Database/Tables/UserSettings.cs | 28 +++ .../Core/Services/SystemUserService.cs | 11 +- .../Core/Services/UserService.cs | 5 +- .../Pages/OAuth/Authorize.cshtml.cs | 6 +- 12 files changed, 533 insertions(+), 320 deletions(-) create mode 100644 Iceshrimp.Backend/Core/Database/Migrations/v2024.1-beta3/20240709231619_RemoveExtraneousUserProfileColumns.cs create mode 100644 Iceshrimp.Backend/Core/Database/Migrations/v2024.1-beta3/20240709234348_MoveUserProfilePropsToSettingsStore.cs diff --git a/Iceshrimp.Backend/Controllers/Mastodon/AccountController.cs b/Iceshrimp.Backend/Controllers/Mastodon/AccountController.cs index e6f1717c..79ad5e5b 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/AccountController.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/AccountController.cs @@ -64,8 +64,6 @@ public class AccountController( user.IsBot = request.IsBot.Value; if (request.IsExplorable.HasValue) user.IsExplorable = request.IsExplorable.Value; - if (request.Source?.Sensitive.HasValue ?? false) - user.UserProfile.AlwaysMarkNsfw = request.Source.Sensitive.Value; if (request.HideCollections.HasValue) user.UserProfile.FFVisibility = request.HideCollections.Value ? UserProfile.UserProfileFFVisibility.Private @@ -79,6 +77,8 @@ public class AccountController( if (request.Source?.Privacy != null) user.UserSettings.DefaultNoteVisibility = StatusEntity.DecodeVisibility(request.Source.Privacy); + if (request.Source?.Sensitive.HasValue ?? false) + user.UserSettings.AlwaysMarkNsfw = request.Source.Sensitive.Value; if (request.Fields?.Where(p => p is { Name: not null, Value: not null }).ToList() is { Count: > 0 } fields) { diff --git a/Iceshrimp.Backend/Controllers/Web/AdminController.cs b/Iceshrimp.Backend/Controllers/Web/AdminController.cs index 1b5d8a65..3036e6f6 100644 --- a/Iceshrimp.Backend/Controllers/Web/AdminController.cs +++ b/Iceshrimp.Backend/Controllers/Web/AdminController.cs @@ -61,13 +61,13 @@ public class AdminController( [ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound)] public async Task ResetPassword(string id, [FromBody] ResetPasswordRequest request) { - var profile = await db.UserProfiles.FirstOrDefaultAsync(p => p.UserId == id && p.UserHost == null) ?? + var settings = await db.UserSettings.FirstOrDefaultAsync(p => p.UserId == id) ?? throw GracefulException.RecordNotFound(); if (request.Password.Length < 8) throw GracefulException.BadRequest("Password must be at least 8 characters long"); - profile.Password = AuthHelpers.HashPassword(request.Password); + settings.Password = AuthHelpers.HashPassword(request.Password); await db.SaveChangesAsync(); } diff --git a/Iceshrimp.Backend/Controllers/Web/AuthController.cs b/Iceshrimp.Backend/Controllers/Web/AuthController.cs index eb4b7185..342b8b6a 100644 --- a/Iceshrimp.Backend/Controllers/Web/AuthController.cs +++ b/Iceshrimp.Backend/Controllers/Web/AuthController.cs @@ -54,10 +54,10 @@ public class AuthController(DatabaseContext db, UserService userSvc, UserRendere p.UsernameLower == request.Username.ToLowerInvariant()); if (user == null) throw GracefulException.Forbidden("Invalid username or password"); - var profile = await db.UserProfiles.FirstOrDefaultAsync(p => p.User == user); - if (profile?.Password == null) + var settings = await db.UserSettings.FirstOrDefaultAsync(p => p.User == user); + if (settings?.Password == null) throw GracefulException.Forbidden("Invalid username or password"); - if (!AuthHelpers.ComparePassword(request.Password, profile.Password)) + if (!AuthHelpers.ComparePassword(request.Password, settings.Password)) throw GracefulException.Forbidden("Invalid username or password"); var session = HttpContext.GetSession(); @@ -67,7 +67,7 @@ public class AuthController(DatabaseContext db, UserService userSvc, UserRendere { Id = IdHelpers.GenerateSlowflakeId(), UserId = user.Id, - Active = !profile.TwoFactorEnabled, + Active = !settings.TwoFactorEnabled, CreatedAt = DateTime.UtcNow, Token = CryptographyHelpers.GenerateRandomString(32) }; @@ -109,15 +109,15 @@ public class AuthController(DatabaseContext db, UserService userSvc, UserRendere Justification = "Argon2 is execution time-heavy by design")] public async Task ChangePassword([FromBody] ChangePasswordRequest request) { - var user = HttpContext.GetUser() ?? throw new GracefulException("HttpContext.GetUser() was null"); - var userProfile = await db.UserProfiles.FirstOrDefaultAsync(p => p.User == user); - if (userProfile is not { Password: not null }) throw new GracefulException("userProfile?.Password was null"); - if (!AuthHelpers.ComparePassword(request.OldPassword, userProfile.Password)) + var user = HttpContext.GetUser() ?? throw new GracefulException("HttpContext.GetUser() was null"); + var settings = await db.UserSettings.FirstOrDefaultAsync(p => p.User == user); + if (settings is not { Password: not null }) throw new GracefulException("settings?.Password was null"); + if (!AuthHelpers.ComparePassword(request.OldPassword, settings.Password)) throw GracefulException.BadRequest("old_password is invalid"); if (request.NewPassword.Length < 8) throw GracefulException.BadRequest("Password must be at least 8 characters long"); - userProfile.Password = AuthHelpers.HashPassword(request.NewPassword); + settings.Password = AuthHelpers.HashPassword(request.NewPassword); await db.SaveChangesAsync(); return await Login(new AuthRequest { Username = user.Username, Password = request.NewPassword }); diff --git a/Iceshrimp.Backend/Core/Database/DatabaseContext.cs b/Iceshrimp.Backend/Core/Database/DatabaseContext.cs index 309f18f2..5e50447d 100644 --- a/Iceshrimp.Backend/Core/Database/DatabaseContext.cs +++ b/Iceshrimp.Backend/Core/Database/DatabaseContext.cs @@ -1089,49 +1089,16 @@ public class DatabaseContext(DbContextOptions options) modelBuilder.Entity(entity => { - entity.Property(e => e.AlwaysMarkNsfw).HasDefaultValue(false); - entity.Property(e => e.AutoAcceptFollowed).HasDefaultValue(false); entity.Property(e => e.Birthday) .IsFixedLength() .HasComment("The birthday (YYYY-MM-DD) of the User."); - entity.Property(e => e.CarefulBot).HasDefaultValue(false); - entity.Property(e => e.ClientData) - .HasDefaultValueSql("'{}'::jsonb") - .HasComment("The client-specific data of the User."); entity.Property(e => e.Description).HasComment("The description (bio) of the User."); - entity.Property(e => e.Email).HasComment("The email address of the User."); - entity.Property(e => e.EmailNotificationTypes) - .HasDefaultValueSql("'[\"follow\", \"receiveFollowRequest\", \"groupInvited\"]'::jsonb"); - entity.Property(e => e.EmailVerified).HasDefaultValue(false); - entity.Property(e => e.EnableWordMute).HasDefaultValue(false); entity.Property(e => e.Fields).HasDefaultValueSql("'[]'::jsonb"); - entity.Property(e => e.InjectFeaturedNote).HasDefaultValue(true); - entity.Property(e => e.Integrations).HasDefaultValueSql("'{}'::jsonb"); entity.Property(e => e.Location).HasComment("The location of the User."); entity.Property(e => e.Mentions).HasDefaultValueSql("'[]'::jsonb"); entity.Property(e => e.ModerationNote).HasDefaultValueSql("''::character varying"); - entity.Property(e => e.MutedInstances) - .HasDefaultValueSql("'[]'::jsonb") - .HasComment("List of instances muted by the user."); - entity.Property(e => e.MutedWords).HasDefaultValueSql("'[]'::jsonb"); - entity.Property(e => e.NoCrawle) - .HasDefaultValue(false) - .HasComment("Whether reject index by crawler."); - entity.Property(e => e.Password) - .HasComment("The password hash of the User. It will be null if the origin of the user is local."); - entity.Property(e => e.PreventAiLearning).HasDefaultValue(true); - entity.Property(e => e.PublicReactions).HasDefaultValue(false); - entity.Property(e => e.ReceiveAnnouncementEmail).HasDefaultValue(true); - entity.Property(e => e.Room) - .HasDefaultValueSql("'{}'::jsonb") - .HasComment("The room data of the User."); - entity.Property(e => e.SecurityKeysAvailable).HasDefaultValue(false); - entity.Property(e => e.TwoFactorEnabled).HasDefaultValue(false); entity.Property(e => e.Url).HasComment("Remote URL of the user."); - entity.Property(e => e.UsePasswordLessLogin).HasDefaultValue(false); entity.Property(e => e.UserHost).HasComment("[Denormalized]"); - entity.Property(e => e.MutingNotificationTypes) - .HasDefaultValueSql("'{}'::public.notification_type_enum[]"); entity.Property(e => e.FFVisibility) .HasDefaultValue(UserProfile.UserProfileFFVisibility.Public); entity.Property(e => e.MentionsResolved).HasDefaultValue(false); @@ -1172,6 +1139,12 @@ public class DatabaseContext(DbContextOptions options) entity.Property(e => e.FilterInaccessible).HasDefaultValue(false); entity.Property(e => e.DefaultNoteVisibility).HasDefaultValue(Note.NoteVisibility.Public); entity.Property(e => e.DefaultRenoteVisibility).HasDefaultValue(Note.NoteVisibility.Public); + entity.Property(e => e.AlwaysMarkNsfw).HasDefaultValue(false); + entity.Property(e => e.AutoAcceptFollowed).HasDefaultValue(false); + entity.Property(e => e.Email); + entity.Property(e => e.EmailVerified).HasDefaultValue(false); + entity.Property(e => e.Password); + entity.Property(e => e.TwoFactorEnabled).HasDefaultValue(false); entity.HasOne(e => e.User).WithOne(e => e.UserSettings); }); diff --git a/Iceshrimp.Backend/Core/Database/Migrations/DatabaseContextModelSnapshot.cs b/Iceshrimp.Backend/Core/Database/Migrations/DatabaseContextModelSnapshot.cs index 1a3d3ebc..1b18e6f8 100644 --- a/Iceshrimp.Backend/Core/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Iceshrimp.Backend/Core/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -4452,18 +4452,6 @@ namespace Iceshrimp.Backend.Core.Database.Migrations .HasColumnType("character varying(32)") .HasColumnName("userId"); - b.Property("AlwaysMarkNsfw") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("alwaysMarkNsfw"); - - b.Property("AutoAcceptFollowed") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("autoAcceptFollowed"); - b.Property("Birthday") .HasMaxLength(10) .HasColumnType("character(10)") @@ -4471,56 +4459,12 @@ namespace Iceshrimp.Backend.Core.Database.Migrations .IsFixedLength() .HasComment("The birthday (YYYY-MM-DD) of the User."); - b.Property("CarefulBot") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("carefulBot"); - - b.Property("ClientData") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("jsonb") - .HasColumnName("clientData") - .HasDefaultValueSql("'{}'::jsonb") - .HasComment("The client-specific data of the User."); - b.Property("Description") .HasMaxLength(2048) .HasColumnType("character varying(2048)") .HasColumnName("description") .HasComment("The description (bio) of the User."); - b.Property("Email") - .HasMaxLength(128) - .HasColumnType("character varying(128)") - .HasColumnName("email") - .HasComment("The email address of the User."); - - b.Property>("EmailNotificationTypes") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("jsonb") - .HasColumnName("emailNotificationTypes") - .HasDefaultValueSql("'[\"follow\", \"receiveFollowRequest\", \"groupInvited\"]'::jsonb"); - - b.Property("EmailVerified") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("emailVerified"); - - b.Property("EmailVerifyCode") - .HasMaxLength(128) - .HasColumnType("character varying(128)") - .HasColumnName("emailVerifyCode"); - - b.Property("EnableWordMute") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("enableWordMute"); - b.Property("FFVisibility") .ValueGeneratedOnAdd() .HasColumnType("user_profile_ffvisibility_enum") @@ -4534,19 +4478,6 @@ namespace Iceshrimp.Backend.Core.Database.Migrations .HasColumnName("fields") .HasDefaultValueSql("'[]'::jsonb"); - b.Property("InjectFeaturedNote") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true) - .HasColumnName("injectFeaturedNote"); - - b.Property("Integrations") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("jsonb") - .HasColumnName("integrations") - .HasDefaultValueSql("'{}'::jsonb"); - b.Property("Lang") .HasMaxLength(32) .HasColumnType("character varying(32)") @@ -4579,106 +4510,17 @@ namespace Iceshrimp.Backend.Core.Database.Migrations .HasColumnName("moderationNote") .HasDefaultValueSql("''::character varying"); - b.Property>("MutedInstances") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("jsonb") - .HasColumnName("mutedInstances") - .HasDefaultValueSql("'[]'::jsonb") - .HasComment("List of instances muted by the user."); - - b.Property("MutedWords") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("jsonb") - .HasColumnName("mutedWords") - .HasDefaultValueSql("'[]'::jsonb"); - - b.Property>("MutingNotificationTypes") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("notification_type_enum[]") - .HasColumnName("mutingNotificationTypes") - .HasDefaultValueSql("'{}'::public.notification_type_enum[]"); - - b.Property("NoCrawle") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("noCrawle") - .HasComment("Whether reject index by crawler."); - - b.Property("Password") - .HasMaxLength(128) - .HasColumnType("character varying(128)") - .HasColumnName("password") - .HasComment("The password hash of the User. It will be null if the origin of the user is local."); - b.Property("PinnedPageId") .HasMaxLength(32) .HasColumnType("character varying(32)") .HasColumnName("pinnedPageId"); - b.Property("PreventAiLearning") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true) - .HasColumnName("preventAiLearning"); - - b.Property("PublicReactions") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("publicReactions"); - - b.Property("ReceiveAnnouncementEmail") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true) - .HasColumnName("receiveAnnouncementEmail"); - - b.Property("Room") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("jsonb") - .HasColumnName("room") - .HasDefaultValueSql("'{}'::jsonb") - .HasComment("The room data of the User."); - - b.Property("SecurityKeysAvailable") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("securityKeysAvailable"); - - b.Property("TwoFactorEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("twoFactorEnabled"); - - b.Property("TwoFactorSecret") - .HasMaxLength(128) - .HasColumnType("character varying(128)") - .HasColumnName("twoFactorSecret"); - - b.Property("TwoFactorTempSecret") - .HasMaxLength(128) - .HasColumnType("character varying(128)") - .HasColumnName("twoFactorTempSecret"); - b.Property("Url") .HasMaxLength(512) .HasColumnType("character varying(512)") .HasColumnName("url") .HasComment("Remote URL of the user."); - b.Property("UsePasswordLessLogin") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("usePasswordLessLogin"); - b.Property("UserHost") .HasMaxLength(512) .HasColumnType("character varying(512)") @@ -4687,8 +4529,6 @@ namespace Iceshrimp.Backend.Core.Database.Migrations b.HasKey("UserId"); - b.HasIndex("EnableWordMute"); - b.HasIndex("PinnedPageId") .IsUnique(); @@ -4771,6 +4611,18 @@ namespace Iceshrimp.Backend.Core.Database.Migrations .HasColumnType("character varying(32)") .HasColumnName("userId"); + b.Property("AlwaysMarkNsfw") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("alwaysMarkNsfw"); + + b.Property("AutoAcceptFollowed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("autoAcceptFollowed"); + b.Property("DefaultNoteVisibility") .ValueGeneratedOnAdd() .HasColumnType("note_visibility_enum") @@ -4783,18 +4635,50 @@ namespace Iceshrimp.Backend.Core.Database.Migrations .HasDefaultValue(Note.NoteVisibility.Public) .HasColumnName("defaultRenoteVisibility"); + b.Property("Email") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("email"); + + b.Property("EmailVerified") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("emailVerified"); + b.Property("FilterInaccessible") .ValueGeneratedOnAdd() .HasColumnType("boolean") .HasDefaultValue(false) .HasColumnName("filterInaccessible"); + b.Property("Password") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("password"); + b.Property("PrivateMode") .ValueGeneratedOnAdd() .HasColumnType("boolean") .HasDefaultValue(false) .HasColumnName("privateMode"); + b.Property("TwoFactorEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("twoFactorEnabled"); + + b.Property("TwoFactorSecret") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("twoFactorSecret"); + + b.Property("TwoFactorTempSecret") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("twoFactorTempSecret"); + b.HasKey("UserId"); b.ToTable("user_settings"); diff --git a/Iceshrimp.Backend/Core/Database/Migrations/v2024.1-beta3/20240709231619_RemoveExtraneousUserProfileColumns.cs b/Iceshrimp.Backend/Core/Database/Migrations/v2024.1-beta3/20240709231619_RemoveExtraneousUserProfileColumns.cs new file mode 100644 index 00000000..39856a52 --- /dev/null +++ b/Iceshrimp.Backend/Core/Database/Migrations/v2024.1-beta3/20240709231619_RemoveExtraneousUserProfileColumns.cs @@ -0,0 +1,223 @@ +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Iceshrimp.Backend.Core.Database.Tables; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Iceshrimp.Backend.Core.Database.Migrations +{ + /// + [DbContext(typeof(DatabaseContext))] + [Migration("20240709231619_RemoveExtraneousUserProfileColumns")] + public partial class RemoveExtraneousUserProfileColumns : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_user_profile_enableWordMute", + table: "user_profile"); + + migrationBuilder.DropColumn( + name: "carefulBot", + table: "user_profile"); + + migrationBuilder.DropColumn( + name: "clientData", + table: "user_profile"); + + migrationBuilder.DropColumn( + name: "emailNotificationTypes", + table: "user_profile"); + + migrationBuilder.DropColumn( + name: "emailVerifyCode", + table: "user_profile"); + + migrationBuilder.DropColumn( + name: "enableWordMute", + table: "user_profile"); + + migrationBuilder.DropColumn( + name: "injectFeaturedNote", + table: "user_profile"); + + migrationBuilder.DropColumn( + name: "integrations", + table: "user_profile"); + + migrationBuilder.DropColumn( + name: "mutedInstances", + table: "user_profile"); + + migrationBuilder.DropColumn( + name: "mutedWords", + table: "user_profile"); + + migrationBuilder.DropColumn( + name: "mutingNotificationTypes", + table: "user_profile"); + + migrationBuilder.DropColumn( + name: "noCrawle", + table: "user_profile"); + + migrationBuilder.DropColumn( + name: "preventAiLearning", + table: "user_profile"); + + migrationBuilder.DropColumn( + name: "publicReactions", + table: "user_profile"); + + migrationBuilder.DropColumn( + name: "receiveAnnouncementEmail", + table: "user_profile"); + + migrationBuilder.DropColumn( + name: "room", + table: "user_profile"); + + migrationBuilder.DropColumn( + name: "securityKeysAvailable", + table: "user_profile"); + + migrationBuilder.DropColumn( + name: "usePasswordLessLogin", + table: "user_profile"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "carefulBot", + table: "user_profile", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "clientData", + table: "user_profile", + type: "jsonb", + nullable: false, + defaultValueSql: "'{}'::jsonb", + comment: "The client-specific data of the User."); + + migrationBuilder.AddColumn>( + name: "emailNotificationTypes", + table: "user_profile", + type: "jsonb", + nullable: false, + defaultValueSql: "'[\"follow\", \"receiveFollowRequest\", \"groupInvited\"]'::jsonb"); + + migrationBuilder.AddColumn( + name: "emailVerifyCode", + table: "user_profile", + type: "character varying(128)", + maxLength: 128, + nullable: true); + + migrationBuilder.AddColumn( + name: "enableWordMute", + table: "user_profile", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "injectFeaturedNote", + table: "user_profile", + type: "boolean", + nullable: false, + defaultValue: true); + + migrationBuilder.AddColumn( + name: "integrations", + table: "user_profile", + type: "jsonb", + nullable: false, + defaultValueSql: "'{}'::jsonb"); + + migrationBuilder.AddColumn>( + name: "mutedInstances", + table: "user_profile", + type: "jsonb", + nullable: false, + defaultValueSql: "'[]'::jsonb", + comment: "List of instances muted by the user."); + + migrationBuilder.AddColumn( + name: "mutedWords", + table: "user_profile", + type: "jsonb", + nullable: false, + defaultValueSql: "'[]'::jsonb"); + + migrationBuilder.AddColumn>( + name: "mutingNotificationTypes", + table: "user_profile", + type: "notification_type_enum[]", + nullable: false, + defaultValueSql: "'{}'::public.notification_type_enum[]"); + + migrationBuilder.AddColumn( + name: "noCrawle", + table: "user_profile", + type: "boolean", + nullable: false, + defaultValue: false, + comment: "Whether reject index by crawler."); + + migrationBuilder.AddColumn( + name: "preventAiLearning", + table: "user_profile", + type: "boolean", + nullable: false, + defaultValue: true); + + migrationBuilder.AddColumn( + name: "publicReactions", + table: "user_profile", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "receiveAnnouncementEmail", + table: "user_profile", + type: "boolean", + nullable: false, + defaultValue: true); + + migrationBuilder.AddColumn( + name: "room", + table: "user_profile", + type: "jsonb", + nullable: false, + defaultValueSql: "'{}'::jsonb", + comment: "The room data of the User."); + + migrationBuilder.AddColumn( + name: "securityKeysAvailable", + table: "user_profile", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "usePasswordLessLogin", + table: "user_profile", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.CreateIndex( + name: "IX_user_profile_enableWordMute", + table: "user_profile", + column: "enableWordMute"); + } + } +} diff --git a/Iceshrimp.Backend/Core/Database/Migrations/v2024.1-beta3/20240709234348_MoveUserProfilePropsToSettingsStore.cs b/Iceshrimp.Backend/Core/Database/Migrations/v2024.1-beta3/20240709234348_MoveUserProfilePropsToSettingsStore.cs new file mode 100644 index 00000000..adf2a1c9 --- /dev/null +++ b/Iceshrimp.Backend/Core/Database/Migrations/v2024.1-beta3/20240709234348_MoveUserProfilePropsToSettingsStore.cs @@ -0,0 +1,202 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Infrastructure; + +#nullable disable + +namespace Iceshrimp.Backend.Core.Database.Migrations +{ + /// + [DbContext(typeof(DatabaseContext))] + [Migration("20240709234348_MoveUserProfilePropsToSettingsStore")] + public partial class MoveUserProfilePropsToSettingsStore : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // First, create user_settings entries for all local users that don't have one yet + migrationBuilder.Sql($@"INSERT INTO ""user_settings"" (""userId"") (SELECT ""id"" FROM ""user"" WHERE ""host"" IS NULL) ON CONFLICT DO NOTHING;"); + + // Now, create the new columns + migrationBuilder.AddColumn(name: "alwaysMarkNsfw", + table: "user_settings", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn(name: "autoAcceptFollowed", + table: "user_settings", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn(name: "email", + table: "user_settings", + type: "character varying(128)", + maxLength: 128, + nullable: true); + + migrationBuilder.AddColumn(name: "emailVerified", + table: "user_settings", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn(name: "password", + table: "user_settings", + type: "character varying(128)", + maxLength: 128, + nullable: true); + + migrationBuilder.AddColumn(name: "twoFactorEnabled", + table: "user_settings", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn(name: "twoFactorSecret", + table: "user_settings", + type: "character varying(128)", + maxLength: 128, + nullable: true); + + migrationBuilder.AddColumn(name: "twoFactorTempSecret", + table: "user_settings", + type: "character varying(128)", + maxLength: 128, + nullable: true); + + // Then, migrate the settings + migrationBuilder.Sql(""" + UPDATE "user_settings" s + SET "alwaysMarkNsfw" = p."alwaysMarkNsfw", + "autoAcceptFollowed" = p."autoAcceptFollowed", + "email" = p."email", + "emailVerified" = p."emailVerified", + "twoFactorTempSecret" = p."twoFactorTempSecret", + "twoFactorSecret" = p."twoFactorSecret", + "twoFactorEnabled" = p."twoFactorEnabled", + "password" = p."password" + FROM "user_profile" p + WHERE p."userId" = s."userId"; + """); + + // Finally, drop the old columns + migrationBuilder.DropColumn(name: "alwaysMarkNsfw", + table: "user_profile"); + + migrationBuilder.DropColumn(name: "autoAcceptFollowed", + table: "user_profile"); + + migrationBuilder.DropColumn(name: "email", + table: "user_profile"); + + migrationBuilder.DropColumn(name: "emailVerified", + table: "user_profile"); + + migrationBuilder.DropColumn(name: "password", + table: "user_profile"); + + migrationBuilder.DropColumn(name: "twoFactorEnabled", + table: "user_profile"); + + migrationBuilder.DropColumn(name: "twoFactorSecret", + table: "user_profile"); + + migrationBuilder.DropColumn(name: "twoFactorTempSecret", + table: "user_profile"); + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn(name: "alwaysMarkNsfw", + table: "user_profile", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn(name: "autoAcceptFollowed", + table: "user_profile", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn(name: "email", + table: "user_profile", + type: "character varying(128)", + maxLength: 128, + nullable: true, + comment: "The email address of the User."); + + migrationBuilder.AddColumn(name: "emailVerified", + table: "user_profile", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn(name: "password", + table: "user_profile", + type: "character varying(128)", + maxLength: 128, + nullable: true, + comment: "The password hash of the User. It will be null if the origin of the user is local."); + + migrationBuilder.AddColumn(name: "twoFactorEnabled", + table: "user_profile", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn(name: "twoFactorSecret", + table: "user_profile", + type: "character varying(128)", + maxLength: 128, + nullable: true); + + migrationBuilder.AddColumn(name: "twoFactorTempSecret", + table: "user_profile", + type: "character varying(128)", + maxLength: 128, + nullable: true); + + migrationBuilder.Sql(""" + UPDATE "user_profile" p + SET "alwaysMarkNsfw" = s."alwaysMarkNsfw", + "autoAcceptFollowed" = s."autoAcceptFollowed", + "email" = s."email", + "emailVerified" = s."emailVerified", + "twoFactorTempSecret" = s."twoFactorTempSecret", + "twoFactorSecret" = s."twoFactorSecret", + "twoFactorEnabled" = s."twoFactorEnabled", + "password" = s."password" + FROM "user_settings" s + WHERE s."userId" = p."userId"; + """); + + migrationBuilder.DropColumn(name: "alwaysMarkNsfw", + table: "user_settings"); + + migrationBuilder.DropColumn(name: "autoAcceptFollowed", + table: "user_settings"); + + migrationBuilder.DropColumn(name: "email", + table: "user_settings"); + + migrationBuilder.DropColumn(name: "emailVerified", + table: "user_settings"); + + migrationBuilder.DropColumn(name: "password", + table: "user_settings"); + + migrationBuilder.DropColumn(name: "twoFactorEnabled", + table: "user_settings"); + + migrationBuilder.DropColumn(name: "twoFactorSecret", + table: "user_settings"); + + migrationBuilder.DropColumn(name: "twoFactorTempSecret", + table: "user_settings"); + } + } +} diff --git a/Iceshrimp.Backend/Core/Database/Tables/UserProfile.cs b/Iceshrimp.Backend/Core/Database/Tables/UserProfile.cs index f9fe269c..8a12a579 100644 --- a/Iceshrimp.Backend/Core/Database/Tables/UserProfile.cs +++ b/Iceshrimp.Backend/Core/Database/Tables/UserProfile.cs @@ -7,19 +7,10 @@ using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; namespace Iceshrimp.Backend.Core.Database.Tables; [Table("user_profile")] -[Index(nameof(EnableWordMute))] [Index(nameof(UserHost))] [Index(nameof(PinnedPageId), IsUnique = true)] public class UserProfile { - [PgName("user_profile_ffvisibility_enum")] - public enum UserProfileFFVisibility - { - [PgName("public")] Public, - [PgName("followers")] Followers, - [PgName("private")] Private - } - [Column("mentionsResolved")] public bool MentionsResolved; [Key] @@ -59,52 +50,6 @@ public class UserProfile [Column("ffVisibility")] public UserProfileFFVisibility FFVisibility { get; set; } - [Column("mutingNotificationTypes")] - public List MutingNotificationTypes { get; set; } = []; - - /// - /// The email address of the User. - /// - [Column("email")] - [StringLength(128)] - public string? Email { get; set; } - - [Column("emailVerifyCode")] - [StringLength(128)] - public string? EmailVerifyCode { get; set; } - - [Column("emailVerified")] public bool EmailVerified { get; set; } - - [Column("twoFactorTempSecret")] - [StringLength(128)] - public string? TwoFactorTempSecret { get; set; } - - [Column("twoFactorSecret")] - [StringLength(128)] - public string? TwoFactorSecret { get; set; } - - [Column("twoFactorEnabled")] public bool TwoFactorEnabled { get; set; } - - /// - /// The password hash of the User. It will be null if the origin of the user is local. - /// - [Column("password")] - [StringLength(128)] - public string? Password { get; set; } - - /// - /// The client-specific data of the User. - /// - //TODO: refactor this column (it's currently a Dictionary, which is terrible) - [Column("clientData", TypeName = "jsonb")] - public string ClientData { get; set; } = null!; - - [Column("autoAcceptFollowed")] public bool AutoAcceptFollowed { get; set; } - - [Column("alwaysMarkNsfw")] public bool AlwaysMarkNsfw { get; set; } - - [Column("carefulBot")] public bool CarefulBot { get; set; } - /// /// [Denormalized] /// @@ -112,62 +57,16 @@ public class UserProfile [StringLength(512)] public string? UserHost { get; set; } - [Column("securityKeysAvailable")] public bool SecurityKeysAvailable { get; set; } - - [Column("usePasswordLessLogin")] public bool UsePasswordLessLogin { get; set; } - [Column("pinnedPageId")] [StringLength(32)] public string? PinnedPageId { get; set; } - /// - /// The room data of the User. - /// - //TODO: refactor this column (it's currently a Dictionary, which is terrible) - [Column("room", TypeName = "jsonb")] - public string Room { get; set; } = null!; - - //TODO: refactor this column (it's currently a Dictionary, which is terrible) - [Column("integrations", TypeName = "jsonb")] - public string Integrations { get; set; } = null!; - - [Column("injectFeaturedNote")] public bool InjectFeaturedNote { get; set; } - - [Column("enableWordMute")] public bool EnableWordMute { get; set; } - - //TODO: refactor this column (it's currently a List, which is terrible) - [Column("mutedWords", TypeName = "jsonb")] - public string MutedWords { get; set; } = null!; - - /// - /// Whether reject index by crawler. - /// - [Column("noCrawle")] - public bool NoCrawle { get; set; } - - [Column("receiveAnnouncementEmail")] public bool ReceiveAnnouncementEmail { get; set; } - - //TODO: refactor this column (this should have been NotificationTypeEnum[]) - [Column("emailNotificationTypes", TypeName = "jsonb")] - public List EmailNotificationTypes { get; set; } = null!; - [Column("lang")] [StringLength(32)] public string? Lang { get; set; } - /// - /// List of instances muted by the user. - /// - //TODO: refactor this column (this should have been a varchar[]) - [Column("mutedInstances", TypeName = "jsonb")] - public List MutedInstances { get; set; } = null!; - - [Column("publicReactions")] public bool PublicReactions { get; set; } - [Column("moderationNote")] [StringLength(8192)] public string ModerationNote { get; set; } = null!; - [Column("preventAiLearning")] public bool PreventAiLearning { get; set; } - [Column("mentions", TypeName = "jsonb")] public List Mentions { get; set; } = null!; @@ -185,4 +84,12 @@ public class UserProfile [J("value")] public required string Value { get; set; } [J("verified")] public bool? IsVerified { get; set; } } + + [PgName("user_profile_ffvisibility_enum")] + public enum UserProfileFFVisibility + { + [PgName("public")] Public = 0, + [PgName("followers")] Followers = 1, + [PgName("private")] Private = 2 + } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Database/Tables/UserSettings.cs b/Iceshrimp.Backend/Core/Database/Tables/UserSettings.cs index c571447c..8dd17174 100644 --- a/Iceshrimp.Backend/Core/Database/Tables/UserSettings.cs +++ b/Iceshrimp.Backend/Core/Database/Tables/UserSettings.cs @@ -19,4 +19,32 @@ public class UserSettings [Column("defaultRenoteVisibility")] public Note.NoteVisibility DefaultRenoteVisibility { get; set; } [Column("privateMode")] public bool PrivateMode { get; set; } [Column("filterInaccessible")] public bool FilterInaccessible { get; set; } + [Column("autoAcceptFollowed")] public bool AutoAcceptFollowed { get; set; } + [Column("alwaysMarkNsfw")] public bool AlwaysMarkNsfw { get; set; } + + // @formatter:off + + [Column("email")] + [StringLength(128)] + public string? Email { get; set; } + + [Column("emailVerified")] + public bool EmailVerified { get; set; } + + [Column("twoFactorTempSecret")] + [StringLength(128)] + public string? TwoFactorTempSecret { get; set; } + + [Column("twoFactorSecret")] + [StringLength(128)] + public string? TwoFactorSecret { get; set; } + + [Column("twoFactorEnabled")] + public bool TwoFactorEnabled { get; set; } + + [Column("password")] + [StringLength(128)] + public string? Password { get; set; } + + // @formatter:on } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Services/SystemUserService.cs b/Iceshrimp.Backend/Core/Services/SystemUserService.cs index 40d29f54..e9e9f723 100644 --- a/Iceshrimp.Backend/Core/Services/SystemUserService.cs +++ b/Iceshrimp.Backend/Core/Services/SystemUserService.cs @@ -90,16 +90,11 @@ public class SystemUserService(ILogger logger, DatabaseContex PublicKey = keypair.ExportSubjectPublicKeyInfoPem() }; - var userProfile = new UserProfile - { - UserId = user.Id, - AutoAcceptFollowed = false, - Password = null - }; - + var userProfile = new UserProfile { UserId = user.Id }; + var userSettings = new UserSettings { UserId = user.Id, Password = null }; var usedUsername = new UsedUsername { CreatedAt = DateTime.UtcNow, Username = username.ToLowerInvariant() }; - await db.AddRangeAsync(user, userKeypair, userProfile, usedUsername); + await db.AddRangeAsync(user, userKeypair, userProfile, userSettings, usedUsername); await db.SaveChangesAsync(); return user; diff --git a/Iceshrimp.Backend/Core/Services/UserService.cs b/Iceshrimp.Backend/Core/Services/UserService.cs index f7b42394..1f6815f6 100644 --- a/Iceshrimp.Backend/Core/Services/UserService.cs +++ b/Iceshrimp.Backend/Core/Services/UserService.cs @@ -399,7 +399,8 @@ public class UserService( PublicKey = keypair.ExportSubjectPublicKeyInfoPem() }; - var userProfile = new UserProfile { UserId = user.Id, Password = AuthHelpers.HashPassword(password) }; + var userProfile = new UserProfile { UserId = user.Id }; + var userSettings = new UserSettings { UserId = user.Id, Password = AuthHelpers.HashPassword(password) }; var usedUsername = new UsedUsername { CreatedAt = DateTime.UtcNow, Username = username.ToLowerInvariant() }; @@ -411,7 +412,7 @@ public class UserService( db.Remove(ticket); } - await db.AddRangeAsync(user, userKeypair, userProfile, usedUsername); + await db.AddRangeAsync(user, userKeypair, userProfile, userSettings, usedUsername); await db.SaveChangesAsync(); return user; diff --git a/Iceshrimp.Backend/Pages/OAuth/Authorize.cshtml.cs b/Iceshrimp.Backend/Pages/OAuth/Authorize.cshtml.cs index 8a6c78c1..dbc25a3c 100644 --- a/Iceshrimp.Backend/Pages/OAuth/Authorize.cshtml.cs +++ b/Iceshrimp.Backend/Pages/OAuth/Authorize.cshtml.cs @@ -55,10 +55,10 @@ public class AuthorizeModel(DatabaseContext db) : PageModel p.UsernameLower == username.ToLowerInvariant()); if (user == null) throw GracefulException.Forbidden("Invalid username or password"); - var userProfile = await db.UserProfiles.FirstOrDefaultAsync(p => p.User == user); - if (userProfile?.Password == null) + var userSettings = await db.UserSettings.FirstOrDefaultAsync(p => p.User == user); + if (userSettings?.Password == null) throw GracefulException.Forbidden("Invalid username or password"); - if (AuthHelpers.ComparePassword(password, userProfile.Password) == false) + if (AuthHelpers.ComparePassword(password, userSettings.Password) == false) throw GracefulException.Forbidden("Invalid username or password"); var token = new OauthToken