Fix bug where users can't be deleted (#648)

* Add username to mod cases if user is deleted

* Add timezone package to docker container

* Remove extra space in migration sql statement

* Changes from self-review
This commit is contained in:
Josh 2023-01-29 22:10:36 -06:00 committed by GitHub
parent 2c2f31ad38
commit 4559d26a54
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 170 additions and 83 deletions

View file

@ -3,7 +3,7 @@
"isRoot": true, "isRoot": true,
"tools": { "tools": {
"dotnet-ef": { "dotnet-ef": {
"version": "7.0.1", "version": "7.0.2",
"commands": [ "commands": [
"dotnet-ef" "dotnet-ef"
] ]

View file

@ -24,7 +24,7 @@ adduser -S lighthouse -G lighthouse -h /lighthouse --uid 1001 && \
mkdir -p /lighthouse/data && \ mkdir -p /lighthouse/data && \
mkdir -p /lighthouse/app && \ mkdir -p /lighthouse/app && \
mkdir -p /lighthouse/temp && \ mkdir -p /lighthouse/temp && \
apk add --no-cache icu-libs su-exec apk add --no-cache icu-libs su-exec tzdata
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false

View file

@ -27,6 +27,7 @@ public class ModerationCaseController : ControllerBase
@case.DismissedAt = DateTime.Now; @case.DismissedAt = DateTime.Now;
@case.DismisserId = user.UserId; @case.DismisserId = user.UserId;
@case.DismisserUsername = user.Username;
@case.Processed = false; @case.Processed = false;

View file

@ -21,8 +21,8 @@ public class NewCasePage : BaseLayout
if (type == null) return this.BadRequest(); if (type == null) return this.BadRequest();
if (affectedId == null) return this.BadRequest(); if (affectedId == null) return this.BadRequest();
this.Type = (CaseType)type; this.Type = type.Value;
this.AffectedId = (int)affectedId; this.AffectedId = affectedId.Value;
return this.Page(); return this.Page();
} }
@ -38,19 +38,19 @@ public class NewCasePage : BaseLayout
reason ??= string.Empty; reason ??= string.Empty;
modNotes ??= string.Empty; modNotes ??= string.Empty;
// this is fucking ugly
// if id is invalid then return bad request // if id is invalid then return bad request
if (!(await ((CaseType)type).IsIdValid((int)affectedId, this.Database))) return this.BadRequest(); if (!await type.Value.IsIdValid((int)affectedId, this.Database)) return this.BadRequest();
ModerationCase @case = new() ModerationCase @case = new()
{ {
Type = (CaseType)type, Type = type.Value,
Reason = reason, Reason = reason,
ModeratorNotes = modNotes, ModeratorNotes = modNotes,
ExpiresAt = expires, ExpiresAt = expires,
CreatedAt = DateTime.Now, CreatedAt = DateTime.Now,
CreatorId = user.UserId, CreatorId = user.UserId,
AffectedId = (int)affectedId, CreatorUsername = user.Username,
AffectedId = affectedId.Value,
}; };
this.Database.Cases.Add(@case); this.Database.Cases.Add(@case);

View file

@ -21,24 +21,44 @@
@if (Model.Dismissed) @if (Model.Dismissed)
{ {
Debug.Assert(Model.Dismisser != null);
Debug.Assert(Model.DismissedAt != null); Debug.Assert(Model.DismissedAt != null);
<h3 class="ui @color header"> @if (Model.Dismisser != null)
This case was dismissed by <a href="/user/@Model.Dismisser.UserId">@Model.Dismisser.Username</a> on @TimeZoneInfo.ConvertTime(Model.DismissedAt.Value, timeZoneInfo).ToString("M/d/yyyy @ h:mm tt").
</h3>
}
else if (Model.Expired && Model.ExpiresAt != null)
{ {
<h3 class="ui @color header"> <h3 class="ui @color header">
This case expired on @TimeZoneInfo.ConvertTime(Model.ExpiresAt.Value, timeZoneInfo).ToString("M/d/yyyy @ h:mm tt"). This case was dismissed by <a href="/user/@Model.Dismisser.UserId">@Model.DismisserUsername</a> on @TimeZoneInfo.ConvertTime(Model.DismissedAt.Value, timeZoneInfo).ToString("M/d/yyyy @ h:mm tt").
</h3>
}
else
{
<h3 class="ui @color header">
This case was dismissed by @Model.DismisserUsername on @TimeZoneInfo.ConvertTime(Model.DismissedAt.Value, timeZoneInfo).ToString("M/d/yyyy @ h:mm tt").
</h3> </h3>
} }
}
else if (Model.Expired)
{
<h3 class="ui @color header">
This case expired on @TimeZoneInfo.ConvertTime(Model.ExpiresAt!.Value, timeZoneInfo).ToString("M/d/yyyy @ h:mm tt").
</h3>
}
@if (Model.Creator != null && Model.Creator.Username.Length != 0)
{
<span> <span>
Case created by <a href="/user/@Model.Creator!.UserId">@Model.Creator.Username</a> Case created by <a href="/user/@Model.Creator.UserId">@Model.Creator.Username</a>
on @TimeZoneInfo.ConvertTime(Model.CreatedAt, timeZoneInfo).ToString("M/d/yyyy @ h:mm tt") on @TimeZoneInfo.ConvertTime(Model.CreatedAt, timeZoneInfo).ToString("M/d/yyyy @ h:mm tt")
</span><br> </span><br>
}
else
{
<span>
Case created by @Model.CreatorUsername
on @TimeZoneInfo.ConvertTime(Model.CreatedAt, timeZoneInfo).ToString("M/d/yyyy @ h:mm tt")
</span><br>
}
@if (Model.Type.AffectsLevel()) @if (Model.Type.AffectsLevel())
{ {

View file

@ -48,6 +48,6 @@ public static class CaseTypeExtensions
if (type.AffectsUser()) return await database.Users.Has(u => u.UserId == affectedId); if (type.AffectsUser()) return await database.Users.Has(u => u.UserId == affectedId);
if (type.AffectsLevel()) return await database.Slots.Has(u => u.SlotId == affectedId); if (type.AffectsLevel()) return await database.Slots.Has(u => u.SlotId == affectedId);
throw new ArgumentOutOfRangeException(); throw new ArgumentOutOfRangeException(nameof(type));
} }
} }

