diff --git a/.idea/.idea.ProjectLighthouse/.idea/indexLayout.xml b/.idea/.idea.ProjectLighthouse/.idea/indexLayout.xml index 46037895..e9bcd75e 100644 --- a/.idea/.idea.ProjectLighthouse/.idea/indexLayout.xml +++ b/.idea/.idea.ProjectLighthouse/.idea/indexLayout.xml @@ -3,11 +3,13 @@ + .config/dotnet-tools.json .github .gitignore .idea CONTRIBUTING.md DatabaseMigrations + LICENSE ProjectLighthouse.sln.DotSettings ProjectLighthouse.sln.DotSettings.user README.md diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/LoginController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/LoginController.cs index 11d6834e..a737bda4 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/LoginController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/LoginController.cs @@ -130,7 +130,7 @@ public class LoginController : ControllerBase new LoginResult { AuthTicket = "MM_AUTH=" + token.UserToken, - LbpEnvVer = ServerStatics.ServerName, + ServerBrand = VersionHelper.FullVersion, }.Serialize() ); } diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs index a0eed01f..8c7a2eb7 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs @@ -86,9 +86,9 @@ public class PublishController : ControllerBase if (slot.Location == null) return this.BadRequest(); - if (slot.Description.Length > 200) return this.BadRequest(); + if (slot.Description.Length > 500) return this.BadRequest(); - if (slot.Name.Length > 100) return this.BadRequest(); + if (slot.Name.Length > 64) return this.BadRequest(); if (slot.Resources.Any(resource => !FileHelper.ResourceExists(resource))) { diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs index be33b216..eb6cb62d 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs @@ -96,7 +96,7 @@ public class ReviewController : ControllerBase Review? newReview = await this.getReviewFromBody(); if (newReview == null) return this.BadRequest(); - if (newReview.Text.Length > 100) return this.BadRequest(); + if (newReview.Text.Length > 512) return this.BadRequest(); Review? review = await this.database.Reviews.FirstOrDefaultAsync(r => r.SlotId == slotId && r.ReviewerId == user.UserId); diff --git a/ProjectLighthouse.Servers.Website/Controllers/Debug/RoomVisualizerController.cs b/ProjectLighthouse.Servers.Website/Controllers/Debug/RoomVisualizerController.cs index a2bff1e0..7039a9bf 100644 --- a/ProjectLighthouse.Servers.Website/Controllers/Debug/RoomVisualizerController.cs +++ b/ProjectLighthouse.Servers.Website/Controllers/Debug/RoomVisualizerController.cs @@ -42,7 +42,7 @@ public class RoomVisualizerController : ControllerBase #if !DEBUG return this.NotFound(); #else - RoomHelper.Rooms.RemoveAll(); + lock(RoomHelper.RoomLock) RoomHelper.Rooms.RemoveAll(); return this.Redirect("/debug/roomVisualizer"); #endif } diff --git a/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml index 5c6e49c3..0fc14314 100644 --- a/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml @@ -1,14 +1,16 @@ @page "/" @using LBPUnion.ProjectLighthouse.Configuration +@using LBPUnion.ProjectLighthouse.Extensions @using LBPUnion.ProjectLighthouse.PlayerData.Profiles -@using LBPUnion.ProjectLighthouse.Types +@using LBPUnion.ProjectLighthouse.Levels @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.LandingPage @{ Layout = "Layouts/BaseLayout"; Model.ShowTitleInPage = false; + bool isMobile = this.Request.IsMobile(); } -

Welcome to Project Lighthouse!

+

Welcome to @ServerConfiguration.Instance.Customization.ServerName!

@if (Model.User != null) { @@ -41,4 +43,39 @@ else { @user.Username } -} \ No newline at end of file +} + +
+ +
+
+
+

Latest Team Picks

+
+
+ @foreach (Slot slot in Model.LatestTeamPicks!) @* Can't reach a point where this is null *@ + { + @await Html.PartialAsync("Partials/SlotCardPartial", slot, Model.GetSlotViewData(slot.SlotId, isMobile)) +
+ } +
+
+
+ @if (isMobile) + { +
+ } +
+
+

Newest Levels

