[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.
This commit is contained in:
Laura Hausmann 2024-07-10 02:42:19 +02:00
parent 10cc6232f3
commit 09cda1a89e
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
12 changed files with 533 additions and 320 deletions

View file

@ -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)
{

View file

@ -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();
}

View file

@ -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<AuthResponse> 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 });

View file

@ -1089,49 +1089,16 @@ public class DatabaseContext(DbContextOptions<DatabaseContext> options)
modelBuilder.Entity<UserProfile>(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<DatabaseContext> 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);
});

View file

@ -4452,18 +4452,6 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
.HasColumnType("character varying(32)")
.HasColumnName("userId");
b.Property<bool>("AlwaysMarkNsfw")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasColumnName("alwaysMarkNsfw");
b.Property<bool>("AutoAcceptFollowed")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasColumnName("autoAcceptFollowed");
b.Property<string>("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<bool>("CarefulBot")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasColumnName("carefulBot");
b.Property<string>("ClientData")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("jsonb")
.HasColumnName("clientData")
.HasDefaultValueSql("'{}'::jsonb")
.HasComment("The client-specific data of the User.");
b.Property<string>("Description")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("description")
.HasComment("The description (bio) of the User.");
b.Property<string>("Email")
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("email")
.HasComment("The email address of the User.");
b.Property<List<string>>("EmailNotificationTypes")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("jsonb")
.HasColumnName("emailNotificationTypes")
.HasDefaultValueSql("'[\"follow\", \"receiveFollowRequest\", \"groupInvited\"]'::jsonb");
b.Property<bool>("EmailVerified")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasColumnName("emailVerified");
b.Property<string>("EmailVerifyCode")
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("emailVerifyCode");
b.Property<bool>("EnableWordMute")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasColumnName("enableWordMute");
b.Property<UserProfile.UserProfileFFVisibility>("FFVisibility")
.ValueGeneratedOnAdd()
.HasColumnType("user_profile_ffvisibility_enum")
@ -4534,19 +4478,6 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
.HasColumnName("fields")
.HasDefaultValueSql("'[]'::jsonb");
b.Property<bool>("InjectFeaturedNote")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true)
.HasColumnName("injectFeaturedNote");
b.Property<string>("Integrations")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("jsonb")
.HasColumnName("integrations")
.HasDefaultValueSql("'{}'::jsonb");
b.Property<string>("Lang")
.HasMaxLength(32)
.HasColumnType("character varying(32)")
@ -4579,106 +4510,17 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
.HasColumnName("moderationNote")
.HasDefaultValueSql("''::character varying");
b.Property<List<string>>("MutedInstances")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("jsonb")
.HasColumnName("mutedInstances")
.HasDefaultValueSql("'[]'::jsonb")
.HasComment("List of instances muted by the user.");
b.Property<string>("MutedWords")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("jsonb")
.HasColumnName("mutedWords")
.HasDefaultValueSql("'[]'::jsonb");
b.Property<List<Notification.NotificationType>>("MutingNotificationTypes")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("notification_type_enum[]")
.HasColumnName("mutingNotificationTypes")
.HasDefaultValueSql("'{}'::public.notification_type_enum[]");
b.Property<bool>("NoCrawle")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasColumnName("noCrawle")
.HasComment("Whether reject index by crawler.");
b.Property<string>("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<string>("PinnedPageId")
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("pinnedPageId");
b.Property<bool>("PreventAiLearning")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true)
.HasColumnName("preventAiLearning");
b.Property<bool>("PublicReactions")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasColumnName("publicReactions");
b.Property<bool>("ReceiveAnnouncementEmail")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true)
.HasColumnName("receiveAnnouncementEmail");
b.Property<string>("Room")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("jsonb")
.HasColumnName("room")
.HasDefaultValueSql("'{}'::jsonb")
.HasComment("The room data of the User.");
b.Property<bool>("SecurityKeysAvailable")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasColumnName("securityKeysAvailable");
b.Property<bool>("TwoFactorEnabled")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasColumnName("twoFactorEnabled");
b.Property<string>("TwoFactorSecret")
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("twoFactorSecret");
b.Property<string>("TwoFactorTempSecret")
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("twoFactorTempSecret");
b.Property<string>("Url")
.HasMaxLength(512)
.HasColumnType("character varying(512)")
.HasColumnName("url")
.HasComment("Remote URL of the user.");
b.Property<bool>("UsePasswordLessLogin")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasColumnName("usePasswordLessLogin");
b.Property<string>("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<bool>("AlwaysMarkNsfw")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasColumnName("alwaysMarkNsfw");
b.Property<bool>("AutoAcceptFollowed")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasColumnName("autoAcceptFollowed");
b.Property<Note.NoteVisibility>("DefaultNoteVisibility")
.ValueGeneratedOnAdd()
.HasColumnType("note_visibility_enum")
@ -4783,18 +4635,50 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
.HasDefaultValue(Note.NoteVisibility.Public)
.HasColumnName("defaultRenoteVisibility");
b.Property<string>("Email")
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("email");
b.Property<bool>("EmailVerified")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasColumnName("emailVerified");
b.Property<bool>("FilterInaccessible")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasColumnName("filterInaccessible");
b.Property<string>("Password")
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("password");
b.Property<bool>("PrivateMode")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasColumnName("privateMode");
b.Property<bool>("TwoFactorEnabled")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasColumnName("twoFactorEnabled");
b.Property<string>("TwoFactorSecret")
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("twoFactorSecret");
b.Property<string>("TwoFactorTempSecret")
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("twoFactorTempSecret");
b.HasKey("UserId");
b.ToTable("user_settings");

View file

@ -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
{
/// <inheritdoc />
[DbContext(typeof(DatabaseContext))]
[Migration("20240709231619_RemoveExtraneousUserProfileColumns")]
public partial class RemoveExtraneousUserProfileColumns : Migration
{
/// <inheritdoc />
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");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "carefulBot",
table: "user_profile",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(
name: "clientData",
table: "user_profile",
type: "jsonb",
nullable: false,
defaultValueSql: "'{}'::jsonb",
comment: "The client-specific data of the User.");
migrationBuilder.AddColumn<List<string>>(
name: "emailNotificationTypes",
table: "user_profile",
type: "jsonb",
nullable: false,
defaultValueSql: "'[\"follow\", \"receiveFollowRequest\", \"groupInvited\"]'::jsonb");
migrationBuilder.AddColumn<string>(
name: "emailVerifyCode",
table: "user_profile",
type: "character varying(128)",
maxLength: 128,
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "enableWordMute",
table: "user_profile",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "injectFeaturedNote",
table: "user_profile",
type: "boolean",
nullable: false,
defaultValue: true);
migrationBuilder.AddColumn<string>(
name: "integrations",
table: "user_profile",
type: "jsonb",
nullable: false,
defaultValueSql: "'{}'::jsonb");
migrationBuilder.AddColumn<List<string>>(
name: "mutedInstances",
table: "user_profile",
type: "jsonb",
nullable: false,
defaultValueSql: "'[]'::jsonb",
comment: "List of instances muted by the user.");
migrationBuilder.AddColumn<string>(
name: "mutedWords",
table: "user_profile",
type: "jsonb",
nullable: false,
defaultValueSql: "'[]'::jsonb");
migrationBuilder.AddColumn<List<Notification.NotificationType>>(
name: "mutingNotificationTypes",
table: "user_profile",
type: "notification_type_enum[]",
nullable: false,
defaultValueSql: "'{}'::public.notification_type_enum[]");
migrationBuilder.AddColumn<bool>(
name: "noCrawle",
table: "user_profile",
type: "boolean",
nullable: false,
defaultValue: false,
comment: "Whether reject index by crawler.");
migrationBuilder.AddColumn<bool>(
name: "preventAiLearning",
table: "user_profile",
type: "boolean",
nullable: false,
defaultValue: true);
migrationBuilder.AddColumn<bool>(
name: "publicReactions",
table: "user_profile",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "receiveAnnouncementEmail",
table: "user_profile",
type: "boolean",
nullable: false,
defaultValue: true);
migrationBuilder.AddColumn<string>(
name: "room",
table: "user_profile",
type: "jsonb",
nullable: false,
defaultValueSql: "'{}'::jsonb",
comment: "The room data of the User.");
migrationBuilder.AddColumn<bool>(
name: "securityKeysAvailable",
table: "user_profile",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "usePasswordLessLogin",
table: "user_profile",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.CreateIndex(
name: "IX_user_profile_enableWordMute",
table: "user_profile",
column: "enableWordMute");
}
}
}

View file

@ -0,0 +1,202 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Infrastructure;
#nullable disable
namespace Iceshrimp.Backend.Core.Database.Migrations
{
/// <inheritdoc />
[DbContext(typeof(DatabaseContext))]
[Migration("20240709234348_MoveUserProfilePropsToSettingsStore")]
public partial class MoveUserProfilePropsToSettingsStore : Migration
{
/// <inheritdoc />
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<bool>(name: "alwaysMarkNsfw",
table: "user_settings",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(name: "autoAcceptFollowed",
table: "user_settings",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(name: "email",
table: "user_settings",
type: "character varying(128)",
maxLength: 128,
nullable: true);
migrationBuilder.AddColumn<bool>(name: "emailVerified",
table: "user_settings",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(name: "password",
table: "user_settings",
type: "character varying(128)",
maxLength: 128,
nullable: true);
migrationBuilder.AddColumn<bool>(name: "twoFactorEnabled",
table: "user_settings",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(name: "twoFactorSecret",
table: "user_settings",
type: "character varying(128)",
maxLength: 128,
nullable: true);
migrationBuilder.AddColumn<string>(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");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(name: "alwaysMarkNsfw",
table: "user_profile",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(name: "autoAcceptFollowed",
table: "user_profile",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(name: "email",
table: "user_profile",
type: "character varying(128)",
maxLength: 128,
nullable: true,
comment: "The email address of the User.");
migrationBuilder.AddColumn<bool>(name: "emailVerified",
table: "user_profile",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(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<bool>(name: "twoFactorEnabled",
table: "user_profile",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(name: "twoFactorSecret",
table: "user_profile",
type: "character varying(128)",
maxLength: 128,
nullable: true);
migrationBuilder.AddColumn<string>(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");
}
}
}

View file

@ -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<Notification.NotificationType> MutingNotificationTypes { get; set; } = [];
/// <summary>
/// The email address of the User.
/// </summary>
[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; }
/// <summary>
/// The password hash of the User. It will be null if the origin of the user is local.
/// </summary>
[Column("password")]
[StringLength(128)]
public string? Password { get; set; }
/// <summary>
/// The client-specific data of the User.
/// </summary>
//TODO: refactor this column (it's currently a Dictionary<string, any>, 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; }
/// <summary>
/// [Denormalized]
/// </summary>
@ -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; }
/// <summary>
/// The room data of the User.
/// </summary>
//TODO: refactor this column (it's currently a Dictionary<string, any>, which is terrible)
[Column("room", TypeName = "jsonb")]
public string Room { get; set; } = null!;
//TODO: refactor this column (it's currently a Dictionary<string, any>, 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<string | string[]>, which is terrible)
[Column("mutedWords", TypeName = "jsonb")]
public string MutedWords { get; set; } = null!;
/// <summary>
/// Whether reject index by crawler.
/// </summary>
[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<string> EmailNotificationTypes { get; set; } = null!;
[Column("lang")] [StringLength(32)] public string? Lang { get; set; }
/// <summary>
/// List of instances muted by the user.
/// </summary>
//TODO: refactor this column (this should have been a varchar[])
[Column("mutedInstances", TypeName = "jsonb")]
public List<string> 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<Note.MentionedUser> 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
}
}

View file

@ -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
}

View file

@ -90,16 +90,11 @@ public class SystemUserService(ILogger<SystemUserService> 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;

View file

@ -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;

View file

@ -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