View file

@ -30,14 +30,18 @@ public class ModerationCase
public DateTime? DismissedAt { get; set; } public DateTime? DismissedAt { get; set; }
public bool Dismissed => this.DismissedAt != null; public bool Dismissed => this.DismissedAt != null;
public int? DismisserId { get; set; } public int? DismisserId { get; set; }
public string? DismisserUsername { get; set; }
[ForeignKey(nameof(DismisserId))] [ForeignKey(nameof(DismisserId))]
public User? Dismisser { get; set; } public virtual User? Dismisser { get; set; }
public int CreatorId { get; set; } public int CreatorId { get; set; }
public required string CreatorUsername { get; set; }
[ForeignKey(nameof(CreatorId))] [ForeignKey(nameof(CreatorId))]
public User? Creator { get; set; } public virtual User? Creator { get; set; }
public int AffectedId { get; set; } public int AffectedId { get; set; }
@ -54,24 +58,4 @@ public class ModerationCase
return database.Slots.FirstOrDefaultAsync(u => u.SlotId == this.AffectedId); return database.Slots.FirstOrDefaultAsync(u => u.SlotId == this.AffectedId);
} }
#endregion #endregion
#region Case creators
#region Level
#endregion
#region User
public static ModerationCase NewBanCase(int caseCreator, int userId, string reason, string modNotes, DateTime caseExpires)
=> new()
{
Type = CaseType.UserBan,
Reason = $"Banned for reason '{reason}'\nModeration notes: {modNotes}",
CreatorId = caseCreator,
CreatedAt = DateTime.Now,
ExpiresAt = caseExpires,
AffectedId = userId,
};
#endregion
#endregion
} }

View file