+
+
+ @foreach (Slot slot in Model.NewestLevels!) @* Can't reach a point where this is null *@ + { + @await Html.PartialAsync("Partials/SlotCardPartial", slot, Model.GetSlotViewData(slot.SlotId, isMobile)) +
+ } +
+
+
+
\ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml.cs index a2968d79..20b13dbc 100644 --- a/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml.cs @@ -1,23 +1,27 @@ #nullable enable using JetBrains.Annotations; using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.Levels; using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; -using LBPUnion.ProjectLighthouse.Types; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages; public class LandingPage : BaseLayout { + public LandingPage(Database database) : base(database) + {} public int AuthenticationAttemptsCount; public List PlayersOnline = new(); public int PlayersOnlineCount; - public LandingPage(Database database) : base(database) - {} + + public List? LatestTeamPicks; + public List? NewestLevels; [UsedImplicitly] public async Task OnGet() @@ -35,6 +39,38 @@ public class LandingPage : BaseLayout List userIds = await this.Database.LastContacts.Where(l => TimeHelper.Timestamp - l.Timestamp < 300).Select(l => l.UserId).ToListAsync(); this.PlayersOnline = await this.Database.Users.Where(u => userIds.Contains(u.UserId)).ToListAsync(); + + const int maxShownLevels = 5; + + this.LatestTeamPicks = await this.Database.Slots.Where + (s => s.TeamPick) + .OrderByDescending(s => s.FirstUploaded) + .Take(maxShownLevels) + .Include(s => s.Creator) + .ToListAsync(); + + this.NewestLevels = await this.Database.Slots.OrderByDescending(s => s.FirstUploaded).Take(maxShownLevels).Include(s => s.Creator).ToListAsync(); + return this.Page(); } + + public ViewDataDictionary GetSlotViewData(int slotId, bool isMobile = false) + => new(ViewData) + { + { + "User", this.User + }, + { + "CallbackUrl", $"~/slot/{slotId}" + }, + { + "ShowLink", true + }, + { + "IsMini", true + }, + { + "IsMobile", isMobile + }, + }; } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Layouts/BaseLayout.cshtml b/ProjectLighthouse.Servers.Website/Pages/Layouts/BaseLayout.cshtml index 2ca305e2..f19c67e2 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Layouts/BaseLayout.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Layouts/BaseLayout.cshtml @@ -33,11 +33,11 @@ @if (Model.Title == string.Empty) { - Project Lighthouse + @ServerConfiguration.Instance.Customization.ServerName } else { - Project Lighthouse - @Model.Title + @ServerConfiguration.Instance.Customization.ServerName - @Model.Title } @@ -52,7 +52,7 @@ @* Embed Stuff *@ - + @if (!string.IsNullOrEmpty(Model.Description)) { diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/SlotCardPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/SlotCardPartial.cshtml index 6e469d63..371d5920 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/SlotCardPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/SlotCardPartial.cshtml @@ -2,7 +2,6 @@ @using LBPUnion.ProjectLighthouse.Configuration @using LBPUnion.ProjectLighthouse.PlayerData @using LBPUnion.ProjectLighthouse.PlayerData.Profiles -@using LBPUnion.ProjectLighthouse.Types @using Microsoft.EntityFrameworkCore @model LBPUnion.ProjectLighthouse.Levels.Slot @@ -12,8 +11,10 @@ await using Database database = new(); string slotName = string.IsNullOrEmpty(Model.Name) ? "Unnamed Level" : Model.Name; - bool isMobile = (bool?)ViewData["IsMobile"] ?? false; + bool isMobile = (bool?)ViewData["IsMobile"] ?? false; + bool mini = (bool?)ViewData["IsMini"] ?? false; + bool isQueued = false; bool isHearted = false; @@ -31,25 +32,44 @@ }
@{ - int size = isMobile ? 50 : 100; + int size = isMobile || mini ? 50 : 100; }
- @if (showLink) + @if (!mini) { -

- @slotName -

+ @if (showLink) + { +

+ @slotName +

+ } + else + { +

+ @slotName +

+ } } else { -

- @slotName -

+ @if (showLink) + { +

+ @slotName +

+ } + else + { +

+ @slotName +

+ } } +
@Model.Hearts @Model.PlaysUnique @@ -68,7 +88,7 @@