@ -517,6 +517,16 @@ public class Database : DbContext
LastContact? lastContact = await this.LastContacts.FirstOrDefaultAsync(l => l.UserId == user.UserId); LastContact? lastContact = await this.LastContacts.FirstOrDefaultAsync(l => l.UserId == user.UserId);
if (lastContact != null) this.LastContacts.Remove(lastContact); if (lastContact != null) this.LastContacts.Remove(lastContact);
foreach (ModerationCase modCase in await this.Cases
.Where(c => c.CreatorId == user.UserId || c.DismisserId == user.UserId)
.ToListAsync())
{
if(modCase.DismisserId == user.UserId)
modCase.DismisserId = null;
if(modCase.CreatorId == user.UserId)
modCase.CreatorId = await SlotHelper.GetPlaceholderUserId(this);
}
foreach (Slot slot in this.Slots.Where(s => s.CreatorId == user.UserId)) await this.RemoveSlot(slot, false); foreach (Slot slot in this.Slots.Where(s => s.CreatorId == user.UserId)) await this.RemoveSlot(slot, false);
this.HeartedProfiles.RemoveRange(this.HeartedProfiles.Where(h => h.UserId == user.UserId)); this.HeartedProfiles.RemoveRange(this.HeartedProfiles.Where(h => h.UserId == user.UserId));

View file

@ -1,4 +0,0 @@
namespace LBPUnion.ProjectLighthouse.Helpers;
public static class LocalizationHelper
{}

View file

@ -41,6 +41,56 @@ public static class SlotHelper
private static readonly SemaphoreSlim semaphore = new(1, 1); private static readonly SemaphoreSlim semaphore = new(1, 1);
private static async Task<int> GetPlaceholderLocationId(Database database)
{
Location? devLocation = await database.Locations.FirstOrDefaultAsync(l => l.Id == 1);
if (devLocation != null) return devLocation.Id;
await semaphore.WaitAsync(TimeSpan.FromSeconds(5));
try
{
devLocation = new Location
{
Id = 1,
};
database.Locations.Add(devLocation);
return devLocation.Id;
}
finally
{
semaphore.Release();
}
}
public static async Task<int> GetPlaceholderUserId(Database database)
{
int devCreatorId = await database.Users.Where(u => u.Username.Length == 0)
.Select(u => u.UserId)
.FirstOrDefaultAsync();
if (devCreatorId != 0) return devCreatorId;
await semaphore.WaitAsync(TimeSpan.FromSeconds(5));
try
{
User devCreator = new()
{
Username = "",
PermissionLevel = PermissionLevel.Banned,
Biography = "Placeholder author of story levels",
BannedReason = "Banned to not show in users list",
LocationId = await GetPlaceholderLocationId(database),
};
database.Users.Add(devCreator);
await database.SaveChangesAsync();
return devCreator.UserId;
}
finally
{
semaphore.Release();
}
}
public static async Task<int> GetPlaceholderSlotId(Database database, int guid, SlotType slotType) public static async Task<int> GetPlaceholderSlotId(Database database, int guid, SlotType slotType)
{ {
int slotId = await database.Slots.Where(s => s.Type == slotType && s.InternalSlotId == guid).Select(s => s.SlotId).FirstOrDefaultAsync(); int slotId = await database.Slots.Where(s => s.Type == slotType && s.InternalSlotId == guid).Select(s => s.SlotId).FirstOrDefaultAsync();
@ -58,31 +108,7 @@ public static class SlotHelper
if (slotId != 0) return slotId; if (slotId != 0) return slotId;
Location? devLocation = await database.Locations.FirstOrDefaultAsync(l => l.Id == 1); int devCreatorId = await GetPlaceholderUserId(database);
if (devLocation == null)
{
devLocation = new Location
{
Id = 1,
};
database.Locations.Add(devLocation);
}
int devCreatorId = await database.Users.Where(u => u.Username.Length == 0).Select(u => u.UserId).FirstOrDefaultAsync();
if (devCreatorId == 0)
{
User devCreator = new()
{
Username = "",
PermissionLevel = PermissionLevel.Banned,
Biography = "Placeholder author of story levels",
BannedReason = "Banned to not show in users list",
LocationId = devLocation.Id,
};
database.Users.Add(devCreator);
await database.SaveChangesAsync();
devCreatorId = devCreator.UserId;
}
Slot slot = new() Slot slot = new()
{ {
@ -90,7 +116,7 @@ public static class SlotHelper
Description = $"Placeholder for {slotType} type level", Description = $"Placeholder for {slotType} type level",
CreatorId = devCreatorId, CreatorId = devCreatorId,
InternalSlotId = guid, InternalSlotId = guid,
LocationId = devLocation.Id, LocationId = await GetPlaceholderLocationId(database),
Type = slotType, Type = slotType,
}; };
@ -103,5 +129,4 @@ public static class SlotHelper
semaphore.Release(); semaphore.Release();
} }
} }
} }