- @if (user != null) + @if (user != null && !mini) { if (isHearted) { diff --git a/ProjectLighthouse.Servers.Website/Pages/PasswordResetPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/PasswordResetPage.cshtml.cs index 5a767460..6f744979 100644 --- a/ProjectLighthouse.Servers.Website/Pages/PasswordResetPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/PasswordResetPage.cshtml.cs @@ -1,5 +1,6 @@ #nullable enable using JetBrains.Annotations; +using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; @@ -38,7 +39,8 @@ public class PasswordResetPage : BaseLayout await this.Database.SaveChangesAsync(); - if (!user.EmailAddressVerified) return this.Redirect("~/login/sendVerificationEmail"); + if (!user.EmailAddressVerified && ServerConfiguration.Instance.Mail.MailEnabled) + return this.Redirect("~/login/sendVerificationEmail"); return this.Redirect("~/"); } diff --git a/ProjectLighthouse.Tests.GameApiTests/Tests/AuthenticationTests.cs b/ProjectLighthouse.Tests.GameApiTests/Tests/AuthenticationTests.cs index 1fe89c3e..acfbfb6b 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Tests/AuthenticationTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Tests/AuthenticationTests.cs @@ -2,6 +2,7 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; using LBPUnion.ProjectLighthouse.Configuration; +using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.PlayerData; using LBPUnion.ProjectLighthouse.Tests; using Xunit; @@ -25,7 +26,7 @@ public class AuthenticationTests : LighthouseServerTest Assert.True(response.IsSuccessStatusCode); string responseContent = await response.Content.ReadAsStringAsync(); Assert.Contains("MM_AUTH=", responseContent); - Assert.Contains(ServerStatics.ServerName, responseContent); + Assert.Contains(VersionHelper.FullVersion, responseContent); } [DatabaseFact] @@ -35,10 +36,10 @@ public class AuthenticationTests : LighthouseServerTest Assert.NotNull(loginResult); Assert.NotNull(loginResult.AuthTicket); - Assert.NotNull(loginResult.LbpEnvVer); + Assert.NotNull(loginResult.ServerBrand); Assert.Contains("MM_AUTH=", loginResult.AuthTicket); - Assert.Equal(ServerStatics.ServerName, loginResult.LbpEnvVer); + Assert.Equal(VersionHelper.FullVersion, loginResult.ServerBrand); } [DatabaseFact] diff --git a/ProjectLighthouse.Tests.WebsiteTests/ProjectLighthouse.Tests.WebsiteTests.csproj b/ProjectLighthouse.Tests.WebsiteTests/ProjectLighthouse.Tests.WebsiteTests.csproj index 68bb134c..4483c810 100644 --- a/ProjectLighthouse.Tests.WebsiteTests/ProjectLighthouse.Tests.WebsiteTests.csproj +++ b/ProjectLighthouse.Tests.WebsiteTests/ProjectLighthouse.Tests.WebsiteTests.csproj @@ -15,7 +15,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/ProjectLighthouse.sln.DotSettings b/ProjectLighthouse.sln.DotSettings index 8a9a5b94..524940d2 100644 --- a/ProjectLighthouse.sln.DotSettings +++ b/ProjectLighthouse.sln.DotSettings @@ -115,6 +115,7 @@ True True True + True True True True diff --git a/ProjectLighthouse/Administration/CompletedMigration.cs b/ProjectLighthouse/Administration/CompletedMigration.cs new file mode 100644 index 00000000..3ddd3b25 --- /dev/null +++ b/ProjectLighthouse/Administration/CompletedMigration.cs @@ -0,0 +1,25 @@ +using System; +using System.ComponentModel.DataAnnotations; +using LBPUnion.ProjectLighthouse.Administration.Maintenance; + +namespace LBPUnion.ProjectLighthouse.Administration; + +/// +/// A record of the completion of a . +/// +public class CompletedMigration +{ + /// + /// The name of the migration. + /// + /// + /// Do not use the user-friendly name when setting this. + /// + [Key] + public string MigrationName { get; set; } + + /// + /// The moment the migration was ran. + /// + public DateTime RanAt { get; set; } +} \ No newline at end of file diff --git a/ProjectLighthouse/Administration/Maintenance/Commands/FlushRedisCommand.cs b/ProjectLighthouse/Administration/Maintenance/Commands/FlushRedisCommand.cs new file mode 100644 index 00000000..85fde257 --- /dev/null +++ b/ProjectLighthouse/Administration/Maintenance/Commands/FlushRedisCommand.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Logging; +using LBPUnion.ProjectLighthouse.StorableLists; + +namespace LBPUnion.ProjectLighthouse.Administration.Maintenance.Commands; + +public class FlushRedisCommand : ICommand +{ + public string Name() => "Flush Redis"; + public string[] Aliases() => new[] { + "flush", "flush-redis", + }; + public string Arguments() => ""; + public int RequiredArgs() => 0; + + public async Task Run(string[] args, Logger logger) + { + await RedisDatabase.FlushAll(); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Administration/Maintenance/IMaintenanceJob.cs b/ProjectLighthouse/Administration/Maintenance/IMaintenanceJob.cs index cf9b82da..15229ed1 100644 --- a/ProjectLighthouse/Administration/Maintenance/IMaintenanceJob.cs +++ b/ProjectLighthouse/Administration/Maintenance/IMaintenanceJob.cs @@ -1,7 +1,9 @@ using System.Threading.Tasks; +using JetBrains.Annotations; namespace LBPUnion.ProjectLighthouse.Administration.Maintenance; +[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)] public interface IMaintenanceJob { public Task Run(); diff --git a/ProjectLighthouse/Administration/Maintenance/IMigrationTask.cs b/ProjectLighthouse/Administration/Maintenance/IMigrationTask.cs new file mode 100644 index 00000000..cba570a3 --- /dev/null +++ b/ProjectLighthouse/Administration/Maintenance/IMigrationTask.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace LBPUnion.ProjectLighthouse.Administration.Maintenance; + +[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)] +public interface IMigrationTask +{ + /// + /// The user-friendly name of a migration. + /// + public string Name(); + + /// + /// Performs the migration. + /// + /// The Lighthouse database. + /// True if successful, false if not. + internal Task Run(Database database); +} \ No newline at end of file diff --git a/ProjectLighthouse/Administration/Maintenance/MaintenanceHelper.cs b/ProjectLighthouse/Administration/Maintenance/MaintenanceHelper.cs index 3d86ad38..7d7490d8 100644 --- a/ProjectLighthouse/Administration/Maintenance/MaintenanceHelper.cs +++ b/ProjectLighthouse/Administration/Maintenance/MaintenanceHelper.cs @@ -1,9 +1,11 @@ #nullable enable using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Logging.Loggers; @@ -11,24 +13,16 @@ namespace LBPUnion.ProjectLighthouse.Administration.Maintenance; public static class MaintenanceHelper { - static MaintenanceHelper() { Commands = getListOfInterfaceObjects(); MaintenanceJobs = getListOfInterfaceObjects(); + MigrationTasks = getListOfInterfaceObjects(); } + public static List Commands { get; } - public static List MaintenanceJobs { get; } - - private static List getListOfInterfaceObjects() where T : class - { - return Assembly.GetExecutingAssembly() - .GetTypes() - .Where(t => t.GetInterfaces().Contains(typeof(T)) && t.GetConstructor(Type.EmptyTypes) != null) - .Select(t => Activator.CreateInstance(t) as T) - .ToList()!; - } + public static List MigrationTasks { get; } public static async Task> RunCommand(string[] args) { @@ -66,11 +60,57 @@ public static class MaintenanceHelper IMaintenanceJob? job = MaintenanceJobs.FirstOrDefault(j => j.GetType().Name == jobName); if (job == null) throw new ArgumentNullException(); - await RunMaintenanceJob(job); - } - - public static async Task RunMaintenanceJob(IMaintenanceJob job) - { await job.Run(); } + + public static async Task RunMigration(IMigrationTask migrationTask, Database? database = null) + { + database ??= new Database(); + + // Migrations should never be run twice. + Debug.Assert(!await database.CompletedMigrations.Has(m => m.MigrationName == migrationTask.GetType().Name)); + + Logger.Info($"Running migration task {migrationTask.Name()}", LogArea.Database); + + bool success; + Exception? exception = null; + + try + { + success = await migrationTask.Run(database); + } + catch(Exception e) + { + success = false; + exception = e; + } + + if(!success) + { + Logger.Error($"Could not run migration {migrationTask.Name()}", LogArea.Database); + if (exception != null) Logger.Error(exception.ToDetailedException(), LogArea.Database); + + return; + } + + Logger.Success($"Successfully completed migration {migrationTask.Name()}", LogArea.Database); + + CompletedMigration completedMigration = new() + { + MigrationName = migrationTask.GetType().Name, + RanAt = DateTime.Now, + }; + + database.CompletedMigrations.Add(completedMigration); + await database.SaveChangesAsync(); + } + + private static List getListOfInterfaceObjects() where T : class + { + return Assembly.GetExecutingAssembly() + .GetTypes() + .Where(t => t.GetInterfaces().Contains(typeof(T)) && t.GetConstructor(Type.EmptyTypes) != null) + .Select(t => Activator.CreateInstance(t) as T) + .ToList()!; + } } \ No newline at end of file diff --git a/ProjectLighthouse/Administration/Maintenance/MaintenanceJobs/CleanupBrokenPhotosMaintenanceJob.cs b/ProjectLighthouse/Administration/Maintenance/MaintenanceJobs/CleanupBrokenPhotosMaintenanceJob.cs index fdd6f523..b8074025 100644 --- a/ProjectLighthouse/Administration/Maintenance/MaintenanceJobs/CleanupBrokenPhotosMaintenanceJob.cs +++ b/ProjectLighthouse/Administration/Maintenance/MaintenanceJobs/CleanupBrokenPhotosMaintenanceJob.cs @@ -25,7 +25,7 @@ public class CleanupBrokenPhotosMaintenanceJob : IMaintenanceJob bool largeHashIsInvalidFile = false; bool tooManyPhotoSubjects = false; bool duplicatePhotoSubjects = false; - bool takenInTheFuture = true; + bool takenInTheFuture = false; // Checks should generally be ordered in least computationally expensive to most. diff --git a/ProjectLighthouse/Administration/Maintenance/MaintenanceJobs/CleanupXMLInjection.cs b/ProjectLighthouse/Administration/Maintenance/MaintenanceJobs/CleanupXMLInjection.cs deleted file mode 100644 index 91dd40d3..00000000 --- a/ProjectLighthouse/Administration/Maintenance/MaintenanceJobs/CleanupXMLInjection.cs +++ /dev/null @@ -1,36 +0,0 @@ -#nullable enable -using System.Threading.Tasks; -using LBPUnion.ProjectLighthouse.Administration.Reports; -using LBPUnion.ProjectLighthouse.Helpers; -using LBPUnion.ProjectLighthouse.Levels; -using LBPUnion.ProjectLighthouse.PlayerData; -using LBPUnion.ProjectLighthouse.PlayerData.Profiles; -using LBPUnion.ProjectLighthouse.PlayerData.Reviews; - -namespace LBPUnion.ProjectLighthouse.Administration.Maintenance.MaintenanceJobs; - -public class CleanupXmlInjection : IMaintenanceJob -{ - private readonly Database database = new(); - public string Name() => "Sanitize user content"; - public string Description() => "Sanitizes all user-generated strings in levels, reviews, comments, users, and scores to prevent XML injection. Only needs to be run once."; - - public async Task Run() - { - foreach (Slot slot in this.database.Slots) SanitizationHelper.SanitizeStringsInClass(slot); - - foreach (Review review in this.database.Reviews) SanitizationHelper.SanitizeStringsInClass(review); - - foreach (Comment comment in this.database.Comments) SanitizationHelper.SanitizeStringsInClass(comment); - - foreach (Score score in this.database.Scores) SanitizationHelper.SanitizeStringsInClass(score); - - foreach (User user in this.database.Users) SanitizationHelper.SanitizeStringsInClass(user); - - foreach (Photo photo in this.database.Photos) SanitizationHelper.SanitizeStringsInClass(photo); - - foreach (GriefReport report in this.database.Reports) SanitizationHelper.SanitizeStringsInClass(report); - - await this.database.SaveChangesAsync(); - } -} \ No newline at end of file diff --git a/ProjectLighthouse/Administration/Maintenance/MigrationTasks/CleanupXMLInjectionMigration.cs b/ProjectLighthouse/Administration/Maintenance/MigrationTasks/CleanupXMLInjectionMigration.cs new file mode 100644 index 00000000..bb1b71e9 --- /dev/null +++ b/ProjectLighthouse/Administration/Maintenance/MigrationTasks/CleanupXMLInjectionMigration.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Helpers; + +namespace LBPUnion.ProjectLighthouse.Administration.Maintenance.MigrationTasks; + +public class CleanupXmlInjectionMigration : IMigrationTask +{ + public string Name() => "Cleanup XML injections"; + + // Weird, but required. Thanks, hejlsberg. + async Task IMigrationTask.Run(Database database) + { + List objsToBeSanitized = new(); + + // Store all the objects we need to sanitize in a list. + // The alternative here is to loop through every table, but thats a ton of code... + objsToBeSanitized.AddRange(database.Slots); + objsToBeSanitized.AddRange(database.Reviews); + objsToBeSanitized.AddRange(database.Comments); + objsToBeSanitized.AddRange(database.Scores); + objsToBeSanitized.AddRange(database.Users); + objsToBeSanitized.AddRange(database.Photos); + objsToBeSanitized.AddRange(database.Reports); + + foreach (object obj in objsToBeSanitized) SanitizationHelper.SanitizeStringsInClass(obj); + + await database.SaveChangesAsync(); + return true; + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Configuration/ConfigurationCategories/CustomizationConfiguration.cs b/ProjectLighthouse/Configuration/ConfigurationCategories/CustomizationConfiguration.cs new file mode 100644 index 00000000..027c1753 --- /dev/null +++ b/ProjectLighthouse/Configuration/ConfigurationCategories/CustomizationConfiguration.cs @@ -0,0 +1,6 @@ +namespace LBPUnion.ProjectLighthouse.Configuration.ConfigurationCategories; + +public class CustomizationConfiguration +{ + public string ServerName { get; set; } = "Project Lighthouse"; +} \ No newline at end of file diff --git a/ProjectLighthouse/Configuration/ServerConfiguration.cs b/ProjectLighthouse/Configuration/ServerConfiguration.cs index ddf8113b..bd3ce250 100644 --- a/ProjectLighthouse/Configuration/ServerConfiguration.cs +++ b/ProjectLighthouse/Configuration/ServerConfiguration.cs @@ -23,7 +23,7 @@ public class ServerConfiguration // You can use an ObsoleteAttribute instead. Make sure you set it to error, though. // // Thanks for listening~ - public const int CurrentConfigVersion = 4; + public const int CurrentConfigVersion = 5; #region Meta @@ -163,6 +163,9 @@ public class ServerConfiguration #endregion + // TODO: Find a way to properly remove config options + // YamlDotNet hates that and it's fucking annoying. + // This seriously sucks. /rant [Obsolete("Obsolete. Use the Website/GameApi/Api listen URLS instead.")] public string ListenUrl { get; set; } = "http://localhost:10060"; @@ -193,5 +196,5 @@ public class ServerConfiguration public MailConfiguration Mail { get; set; } = new(); public UserGeneratedContentLimitConfiguration UserGeneratedContentLimits { get; set; } = new(); public WebsiteConfiguration WebsiteConfiguration { get; set; } = new(); - + public CustomizationConfiguration Customization { get; set; } = new(); } \ No newline at end of file diff --git a/ProjectLighthouse/Configuration/ServerStatics.cs b/ProjectLighthouse/Configuration/ServerStatics.cs index 233d0eba..8e87417d 100644 --- a/ProjectLighthouse/Configuration/ServerStatics.cs +++ b/ProjectLighthouse/Configuration/ServerStatics.cs @@ -2,13 +2,12 @@ using System; using System.Linq; using LBPUnion.ProjectLighthouse.Logging; +using LBPUnion.ProjectLighthouse.Types; namespace LBPUnion.ProjectLighthouse.Configuration; public static class ServerStatics { - public const string ServerName = "ProjectLighthouse"; - public const int PageSize = 20; public static bool DbConnected { @@ -25,11 +24,18 @@ public static class ServerStatics } } + // FIXME: This needs to go at some point. public static bool IsUnitTesting => AppDomain.CurrentDomain.GetAssemblies().Any(assembly => assembly.FullName!.StartsWith("xunit")); #if DEBUG - public static readonly bool IsDebug = true; + public const bool IsDebug = true; #else - public static readonly bool IsDebug = false; + public const bool IsDebug = false; #endif + + /// + /// The servertype, determined on startup. Shouldn't be null unless very very early in startup. + /// + // The way of doing this is kinda weird, but it works. + public static ServerType ServerType; } \ No newline at end of file diff --git a/ProjectLighthouse/Database.cs b/ProjectLighthouse/Database.cs index 7430e070..eca4a621 100644 --- a/ProjectLighthouse/Database.cs +++ b/ProjectLighthouse/Database.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Administration; using LBPUnion.ProjectLighthouse.Administration.Reports; using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Helpers; @@ -21,6 +22,7 @@ namespace LBPUnion.ProjectLighthouse; public class Database : DbContext { + public DbSet CompletedMigrations { get; set; } public DbSet Users { get; set; } public DbSet Locations { get; set; } public DbSet Slots { get; set; } diff --git a/ProjectLighthouse/Extensions/DatabaseExtensions.cs b/ProjectLighthouse/Extensions/DatabaseExtensions.cs index eb8af6c7..a7e27782 100644 --- a/ProjectLighthouse/Extensions/DatabaseExtensions.cs +++ b/ProjectLighthouse/Extensions/DatabaseExtensions.cs @@ -1,4 +1,7 @@ +using System; using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; using LBPUnion.ProjectLighthouse.Levels; using LBPUnion.ProjectLighthouse.PlayerData; using LBPUnion.ProjectLighthouse.PlayerData.Reviews; @@ -52,4 +55,7 @@ public static class DatabaseExtensions return query; } + + public static async Task Has(this IQueryable queryable, Expression> predicate) => + await queryable.FirstOrDefaultAsync(predicate) != null; } \ No newline at end of file diff --git a/ProjectLighthouse/Helpers/VersionHelper.cs b/ProjectLighthouse/Helpers/VersionHelper.cs index 3642c5fe..212eb091 100644 --- a/ProjectLighthouse/Helpers/VersionHelper.cs +++ b/ProjectLighthouse/Helpers/VersionHelper.cs @@ -31,8 +31,8 @@ public static class VersionHelper { Logger.Error ( - "Project Lighthouse was built incorrectly. Please make sure git is available when building. " + - "Because of this, you will not be notified of updates.", + "Project Lighthouse was built incorrectly. Please make sure git is available when building.", +// "Because of this, you will not be notified of updates.", LogArea.Startup ); CommitHash = "invalid"; @@ -54,14 +54,14 @@ public static class VersionHelper public static string CommitHash { get; set; } public static string Branch { get; set; } - public static string FullVersion => $"{ServerStatics.ServerName} {Branch}@{CommitHash} {Build}"; + public static string FullVersion => $"Project Lighthouse {Branch}@{CommitHash} {Build} ({ServerConfiguration.Instance.Customization.ServerName})"; public static bool IsDirty => CommitHash.EndsWith("-dirty") || CommitsOutOfDate != 1 || CommitHash == "invalid" || Branch == "invalid"; public static int CommitsOutOfDate { get; set; } public static bool CanCheckForUpdates { get; set; } public static string[] Remotes { get; set; } public const string Build = - #if DEBUG + #if DEBUG "Debug"; #elif RELEASE "Release"; diff --git a/ProjectLighthouse/Logging/Loggers/ConsoleLogger.cs b/ProjectLighthouse/Logging/Loggers/ConsoleLogger.cs index 4cc2351a..3ebc3343 100644 --- a/ProjectLighthouse/Logging/Loggers/ConsoleLogger.cs +++ b/ProjectLighthouse/Logging/Loggers/ConsoleLogger.cs @@ -11,7 +11,8 @@ public class ConsoleLogger : ILogger foreach (string line in logLine.Message.Split('\n')) { - // The following is scuffed. Beware~ + // The following is scuffed. + // Beware~ // Write the level! [Success] Console.ForegroundColor = ConsoleColor.White; diff --git a/ProjectLighthouse/Logging/Loggers/LighthouseFileLogger.cs b/ProjectLighthouse/Logging/Loggers/FileLogger.cs similarity index 62% rename from ProjectLighthouse/Logging/Loggers/LighthouseFileLogger.cs rename to ProjectLighthouse/Logging/Loggers/FileLogger.cs index 405e31a1..802263e5 100644 --- a/ProjectLighthouse/Logging/Loggers/LighthouseFileLogger.cs +++ b/ProjectLighthouse/Logging/Loggers/FileLogger.cs @@ -1,11 +1,11 @@ using System; using System.IO; +using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Files; -using LBPUnion.ProjectLighthouse.Helpers; namespace LBPUnion.ProjectLighthouse.Logging.Loggers; -public class LighthouseFileLogger : ILogger +public class FileLogger : ILogger { private static readonly string logsDirectory = Path.Combine(Environment.CurrentDirectory, "logs"); @@ -13,8 +13,8 @@ public class LighthouseFileLogger : ILogger { FileHelper.EnsureDirectoryCreated(logsDirectory); - string contentFile = $"[{line.Level}] <{line.Trace.Name}:{line.Trace.Section}> {line.Message}\n"; - string contentAll = $"[{line.Area}:{line.Level}] <{line.Trace.Name}:{line.Trace.Section}> {line.Message}\n"; + string contentFile = $"[{ServerStatics.ServerType}] [{line.Level}] <{line.Trace.Name}:{line.Trace.Section}> {line.Message}\n"; + string contentAll = $"[{ServerStatics.ServerType}] [{line.Area}:{line.Level}] <{line.Trace.Name}:{line.Trace.Section}> {line.Message}\n"; try { diff --git a/ProjectLighthouse/Match/Rooms/RoomHelper.cs b/ProjectLighthouse/Match/Rooms/RoomHelper.cs index 9d4f18e4..33f1959c 100644 --- a/ProjectLighthouse/Match/Rooms/RoomHelper.cs +++ b/ProjectLighthouse/Match/Rooms/RoomHelper.cs @@ -16,7 +16,8 @@ namespace LBPUnion.ProjectLighthouse.Match.Rooms; public class RoomHelper { - public static readonly StorableList Rooms = RoomStore.GetRooms(); + public static readonly object RoomLock = new(); + public static StorableList Rooms => RoomStore.GetRooms(); public static void StartCleanupThread() { @@ -162,7 +163,7 @@ public class RoomHelper }; CleanupRooms(room.HostId, room); - lock(Rooms) Rooms.Add(room); + lock(RoomLock) Rooms.Add(room); Logger.Info($"Created room (id: {room.RoomId}) for host {room.HostId}", LogArea.Match); return room; @@ -193,7 +194,7 @@ public class RoomHelper [SuppressMessage("ReSharper", "InvertIf")] public static void CleanupRooms(int? hostId = null, Room? newRoom = null, Database? database = null) { - lock(Rooms) + lock(RoomLock) { int roomCountBeforeCleanup = Rooms.Count(); diff --git a/ProjectLighthouse/Migrations/20220610061641_AddCompletedMigrations.cs b/ProjectLighthouse/Migrations/20220610061641_AddCompletedMigrations.cs new file mode 100644 index 00000000..5c33f89a --- /dev/null +++ b/ProjectLighthouse/Migrations/20220610061641_AddCompletedMigrations.cs @@ -0,0 +1,37 @@ +using System; +using LBPUnion.ProjectLighthouse; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ProjectLighthouse.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20220610061641_AddCompletedMigrations")] + public class AddCompletedMigrations : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "CompletedMigrations", + columns: table => new + { + MigrationName = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + RanAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CompletedMigrations", x => x.MigrationName); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CompletedMigrations"); + } + } +} diff --git a/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs b/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs index 0053459b..147d7e03 100644 --- a/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs +++ b/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs @@ -1,4 +1,5 @@ // +using System; using LBPUnion.ProjectLighthouse; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -18,6 +19,19 @@ namespace ProjectLighthouse.Migrations .HasAnnotation("ProductVersion", "6.0.5") .HasAnnotation("Relational:MaxIdentifierLength", 64); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Administration.CompletedMigration", b => + { + b.Property("MigrationName") + .HasColumnType("varchar(255)"); + + b.Property("RanAt") + .HasColumnType("datetime(6)"); + + b.HasKey("MigrationName"); + + b.ToTable("CompletedMigrations"); + }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Administration.Reports.GriefReport", b => { b.Property("ReportId") @@ -581,15 +595,16 @@ namespace ProjectLighthouse.Migrations b.Property("AdminGrantedSlots") .HasColumnType("int"); + b.Property("Banned") + .HasColumnType("tinyint(1)"); + b.Property("BannedReason") .HasColumnType("longtext"); b.Property("Biography") - .IsRequired() .HasColumnType("longtext"); b.Property("BooHash") - .IsRequired() .HasColumnType("longtext"); b.Property("EmailAddress") @@ -602,48 +617,39 @@ namespace ProjectLighthouse.Migrations .HasColumnType("int"); b.Property("IconHash") - .IsRequired() .HasColumnType("longtext"); + b.Property("IsAdmin") + .HasColumnType("tinyint(1)"); + b.Property("LocationId") .HasColumnType("int"); b.Property("MehHash") - .IsRequired() .HasColumnType("longtext"); b.Property("Password") - .IsRequired() .HasColumnType("longtext"); b.Property("PasswordResetRequired") .HasColumnType("tinyint(1)"); - b.Property("PermissionLevel") - .HasColumnType("int"); - b.Property("Pins") - .IsRequired() .HasColumnType("longtext"); b.Property("PlanetHashLBP2") - .IsRequired() .HasColumnType("longtext"); b.Property("PlanetHashLBP3") - .IsRequired() .HasColumnType("longtext"); b.Property("PlanetHashLBPVita") - .IsRequired() .HasColumnType("longtext"); b.Property("Username") - .IsRequired() .HasColumnType("longtext"); b.Property("YayHash") - .IsRequired() .HasColumnType("longtext"); b.HasKey("UserId"); diff --git a/ProjectLighthouse/PlayerData/LoginResult.cs b/ProjectLighthouse/PlayerData/LoginResult.cs index f577f175..09842ac3 100644 --- a/ProjectLighthouse/PlayerData/LoginResult.cs +++ b/ProjectLighthouse/PlayerData/LoginResult.cs @@ -15,9 +15,12 @@ public class LoginResult public string AuthTicket { get; set; } [XmlElement("lbpEnvVer")] - public string LbpEnvVer { get; set; } + public string ServerBrand { get; set; } public string Serialize() => LbpSerializer.Elements - (new KeyValuePair("authTicket", this.AuthTicket), new KeyValuePair("lbpEnvVer", this.LbpEnvVer)); + ( + new KeyValuePair("authTicket", this.AuthTicket), + new KeyValuePair("lbpEnvVer", this.ServerBrand) + ); } \ No newline at end of file diff --git a/ProjectLighthouse/ProjectLighthouse.csproj b/ProjectLighthouse/ProjectLighthouse.csproj index 4df7087c..f38c51ff 100644 --- a/ProjectLighthouse/ProjectLighthouse.csproj +++ b/ProjectLighthouse/ProjectLighthouse.csproj @@ -11,7 +11,7 @@ - + @@ -50,10 +50,6 @@ - - - - diff --git a/ProjectLighthouse/Startup/DebugWarmupLifetime.cs b/ProjectLighthouse/Startup/DebugWarmupLifetime.cs index 0af030c3..2161dbf4 100644 --- a/ProjectLighthouse/Startup/DebugWarmupLifetime.cs +++ b/ProjectLighthouse/Startup/DebugWarmupLifetime.cs @@ -20,7 +20,6 @@ public class DebugWarmupLifetime : IHostLifetime private CancellationTokenRegistration applicationStartedRegistration; private readonly ConsoleLifetime consoleLifetime; - public static ServerType ServerType; public DebugWarmupLifetime ( @@ -39,7 +38,7 @@ public class DebugWarmupLifetime : IHostLifetime { using HttpClient client = new(); - string url = ServerType switch + string url = ServerStatics.ServerType switch { ServerType.GameServer => ServerConfiguration.Instance.GameApiListenUrl, ServerType.Website => ServerConfiguration.Instance.WebsiteListenUrl, diff --git a/ProjectLighthouse/StartupTasks.cs b/ProjectLighthouse/StartupTasks.cs index 100c3fc1..95497e90 100644 --- a/ProjectLighthouse/StartupTasks.cs +++ b/ProjectLighthouse/StartupTasks.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; +using LBPUnion.ProjectLighthouse.Administration; using LBPUnion.ProjectLighthouse.Administration.Maintenance; using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Extensions; @@ -9,6 +11,7 @@ using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Logging.Loggers; using LBPUnion.ProjectLighthouse.Match.Rooms; +using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using LBPUnion.ProjectLighthouse.Startup; using LBPUnion.ProjectLighthouse.StorableLists; using LBPUnion.ProjectLighthouse.Types; @@ -24,18 +27,16 @@ public static class StartupTasks Stopwatch stopwatch = new(); stopwatch.Start(); - #if DEBUG - DebugWarmupLifetime.ServerType = serverType; - #endif + ServerStatics.ServerType = serverType; // Setup logging Logger.Instance.AddLogger(new ConsoleLogger()); - Logger.Instance.AddLogger(new LighthouseFileLogger()); + Logger.Instance.AddLogger(new FileLogger()); Logger.Info($"Welcome to the Project Lighthouse {serverType.ToString()}!", LogArea.Startup); Logger.Info($"You are running version {VersionHelper.FullVersion}", LogArea.Startup); - // Referencing ServerSettings.Instance here loads the config, see ServerSettings.cs for more information + // Referencing ServerConfiguration.Instance here loads the config, see ServerConfiguration.cs for more information Logger.Success("Loaded config file version " + ServerConfiguration.Instance.ConfigVersion, LogArea.Startup); Logger.Info("Connecting to the database...", LogArea.Startup); @@ -46,13 +47,15 @@ public static class StartupTasks } else { - Logger.Success("Connected!", LogArea.Startup); + Logger.Success("Connected to the database!", LogArea.Startup); } if (!dbConnected) Environment.Exit(1); using Database database = new(); - - Logger.Info("Migrating database...", LogArea.Database); + + #if !DEBUG + if(serverType == ServerType.GameServer) + #endif migrateDatabase(database); if (ServerConfiguration.Instance.InfluxDB.InfluxEnabled) @@ -83,25 +86,66 @@ public static class StartupTasks { FileHelper.ConvertAllTexturesToPng(); } - - Logger.Info("Starting room cleanup thread...", LogArea.Startup); - RoomHelper.StartCleanupThread(); - + Logger.Info("Initializing Redis...", LogArea.Startup); RedisDatabase.Initialize().Wait(); + if (serverType == ServerType.GameServer) + { + Logger.Info("Starting room cleanup thread...", LogArea.Startup); + RoomHelper.StartCleanupThread(); + } + + // Create admin user if no users exist + if (serverType == ServerType.Website && database.Users.CountAsync().Result == 0) + { + const string passwordClear = "lighthouse"; + string password = CryptoHelper.BCryptHash(CryptoHelper.Sha256Hash(passwordClear)); + + User admin = database.CreateUser("admin", password).Result; + admin.IsAdmin = true; + admin.PasswordResetRequired = true; + + database.SaveChanges(); + + Logger.Success("No users were found, so an admin user was created. " + + $"The username is 'admin' and the password is '{passwordClear}'.", LogArea.Startup); + } + stopwatch.Stop(); Logger.Success($"Ready! Startup took {stopwatch.ElapsedMilliseconds}ms. Passing off control to ASP.NET...", LogArea.Startup); } private static void migrateDatabase(Database database) { + Logger.Info("Migrating database...", LogArea.Database); + Stopwatch totalStopwatch = new(); Stopwatch stopwatch = new(); + totalStopwatch.Start(); stopwatch.Start(); database.Database.MigrateAsync().Wait(); + stopwatch.Stop(); + Logger.Success($"Structure migration took {stopwatch.ElapsedMilliseconds}ms.", LogArea.Database); + + stopwatch.Reset(); + stopwatch.Start(); + + List completedMigrations = database.CompletedMigrations.ToList(); + List migrationsToRun = MaintenanceHelper.MigrationTasks + .Where(migrationTask => !completedMigrations + .Select(m => m.MigrationName) + .Contains(migrationTask.GetType().Name) + ).ToList(); + + foreach (IMigrationTask migrationTask in migrationsToRun) + { + MaintenanceHelper.RunMigration(migrationTask, database).Wait(); + } stopwatch.Stop(); - Logger.Success($"Migration took {stopwatch.ElapsedMilliseconds}ms.", LogArea.Database); + totalStopwatch.Stop(); + Logger.Success($"Extra migration tasks took {stopwatch.ElapsedMilliseconds}ms.", LogArea.Database); + Logger.Success($"Total migration took {totalStopwatch.ElapsedMilliseconds}ms.", LogArea.Database); } } \ No newline at end of file diff --git a/ProjectLighthouse/StorableLists/RedisDatabase.cs b/ProjectLighthouse/StorableLists/RedisDatabase.cs index f97b127d..6910aec0 100644 --- a/ProjectLighthouse/StorableLists/RedisDatabase.cs +++ b/ProjectLighthouse/StorableLists/RedisDatabase.cs @@ -39,8 +39,7 @@ public static class RedisDatabase return; } - await connection.RecreateIndexAsync(typeof(Room)); - await connection.RecreateIndexAsync(typeof(UserFriendData)); + await createIndexes(connection); } catch(Exception e) { @@ -52,6 +51,20 @@ public static class RedisDatabase Logger.Success("Initialized Redis.", LogArea.Redis); } + public static async Task FlushAll() + { + IRedisConnection connection = getConnection(); + await connection.ExecuteAsync("FLUSHALL"); + + await createIndexes(connection); + } + + private static async Task createIndexes(IRedisConnection connection) + { + await connection.RecreateIndexAsync(typeof(Room)); + await connection.RecreateIndexAsync(typeof(UserFriendData)); + } + private static IRedisConnection getConnection() { Logger.Debug("Getting a Redis connection", LogArea.Redis); diff --git a/scripts-and-tools/create-migration.sh b/scripts-and-tools/create-migration.sh index ab89099f..48476430 100755 --- a/scripts-and-tools/create-migration.sh +++ b/scripts-and-tools/create-migration.sh @@ -1,4 +1,9 @@ #!/bin/bash +# Developer script to create EntityFramework database migrations +# +# $1: Name of the migration, e.g. SwitchToPermissionLevels +# Invoked manually + export LIGHTHOUSE_DB_CONNECTION_STRING='server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse' dotnet ef migrations add "$1" --project ../ProjectLighthouse \ No newline at end of file