View file

@ -4,13 +4,13 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.PlayerData; using LBPUnion.ProjectLighthouse.PlayerData;
using Microsoft.EntityFrameworkCore;
using YamlDotNet.Core.Tokens; using YamlDotNet.Core.Tokens;
namespace LBPUnion.ProjectLighthouse.Levels.Categories; namespace LBPUnion.ProjectLighthouse.Levels.Categories;
public class HighestRatedCategory : Category public class HighestRatedCategory : Category
{ {
Random rand = new();
public override string Name { get; set; } = "Highest Rated"; public override string Name { get; set; } = "Highest Rated";
public override string Description { get; set; } = "Community Highest Rated content"; public override string Description { get; set; } = "Community Highest Rated content";
public override string IconHash { get; set; } = "g820603"; public override string IconHash { get; set; } = "g820603";
@ -21,7 +21,7 @@ public class HighestRatedCategory : Category
=> database.Slots.ByGameVersion(GameVersion.LittleBigPlanet3, false, true) => database.Slots.ByGameVersion(GameVersion.LittleBigPlanet3, false, true)
.AsEnumerable() .AsEnumerable()
.OrderByDescending(s => s.Thumbsup) .OrderByDescending(s => s.Thumbsup)
.ThenBy(_ => rand.Next()) .ThenBy(_ => EF.Functions.Random())
.Skip(Math.Max(0, pageStart - 1)) .Skip(Math.Max(0, pageStart - 1))
.Take(Math.Min(pageSize, 20)); .Take(Math.Min(pageSize, 20));
public override int GetTotalSlots(Database database) => database.Slots.Count(s => s.Type == SlotType.User); public override int GetTotalSlots(Database database) => database.Slots.Count(s => s.Type == SlotType.User);

View file

@ -4,23 +4,23 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.PlayerData; using LBPUnion.ProjectLighthouse.PlayerData;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Levels.Categories; namespace LBPUnion.ProjectLighthouse.Levels.Categories;
public class MostHeartedCategory : Category public class MostHeartedCategory : Category
{ {
Random rand = new();
public override string Name { get; set; } = "Most Hearted"; public override string Name { get; set; } = "Most Hearted";
public override string Description { get; set; } = "The Most Hearted Content"; public override string Description { get; set; } = "The Most Hearted Content";
public override string IconHash { get; set; } = "g820607"; public override string IconHash { get; set; } = "g820607";
public override string Endpoint { get; set; } = "mostHearted"; public override string Endpoint { get; set; } = "mostHearted";
public override Slot? GetPreviewSlot(Database database) => database.Slots.Where(s => s.Type == SlotType.User).AsEnumerable().OrderByDescending(s => s.Hearts).FirstOrDefault(); public override Slot? GetPreviewSlot(Database database) => database.Slots.Where(s => s.Type == SlotType.User).AsEnumerable().MaxBy(s => s.Hearts);
public override IEnumerable<Slot> GetSlots public override IEnumerable<Slot> GetSlots
(Database database, int pageStart, int pageSize) (Database database, int pageStart, int pageSize)
=> database.Slots.ByGameVersion(GameVersion.LittleBigPlanet3, false, true) => database.Slots.ByGameVersion(GameVersion.LittleBigPlanet3, false, true)
.AsEnumerable() .AsEnumerable()
.OrderByDescending(s => s.Hearts) .OrderByDescending(s => s.Hearts)
.ThenBy(_ => rand.Next()) .ThenBy(_ => EF.Functions.Random())
.Skip(Math.Max(0, pageStart - 1)) .Skip(Math.Max(0, pageStart - 1))
.Take(Math.Min(pageSize, 20)); .Take(Math.Min(pageSize, 20));
public override int GetTotalSlots(Database database) => database.Slots.Count(s => s.Type == SlotType.User); public override int GetTotalSlots(Database database) => database.Slots.Count(s => s.Type == SlotType.User);

View file

@ -4,24 +4,23 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.PlayerData; using LBPUnion.ProjectLighthouse.PlayerData;
using YamlDotNet.Core.Tokens; using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Levels.Categories; namespace LBPUnion.ProjectLighthouse.Levels.Categories;
public class MostPlayedCategory : Category public class MostPlayedCategory : Category
{ {
Random rand = new();
public override string Name { get; set; } = "Most Played"; public override string Name { get; set; } = "Most Played";
public override string Description { get; set; } = "The most played content"; public override string Description { get; set; } = "The most played content";
public override string IconHash { get; set; } = "g820608"; public override string IconHash { get; set; } = "g820608";
public override string Endpoint { get; set; } = "mostUniquePlays"; public override string Endpoint { get; set; } = "mostUniquePlays";
public override Slot? GetPreviewSlot(Database database) => database.Slots.Where(s => s.Type == SlotType.User).AsEnumerable().OrderByDescending(s => s.PlaysUnique).FirstOrDefault(); public override Slot? GetPreviewSlot(Database database) => database.Slots.Where(s => s.Type == SlotType.User).AsEnumerable().MaxBy(s => s.PlaysUnique);
public override IEnumerable<Slot> GetSlots public override IEnumerable<Slot> GetSlots
(Database database, int pageStart, int pageSize) (Database database, int pageStart, int pageSize)
=> database.Slots.ByGameVersion(GameVersion.LittleBigPlanet3, false, true) => database.Slots.ByGameVersion(GameVersion.LittleBigPlanet3, false, true)
.AsEnumerable() .AsEnumerable()
.OrderByDescending(s => s.PlaysUnique) .OrderByDescending(s => s.PlaysUnique)
.ThenBy(_ => rand.Next()) .ThenBy(_ => EF.Functions.Random())
.Skip(Math.Max(0, pageStart - 1)) .Skip(Math.Max(0, pageStart - 1))
.Take(Math.Min(pageSize, 20)); .Take(Math.Min(pageSize, 20));
public override int GetTotalSlots(Database database) => database.Slots.Count(s => s.Type == SlotType.User); public override int GetTotalSlots(Database database) => database.Slots.Count(s => s.Type == SlotType.User);

View file

@ -0,0 +1,45 @@
using LBPUnion.ProjectLighthouse;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ProjectLighthouse.Migrations
{
[DbContext(typeof(Database))]
[Migration("20230127021453_AddUsernameToCaseTable")]
public partial class AddUsernameToCaseTable : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "CreatorUsername",
table: "Cases",
type: "longtext",
nullable: false,
defaultValue: "")
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<string>(
name: "DismisserUsername",
table: "Cases",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.Sql("UPDATE Cases INNER JOIN Users ON Cases.CreatorId = Users.UserId SET Cases.CreatorUsername = Users.Username WHERE Cases.CreatorUsername = '';");
migrationBuilder.Sql("UPDATE Cases INNER JOIN Users ON Cases.DismisserId = Users.UserId SET Cases.DismisserUsername = Users.Username WHERE Cases.DismisserUsername is NULL;");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CreatorUsername",
table: "Cases");
migrationBuilder.DropColumn(
name: "DismisserUsername",
table: "Cases");
}
}
}

View file

@ -16,7 +16,7 @@ namespace ProjectLighthouse.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "7.0.1") .HasAnnotation("ProductVersion", "7.0.2")
.HasAnnotation("Relational:MaxIdentifierLength", 64); .HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Administration.CompletedMigration", b => modelBuilder.Entity("LBPUnion.ProjectLighthouse.Administration.CompletedMigration", b =>
@ -47,12 +47,19 @@ namespace ProjectLighthouse.Migrations
b.Property<int>("CreatorId") b.Property<int>("CreatorId")
.HasColumnType("int"); .HasColumnType("int");
b.Property<string>("CreatorUsername")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime?>("DismissedAt") b.Property<DateTime?>("DismissedAt")
.HasColumnType("datetime(6)"); .HasColumnType("datetime(6)");
b.Property<int?>("DismisserId") b.Property<int?>("DismisserId")
.HasColumnType("int"); .HasColumnType("int");
b.Property<string>("DismisserUsername")
.HasColumnType("longtext");
b.Property<DateTime?>("ExpiresAt") b.Property<DateTime?>("ExpiresAt")
.HasColumnType("datetime(6)"); .HasColumnType("datetime(6)");