Merge branch 'main' into lbp3-collections

This commit is contained in:
jvyden 2022-01-11 02:59:18 -05:00
commit 757a96d435
No known key found for this signature in database
GPG key ID: 18BCF2BE0262B278
39 changed files with 840 additions and 59 deletions

View file

@ -13,9 +13,9 @@ jobs:
fail-fast: false
matrix:
os:
- { prettyName: Windows, fullName: windows-latest, database: true }
- { prettyName: macOS, fullName: macos-latest, database: true }
- { prettyName: Linux, fullName: ubuntu-latest, database: true }
- { prettyName: Windows, fullName: windows-latest, database: true, webTest: false }
- { prettyName: macOS, fullName: macos-latest, database: true, webTest: false }
- { prettyName: Linux, fullName: ubuntu-latest, database: true, webTest: true }
timeout-minutes: 10
env:
DB_DATABASE: lighthouse
@ -44,10 +44,18 @@ jobs:
- name: Compile
run: dotnet build -c Debug
- name: Test
- name: Run tests on ProjectLighthouse.Tests
continue-on-error: true
run: dotnet test --logger "trx;LogFileName=${{github.workspace}}/TestResults-${{matrix.os.prettyName}}.trx"
run: dotnet test --logger "trx;LogFileName=${{github.workspace}}/TestResults-${{matrix.os.prettyName}}-Tests.trx" ProjectLighthouse.Tests
- name: Run tests on ProjectLighthouse.Tests.GameApiTests
continue-on-error: true
run: dotnet test --logger "trx;LogFileName=${{github.workspace}}/TestResults-${{matrix.os.prettyName}}-GameApiTests.trx" ProjectLighthouse.Tests.GameApiTests
- name: Run tests on ProjectLighthouse.Tests.WebsiteTests
if: ${{ matrix.os.webTest }}
continue-on-error: true
run: dotnet test --logger "trx;LogFileName=${{github.workspace}}/TestResults-${{matrix.os.prettyName}}-WebsiteTests.trx" ProjectLighthouse.Tests.WebsiteTests
# Attempt to upload results even if test fails.
# https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always
@ -56,7 +64,7 @@ jobs:
if: ${{ always() }}
with:
name: lighthouse-test-results-${{matrix.os.prettyName}}
path: ${{github.workspace}}/TestResults-${{matrix.os.prettyName}}.trx
path: ${{github.workspace}}/TestResults-${{matrix.os.prettyName}}-*.trx
- name: Process Test Results
id: process-trx

View file

@ -8,7 +8,7 @@
<jdbc-url>jdbc:mysql://localhost:3306/lighthouse</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="Lighthouse Production" uuid="b323608d-d984-40d0-942e-2c2ea35006d8">
<data-source source="LOCAL" name="Lighthouse Production" read-only="true" uuid="b323608d-d984-40d0-942e-2c2ea35006d8">
<driver-ref>mariadb</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.mariadb.jdbc.Driver</jdbc-driver>

View file

@ -1,13 +1,14 @@
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Tests;
using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Settings;
using Xunit;
namespace LBPUnion.ProjectLighthouse.Tests
namespace ProjectLighthouse.Tests.GameApiTests
{
public class AuthenticationTests : LighthouseTest
public class AuthenticationTests : LighthouseServerTest
{
[Fact]
public async Task ShouldReturnErrorOnNoPostData()

View file

@ -1,12 +1,14 @@
using System;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Tests;
using LBPUnion.ProjectLighthouse.Types;
using Xunit;
namespace LBPUnion.ProjectLighthouse.Tests
namespace ProjectLighthouse.Tests.GameApiTests
{
public class DatabaseTests : LighthouseTest
public class DatabaseTests : LighthouseServerTest
{
[DatabaseFact]
public async Task CanCreateUserTwice()
@ -21,8 +23,6 @@ namespace LBPUnion.ProjectLighthouse.Tests
Assert.NotNull(userB);
await database.RemoveUser(userA); // Only remove userA since userA and userB are the same user
await database.SaveChangesAsync();
}
}
}

View file

@ -4,12 +4,13 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Tests;
using LBPUnion.ProjectLighthouse.Types;
using Xunit;
namespace LBPUnion.ProjectLighthouse.Tests
namespace ProjectLighthouse.Tests.GameApiTests
{
public class MatchTests : LighthouseTest
public class MatchTests : LighthouseServerTest
{
private static readonly SemaphoreSlim semaphore = new(1, 1);

View file

@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2021.3.0"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0"/>
<PackageReference Include="xunit" Version="2.4.1"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ProjectLighthouse.Tests\ProjectLighthouse.Tests.csproj"/>
<ProjectReference Include="..\ProjectLighthouse\ProjectLighthouse.csproj"/>
</ItemGroup>
<ItemGroup>
<Content Include="..\ProjectLighthouse.Tests\ExampleFiles\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<ProjectReference Include="..\ProjectLighthouse\ProjectLighthouse.csproj"/>
</ItemGroup>
</Project>

View file

@ -1,15 +1,17 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Tests;
using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Profiles;
using Xunit;
namespace LBPUnion.ProjectLighthouse.Tests
namespace ProjectLighthouse.Tests.GameApiTests
{
public class SlotTests : LighthouseTest
public class SlotTests : LighthouseServerTest
{
[DatabaseFact]
public async Task ShouldOnlyShowUsersLevels()

View file

@ -3,11 +3,12 @@ using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Tests;
using Xunit;
namespace LBPUnion.ProjectLighthouse.Tests
namespace ProjectLighthouse.Tests.GameApiTests
{
public class UploadTests : LighthouseTest
public class UploadTests : LighthouseServerTest
{
public UploadTests()
{

View file

@ -0,0 +1,64 @@
using System;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Tests;
using LBPUnion.ProjectLighthouse.Types;
using OpenQA.Selenium;
using Xunit;
namespace ProjectLighthouse.Tests.WebsiteTests
{
public class AdminTests : LighthouseWebTest
{
public const string AdminPanelButtonXPath = "/html/body/div/header/div/div/div/a[2]";
[DatabaseFact]
public async Task ShouldShowAdminPanelButtonWhenAdmin()
{
await using Database database = new();
Random random = new();
User user = await database.CreateUser($"unitTestUser{random.Next()}", HashHelper.BCryptHash("i'm an engineering failure"));
WebToken webToken = new()
{
UserId = user.UserId,
UserToken = HashHelper.GenerateAuthToken(),
};
database.WebTokens.Add(webToken);
user.IsAdmin = true;
await database.SaveChangesAsync();
this.Driver.Navigate().GoToUrl(this.BaseAddress + "/");
this.Driver.Manage().Cookies.AddCookie(new Cookie("LighthouseToken", webToken.UserToken));
this.Driver.Navigate().Refresh();
Assert.Contains("Admin Panel", this.Driver.FindElement(By.XPath(AdminPanelButtonXPath)).Text);
}
[DatabaseFact]
public async Task ShouldNotShowAdminPanelButtonWhenNotAdmin()
{
await using Database database = new();
Random random = new();
User user = await database.CreateUser($"unitTestUser{random.Next()}", HashHelper.BCryptHash("i'm an engineering failure"));
WebToken webToken = new()
{
UserId = user.UserId,
UserToken = HashHelper.GenerateAuthToken(),
};
database.WebTokens.Add(webToken);
user.IsAdmin = false;
await database.SaveChangesAsync();
this.Driver.Navigate().GoToUrl(this.BaseAddress + "/");
this.Driver.Manage().Cookies.AddCookie(new Cookie("LighthouseToken", webToken.UserToken));
this.Driver.Navigate().Refresh();
Assert.DoesNotContain("Admin Panel", this.Driver.FindElement(By.XPath(AdminPanelButtonXPath)).Text);
}
}
}

View file

@ -0,0 +1,106 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Tests;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.EntityFrameworkCore;
using OpenQA.Selenium;
using Xunit;
namespace ProjectLighthouse.Tests.WebsiteTests
{
public class AuthenticationTests : LighthouseWebTest
{
[DatabaseFact]
public async Task ShouldLoginWithPassword()
{
await using Database database = new();
Random random = new();
string password = HashHelper.Sha256Hash(HashHelper.GenerateRandomBytes(64).ToArray());
User user = await database.CreateUser($"unitTestUser{random.Next()}", HashHelper.BCryptHash(HashHelper.Sha256Hash(password)));
this.Driver.Navigate().GoToUrl(this.BaseAddress + "/login");
this.Driver.FindElement(By.Id("text")).SendKeys(user.Username);
this.Driver.FindElement(By.Id("password")).SendKeys(password);
this.Driver.FindElement(By.Id("submit")).Click();
WebToken? webToken = await database.WebTokens.FirstOrDefaultAsync(t => t.UserId == user.UserId);
Assert.NotNull(webToken);
await database.RemoveUser(user);
}
[DatabaseFact]
public async Task ShouldNotLoginWithNoPassword()
{
await using Database database = new();
Random random = new();
User user = await database.CreateUser($"unitTestUser{random.Next()}", HashHelper.BCryptHash("just like the hindenberg,"));
this.Driver.Navigate().GoToUrl(this.BaseAddress + "/login");
this.Driver.FindElement(By.Id("text")).SendKeys(user.Username);
this.Driver.FindElement(By.Id("submit")).Click();
WebToken? webToken = await database.WebTokens.FirstOrDefaultAsync(t => t.UserId == user.UserId);
Assert.Null(webToken);
await database.RemoveUser(user);
}
[DatabaseFact]
public async Task ShouldNotLoginWithWrongPassword()
{
await using Database database = new();
Random random = new();
User user = await database.CreateUser($"unitTestUser{random.Next()}", HashHelper.BCryptHash("i'm an engineering failure"));
this.Driver.Navigate().GoToUrl(this.BaseAddress + "/login");
this.Driver.FindElement(By.Id("text")).SendKeys(user.Username);
this.Driver.FindElement(By.Id("password")).SendKeys("nah man");
this.Driver.FindElement(By.Id("submit")).Click();
WebToken? webToken = await database.WebTokens.FirstOrDefaultAsync(t => t.UserId == user.UserId);
Assert.Null(webToken);
await database.RemoveUser(user);
}
[DatabaseFact]
public async Task ShouldLoginWithInjectedCookie()
{
const string loggedInAsUsernameTextXPath = "/html/body/div/div/div/p[1]/b";
await using Database database = new();
Random random = new();
User user = await database.CreateUser($"unitTestUser{random.Next()}", HashHelper.BCryptHash("i'm an engineering failure"));
WebToken webToken = new()
{
UserId = user.UserId,
UserToken = HashHelper.GenerateAuthToken(),
};
database.WebTokens.Add(webToken);
await database.SaveChangesAsync();
INavigation navigation = this.Driver.Navigate();
navigation.GoToUrl(this.BaseAddress + "/");
this.Driver.Manage().Cookies.AddCookie(new Cookie("LighthouseToken", webToken.UserToken));
Assert.Throws<NoSuchElementException>(() => this.Driver.FindElement(By.XPath(loggedInAsUsernameTextXPath)));
navigation.Refresh();
Assert.True(this.Driver.FindElement(By.XPath(loggedInAsUsernameTextXPath)).Text == user.Username);
await database.RemoveUser(user);
}
}
}

View file

@ -0,0 +1,50 @@
using System;
using System.Linq;
using LBPUnion.ProjectLighthouse;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server.Features;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using Xunit;
namespace ProjectLighthouse.Tests.WebsiteTests
{
[Collection(nameof(LighthouseWebTest))]
public class LighthouseWebTest : IDisposable
{
public readonly IWebHost WebHost = new WebHostBuilder().UseKestrel().UseStartup<TestStartup>().UseWebRoot("StaticFiles").Build();
public readonly string BaseAddress;
public readonly IWebDriver Driver;
public LighthouseWebTest()
{
this.WebHost.Start();
IServerAddressesFeature? serverAddressesFeature = WebHost.ServerFeatures.Get<IServerAddressesFeature>();
if (serverAddressesFeature == null) throw new ArgumentNullException();
this.BaseAddress = serverAddressesFeature.Addresses.First();
ChromeOptions chromeOptions = new();
if (Convert.ToBoolean(Environment.GetEnvironmentVariable("CI") ?? "false"))
{
chromeOptions.AddArgument("headless");
chromeOptions.AddArgument("no-sandbox");
chromeOptions.AddArgument("disable-dev-shm-usage");
Console.WriteLine("We are in a CI environment, so chrome headless mode has been enabled.");
}
this.Driver = new ChromeDriver(chromeOptions);
}
public void Dispose()
{
this.Driver.Close();
this.Driver.Dispose();
this.WebHost.Dispose();
GC.SuppressFinalize(this);
}
}
}

View file

@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2021.3.0"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0"/>
<PackageReference Include="Selenium.WebDriver" Version="4.1.0"/>
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="96.0.4664.4500"/>
<PackageReference Include="xunit" Version="2.4.1"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ProjectLighthouse.Tests\ProjectLighthouse.Tests.csproj"/>
<ProjectReference Include="..\ProjectLighthouse\ProjectLighthouse.csproj"/>
</ItemGroup>
</Project>

View file

@ -0,0 +1,84 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Tests;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.EntityFrameworkCore;
using OpenQA.Selenium;
using Xunit;
namespace ProjectLighthouse.Tests.WebsiteTests
{
public class RegisterTests : LighthouseWebTest
{
[DatabaseFact]
public async Task ShouldRegister()
{
await using Database database = new();
string username = "unitTestUser" + new Random().Next();
string password = HashHelper.Sha256Hash(HashHelper.GenerateRandomBytes(64).ToArray());
this.Driver.Navigate().GoToUrl(this.BaseAddress + "/register");
this.Driver.FindElement(By.Id("text")).SendKeys(username);
this.Driver.FindElement(By.Id("password")).SendKeys(password);
this.Driver.FindElement(By.Id("confirmPassword")).SendKeys(password);
this.Driver.FindElement(By.Id("submit")).Click();
User? user = await database.Users.FirstOrDefaultAsync(u => u.Username == username);
Assert.NotNull(user);
await database.RemoveUser(user);
}
[DatabaseFact]
public async Task ShouldNotRegisterWithMismatchingPasswords()
{
await using Database database = new();
string username = "unitTestUser" + new Random().Next();
string password = HashHelper.Sha256Hash(HashHelper.GenerateRandomBytes(64).ToArray());
this.Driver.Navigate().GoToUrl(this.BaseAddress + "/register");
this.Driver.FindElement(By.Id("text")).SendKeys(username);
this.Driver.FindElement(By.Id("password")).SendKeys(password);
this.Driver.FindElement(By.Id("confirmPassword")).SendKeys(password + "a");
this.Driver.FindElement(By.Id("submit")).Click();
User? user = await database.Users.FirstOrDefaultAsync(u => u.Username == username);
Assert.Null(user);
}
[DatabaseFact]
public async Task ShouldNotRegisterWithTakenUsername()
{
await using Database database = new();
string username = "unitTestUser" + new Random().Next();
string password = HashHelper.Sha256Hash(HashHelper.GenerateRandomBytes(64).ToArray());
await database.CreateUser(username, HashHelper.BCryptHash(password));
User? user = await database.Users.FirstOrDefaultAsync(u => u.Username == username);
Assert.NotNull(user);
this.Driver.Navigate().GoToUrl(this.BaseAddress + "/register");
this.Driver.FindElement(By.Id("text")).SendKeys(username);
this.Driver.FindElement(By.Id("password")).SendKeys(password);
this.Driver.FindElement(By.Id("confirmPassword")).SendKeys(password);
this.Driver.FindElement(By.Id("submit")).Click();
Assert.Contains("The username you've chosen is already taken.", this.Driver.PageSource);
}
}
}

View file

@ -14,15 +14,14 @@ using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Tests
{
[SuppressMessage("ReSharper", "UnusedMember.Global")]
public class LighthouseTest
public class LighthouseServerTest
{
public readonly HttpClient Client;
public readonly TestServer Server;
public LighthouseTest()
public LighthouseServerTest()
{
this.Server = new TestServer(new WebHostBuilder().UseStartup<TestStartup>());
this.Client = this.Server.CreateClient();
}
public async Task<HttpResponseMessage> AuthenticateResponse(int number = -1, bool createUser = true)

View file

@ -32,9 +32,9 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ProjectLighthouse\ProjectLighthouse.csproj"/>
<Content Include="ExampleFiles\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<ProjectReference Include="..\ProjectLighthouse\ProjectLighthouse.csproj"/>
</ItemGroup>
</Project>

View file

@ -4,7 +4,7 @@ using Xunit;
namespace LBPUnion.ProjectLighthouse.Tests
{
public class SerializerTests : LighthouseTest
public class SerializerTests
{
[Fact]
public void BlankElementWorks()

View file

@ -4,6 +4,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectLighthouse", "Projec
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectLighthouse.Tests", "ProjectLighthouse.Tests\ProjectLighthouse.Tests.csproj", "{AFC74569-B289-4ACC-B21C-313A3A62C017}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{D360C08E-EA47-43AC-A566-FDF413442980}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectLighthouse.Tests.GameApiTests", "ProjectLighthouse.Tests.GameApiTests\ProjectLighthouse.Tests.GameApiTests.csproj", "{200EED99-FE3E-45C6-A51E-76ED9819CA2B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectLighthouse.Tests.WebsiteTests", "ProjectLighthouse.Tests.WebsiteTests\ProjectLighthouse.Tests.WebsiteTests.csproj", "{CF65EB5B-5364-4D2A-8639-F147A67F08E7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -17,6 +23,16 @@ Global
{AFC74569-B289-4ACC-B21C-313A3A62C017}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AFC74569-B289-4ACC-B21C-313A3A62C017}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AFC74569-B289-4ACC-B21C-313A3A62C017}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AFC74569-B289-4ACC-B21C-313A3A62C017}.Release|Any CPU.Build.0 = Release|Any CPU
{200EED99-FE3E-45C6-A51E-76ED9819CA2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{200EED99-FE3E-45C6-A51E-76ED9819CA2B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{200EED99-FE3E-45C6-A51E-76ED9819CA2B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CF65EB5B-5364-4D2A-8639-F147A67F08E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CF65EB5B-5364-4D2A-8639-F147A67F08E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CF65EB5B-5364-4D2A-8639-F147A67F08E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{AFC74569-B289-4ACC-B21C-313A3A62C017} = {D360C08E-EA47-43AC-A566-FDF413442980}
{200EED99-FE3E-45C6-A51E-76ED9819CA2B} = {D360C08E-EA47-43AC-A566-FDF413442980}
{CF65EB5B-5364-4D2A-8639-F147A67F08E7} = {D360C08E-EA47-43AC-A566-FDF413442980}
EndGlobalSection
EndGlobal

View file

@ -90,6 +90,7 @@
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Affero/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=airfryer/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=asdf/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=BCAS/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=BCES/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=BCET/@EntryIndexedValue">True</s:Boolean>

View file

@ -73,7 +73,8 @@ namespace LBPUnion.ProjectLighthouse.Controllers
}
User? user = await this.database.UserFromGameToken(token, true);
if (user == null)
if (user == null || user.Banned)
{
Logger.Log("unable to find a user from a token, rejecting login", LoggerLevelLogin.Instance);
return this.StatusCode(403, "");

View file

@ -28,7 +28,7 @@ namespace LBPUnion.ProjectLighthouse.Controllers
User? user = await this.database.UserFromGameRequest(this.Request);
if (user == null) return this.StatusCode(403, "");
return this.Ok(ServerSettings.Instance.EulaText + "\n" + $"{EulaHelper.License}\n");
return this.Ok($"{EulaHelper.License}\n{ServerSettings.Instance.EulaText}");
}
[HttpGet("announce")]
@ -47,19 +47,24 @@ namespace LBPUnion.ProjectLighthouse.Controllers
GameToken gameToken = userAndToken.Value.Item2;
#endif
string announceText = ServerSettings.Instance.AnnounceText;
announceText = announceText.Replace("%user", user.Username);
announceText = announceText.Replace("%id", user.UserId.ToString());
return this.Ok
(
$"You are now logged in as {user.Username}.\n\n" +
announceText +
#if DEBUG
"---DEBUG INFO---\n" +
"\n\n---DEBUG INFO---\n" +
$"user.UserId: {user.UserId}\n" +
$"token.Approved: {gameToken.Approved}\n" +
$"token.Used: {gameToken.Used}\n" +
$"token.UserLocation: {gameToken.UserLocation}\n" +
$"token.GameVersion: {gameToken.GameVersion}\n" +
"---DEBUG INFO---\n\n" +
"---DEBUG INFO---" +
#endif
ServerSettings.Instance.EulaText
"\n"
);
}

View file

@ -91,7 +91,21 @@ namespace LBPUnion.ProjectLighthouse.Controllers
[HttpGet("slots/lbp2cool")]
[HttpGet("slots/cool")]
public async Task<IActionResult> CoolSlots([FromQuery] int page) => await this.LuckyDipSlots(30 * page, 30, 69);
public async Task<IActionResult> CoolSlots
(
[FromQuery] int pageStart,
[FromQuery] int pageSize,
[FromQuery] string gameFilterType,
[FromQuery] int players,
[FromQuery] Boolean move,
[FromQuery] int? page = null
)
{
int _pageStart = pageStart;
if (page != null) _pageStart = (int)page * 30;
// bit of a better placeholder until we can track average user interaction with /stream endpoint
return await ThumbsSlots(_pageStart, Math.Min(pageSize, 30), gameFilterType, players, move, "thisWeek");
}
[HttpGet("slots")]
public async Task<IActionResult> NewestSlots([FromQuery] int pageStart, [FromQuery] int pageSize)
@ -198,5 +212,137 @@ namespace LBPUnion.ProjectLighthouse.Controllers
)
);
}
[HttpGet("slots/thumbs")]
public async Task<IActionResult> ThumbsSlots
(
[FromQuery] int pageStart,
[FromQuery] int pageSize,
[FromQuery] string gameFilterType,
[FromQuery] int players,
[FromQuery] Boolean move,
[FromQuery] string? dateFilterType = null
)
{
Random rand = new();
IEnumerable<Slot> slots = FilterByRequest(gameFilterType, dateFilterType)
.AsEnumerable()
.OrderByDescending(s => s.Thumbsup)
.ThenBy(_ => rand.Next())
.Skip(pageStart - 1)
.Take(Math.Min(pageSize, 30));
string response = slots.Aggregate(string.Empty, (current, slot) => current + slot.Serialize());
return this.Ok(LbpSerializer.TaggedStringElement("slots", response, "hint_start", pageStart + Math.Min(pageSize, 30)));
}
[HttpGet("slots/mostUniquePlays")]
public async Task<IActionResult> MostUniquePlaysSlots
(
[FromQuery] int pageStart,
[FromQuery] int pageSize,
[FromQuery] string gameFilterType,
[FromQuery] int players,
[FromQuery] Boolean move,
[FromQuery] string? dateFilterType = null
)
{
Random rand = new();
IEnumerable<Slot> slots = FilterByRequest(gameFilterType, dateFilterType)
.AsEnumerable()
.OrderByDescending
(
s =>
{
// probably not the best way to do this?
return GetGameFilter(gameFilterType) switch
{
GameVersion.LittleBigPlanet1 => s.PlaysLBP1Unique,
GameVersion.LittleBigPlanet2 => s.PlaysLBP2Unique,
GameVersion.LittleBigPlanet3 => s.PlaysLBP3Unique,
GameVersion.LittleBigPlanetVita => s.PlaysLBPVitaUnique,
_ => s.PlaysUnique,
};
}
)
.ThenBy(_ => rand.Next())
.Skip(pageStart - 1)
.Take(Math.Min(pageSize, 30));
string response = slots.Aggregate(string.Empty, (current, slot) => current + slot.Serialize());
return this.Ok(LbpSerializer.TaggedStringElement("slots", response, "hint_start", pageStart + Math.Min(pageSize, 30)));
}
[HttpGet("slots/mostHearted")]
public async Task<IActionResult> MostHeartedSlots
(
[FromQuery] int pageStart,
[FromQuery] int pageSize,
[FromQuery] string gameFilterType,
[FromQuery] int players,
[FromQuery] Boolean move,
[FromQuery] string? dateFilterType = null
)
{
Random rand = new();
IEnumerable<Slot> slots = FilterByRequest(gameFilterType, dateFilterType)
.AsEnumerable()
.OrderByDescending(s => s.Hearts)
.ThenBy(_ => rand.Next())
.Skip(pageStart - 1)
.Take(Math.Min(pageSize, 30));
string response = slots.Aggregate(string.Empty, (current, slot) => current + slot.Serialize());
return this.Ok(LbpSerializer.TaggedStringElement("slots", response, "hint_start", pageStart + Math.Min(pageSize, 30)));
}
public GameVersion GetGameFilter(string gameFilterType)
{
return gameFilterType switch
{
"lbp1" => GameVersion.LittleBigPlanet1,
"lbp2" => GameVersion.LittleBigPlanet2,
"lbp3" => GameVersion.LittleBigPlanet3,
"both" => GameVersion.LittleBigPlanet2, // LBP2 default option
_ => GameVersion.Unknown,
};
}
public IQueryable<Slot> FilterByRequest(string gameFilterType, string? dateFilterType)
{
string _dateFilterType = dateFilterType ?? "";
long oldestTime = _dateFilterType switch
{
"thisWeek" => DateTimeOffset.Now.AddDays(-7).ToUnixTimeMilliseconds(),
"thisMonth" => DateTimeOffset.Now.AddDays(-31).ToUnixTimeMilliseconds(),
_ => 0,
};
GameVersion gameVersion = GetGameFilter(gameFilterType);
IQueryable<Slot> whereSlots;
// ReSharper disable once ConvertIfStatementToConditionalTernaryExpression
if (gameFilterType == "both")
{
// Get game versions less than the current version
// Needs support for LBP3 ("both" = LBP1+2)
whereSlots = this.database.Slots.Where(s => s.GameVersion <= gameVersion && s.FirstUploaded >= oldestTime);
}
else
{
// Get game versions exactly equal to gamefiltertype
whereSlots = this.database.Slots.Where(s => s.GameVersion == gameVersion && s.FirstUploaded >= oldestTime);
}
return whereSlots.Include(s => s.Creator).Include(s => s.Location);
}
}
}

View file

@ -19,7 +19,7 @@ namespace LBPUnion.ProjectLighthouse.Controllers.Website.Admin
this.database = database;
}
[Route("teamPick")]
[HttpGet("teamPick")]
public async Task<IActionResult> TeamPick([FromRoute] int id)
{
User? user = this.database.UserFromWebRequest(this.Request);
@ -35,7 +35,7 @@ namespace LBPUnion.ProjectLighthouse.Controllers.Website.Admin
return this.Ok();
}
[Route("removeTeamPick")]
[HttpGet("removeTeamPick")]
public async Task<IActionResult> RemoveTeamPick([FromRoute] int id)
{
User? user = this.database.UserFromWebRequest(this.Request);
@ -51,7 +51,7 @@ namespace LBPUnion.ProjectLighthouse.Controllers.Website.Admin
return this.Ok();
}
[Route("delete")]
[HttpGet("delete")]
public async Task<IActionResult> DeleteLevel([FromRoute] int id)
{
User? user = this.database.UserFromWebRequest(this.Request);

View file

@ -0,0 +1,37 @@
#nullable enable
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Controllers.Website.Admin
{
[ApiController]
[Route("admin/user/{id:int}")]
public class AdminUserController : ControllerBase
{
private readonly Database database;
public AdminUserController(Database database)
{
this.database = database;
}
[HttpGet("unban")]
public async Task<IActionResult> UnbanUser([FromRoute] int id)
{
User? user = this.database.UserFromWebRequest(this.Request);
if (user == null || !user.IsAdmin) return this.NotFound();
User? targetedUser = await this.database.Users.FirstOrDefaultAsync(u => u.UserId == id);
;
if (targetedUser == null) return this.NotFound();
targetedUser.Banned = false;
targetedUser.BannedReason = null;
await this.database.SaveChangesAsync();
return this.Redirect($"/user/{targetedUser.UserId}");
}
}
}

View file

@ -11,11 +11,11 @@ namespace LBPUnion.ProjectLighthouse.Maintenance.Commands
public class DeleteUserCommand : ICommand
{
private readonly Database database = new();
public string Name() => "Delete/Ban User";
public string Name() => "Delete User";
public string[] Aliases()
=> new[]
{
"deleteUser", "wipeUser", "banUser",
"deleteUser", "wipeUser",
};
public string Arguments() => "<username/userId>";
public int RequiredArgs() => 1;

View file

@ -0,0 +1,41 @@
using LBPUnion.ProjectLighthouse;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ProjectLighthouse.Migrations
{
[DbContext(typeof(Database))]
[Migration("20211217000749_AddBannedPropertiesToUser")]
public partial class AddBannedPropertiesToUser : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "Banned",
table: "Users",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(
name: "BannedReason",
table: "Users",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Banned",
table: "Users");
migrationBuilder.DropColumn(
name: "BannedReason",
table: "Users");
}
}
}

View file

@ -546,6 +546,12 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<bool>("Banned")
.HasColumnType("tinyint(1)");
b.Property<string>("BannedReason")
.HasColumnType("longtext");
b.Property<string>("Biography")
.HasColumnType("longtext");

View file

@ -0,0 +1,20 @@
@page "/admin/user/{id:int}/ban"
@model LBPUnion.ProjectLighthouse.Pages.Admin.AdminBanUserPage
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Ban " + Model.TargetedUser!.Username + "?";
}
<p>Are you sure you want to ban this user?</p>
<form method="post">
@Html.AntiForgeryToken()
<div class="ui left labeled input">
<label for="text" class="ui blue label">Reason: </label>
<input type="text" name="reason" id="text">
</div><br><br>
<input type="submit" value="Yes, ban @Model.TargetedUser.Username!" id="submit" class="ui red button"><br>
</form>

View file

@ -0,0 +1,49 @@
#nullable enable
using System.Linq;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Pages.Admin;
public class AdminBanUserPage : BaseLayout
{
public AdminBanUserPage(Database database) : base(database)
{}
public User? TargetedUser;
public async Task<IActionResult> OnGet([FromRoute] int id)
{
User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null || !user.IsAdmin) return this.NotFound();
this.TargetedUser = await this.Database.Users.FirstOrDefaultAsync(u => u.UserId == id);
if (this.TargetedUser == null) return this.NotFound();
return this.Page();
}
public async Task<IActionResult> OnPost([FromRoute] int id, string reason)
{
User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null || !user.IsAdmin) return this.NotFound();
this.TargetedUser = await this.Database.Users.FirstOrDefaultAsync(u => u.UserId == id);
if (this.TargetedUser == null) return this.NotFound();
this.TargetedUser.Banned = true;
this.TargetedUser.BannedReason = reason;
// invalidate all currently active gametokens
this.Database.GameTokens.RemoveRange(this.Database.GameTokens.Where(t => t.UserId == this.TargetedUser.UserId));
// invalidate all currently active webtokens
this.Database.WebTokens.RemoveRange(this.Database.WebTokens.Where(t => t.UserId == this.TargetedUser.UserId));
await this.Database.SaveChangesAsync();
return this.Redirect($"/user/{this.TargetedUser.UserId}");
}
}

View file

@ -1,7 +1,7 @@
@page "/admin"
@using LBPUnion.ProjectLighthouse.Helpers
@using LBPUnion.ProjectLighthouse.Maintenance
@model LBPUnion.ProjectLighthouse.Pages.AdminPanelPage
@model LBPUnion.ProjectLighthouse.Pages.Admin.AdminPanelPage
@{
Layout = "Layouts/BaseLayout";

View file

@ -7,7 +7,7 @@ using LBPUnion.ProjectLighthouse.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Pages
namespace LBPUnion.ProjectLighthouse.Pages.Admin
{
public class AdminPanelPage : BaseLayout
{

View file

@ -24,7 +24,7 @@
<div class="header">
Uh oh!
</div>
<p>@Model.Error</p>
<p style="white-space: pre-line">@Model.Error</p>
</div>
}

View file

@ -1,7 +1,9 @@
#nullable enable
using System.Threading.Tasks;
using JetBrains.Annotations;
using Kettu;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
@ -36,16 +38,25 @@ namespace LBPUnion.ProjectLighthouse.Pages
User? user = await this.Database.Users.FirstOrDefaultAsync(u => u.Username == username);
if (user == null)
{
Logger.Log($"User {username} failed to login on web due to invalid username", LoggerLevelLogin.Instance);
this.Error = "The username or password you entered is invalid.";
return this.Page();
}
if (!BCrypt.Net.BCrypt.Verify(password, user.Password))
{
Logger.Log($"User {user.Username} (id: {user.UserId}) failed to login on web due to invalid password", LoggerLevelLogin.Instance);
this.Error = "The username or password you entered is invalid.";
return this.Page();
}
if (user.Banned)
{
Logger.Log($"User {user.Username} (id: {user.UserId}) failed to login on web due to being banned", LoggerLevelLogin.Instance);
this.Error = "You have been banned. Please contact an administrator for more information.\nReason: " + user.BannedReason;
return this.Page();
}
WebToken webToken = new()
{
UserId = user.UserId,

View file

@ -6,7 +6,7 @@
Model.Title = "Password Reset Required";
}
<p>An admin has deemed it necessary that you reset your password. Please do so.</p>
<p>An administrator has deemed it necessary that you reset your password. Please do so.</p>
<a href="/passwordReset">
<div class="ui blue button">Reset Password</div>

View file

@ -14,12 +14,34 @@
Model.Description = Model.ProfileUser!.Biography;
}
@if (Model.ProfileUser.Banned)
{
<div class="ui inverted red segment">
<p>
<b>User is currently banned!</b>
</p>
@if (Model.User != null && Model.User.IsAdmin)
{
<p>Reason: @Model.ProfileUser.BannedReason</p>
<a class="ui inverted button" href="/admin/user/@Model.ProfileUser.UserId/unban">
<i class="ban icon"></i>
<span>Unban User</span>
</a>
}
else
{
<p>For shame...</p>
}
</div>
}
<div class="ui grid">
<div class="eight wide column">
<h1>@Model.Title</h1>
<p>
<i>@Model.ProfileUser!.Status</i>
</p>
<div class="statsUnderTitle">
<i class="pink heart icon" title="Hearts"></i> <span>@Model.ProfileUser.Hearts</span>
<i class="blue comment icon" title="Comments"></i> <span>@Model.ProfileUser.Comments</span>
@ -53,6 +75,13 @@
<span>Reset Password</span>
</a>
}
@if (Model.User != null && Model.User.IsAdmin && !Model.ProfileUser.Banned)
{
<a class="ui red button" href="/admin/user/@Model.ProfileUser.UserId/ban">
<i class="ban icon"></i>
<span>Ban User</span>
</a>
}
</div>
<div class="eight wide column">
<div class="ui blue segment">

View file

@ -87,11 +87,34 @@ namespace LBPUnion.ProjectLighthouse
Stopwatch requestStopwatch = new();
requestStopwatch.Start();
context.Request.EnableBuffering(); // Allows us to reset the position of Request.Body for later logging
// Log all headers.
// foreach (KeyValuePair<string, StringValues> header in context.Request.Headers) Logger.Log($"{header.Key}: {header.Value}");
context.Request.EnableBuffering(); // Allows us to reset the position of Request.Body for later logging
await next(context); // Handle the request so we can get the status code from it
requestStopwatch.Stop();
Logger.Log
(
$"{context.Response.StatusCode}, {requestStopwatch.ElapsedMilliseconds}ms: {context.Request.Method} {context.Request.Path}{context.Request.QueryString}",
LoggerLevelHttp.Instance
);
if (context.Request.Method == "POST")
{
context.Request.Body.Position = 0;
Logger.Log(await new StreamReader(context.Request.Body).ReadToEndAsync(), LoggerLevelHttp.Instance);
}
}
);
// Digest check
app.Use
(
async (context, next) =>
{
// Client digest check.
if (!context.Request.Cookies.TryGetValue("MM_AUTH", out string authCookie)) authCookie = string.Empty;
string digestPath = context.Request.Path;
@ -119,7 +142,7 @@ namespace LBPUnion.ProjectLighthouse
Stream oldResponseStream = context.Response.Body;
context.Response.Body = responseBuffer;
await next(); // Handle the request so we can get the status code from it
await next(context); // Handle the request so we can get the server digest hash
// Compute the server digest hash.
if (computeDigests)
@ -138,7 +161,13 @@ namespace LBPUnion.ProjectLighthouse
responseBuffer.Position = 0;
await responseBuffer.CopyToAsync(oldResponseStream);
context.Response.Body = oldResponseStream;
}
);
app.Use
(
async (context, next) =>
{
#nullable enable
// Log LastContact for LBP1. This is done on LBP2/3/V on a Match request.
if (context.Request.Path.ToString().StartsWith("/LITTLEBIGPLANETPS3_XML"))
@ -153,19 +182,7 @@ namespace LBPUnion.ProjectLighthouse
}
#nullable disable
requestStopwatch.Stop();
Logger.Log
(
$"{context.Response.StatusCode}, {requestStopwatch.ElapsedMilliseconds}ms: {context.Request.Method} {context.Request.Path}{context.Request.QueryString}",
LoggerLevelHttp.Instance
);
if (context.Request.Method == "POST")
{
context.Request.Body.Position = 0;
Logger.Log(await new StreamReader(context.Request.Body).ReadToEndAsync(), LoggerLevelHttp.Instance);
}
await next(context);
}
);

View file

@ -12,7 +12,7 @@ namespace LBPUnion.ProjectLighthouse.Types.Settings
public class ServerSettings
{
public const int CurrentConfigVersion = 13; // MUST BE INCREMENTED FOR EVERY CONFIG CHANGE!
public const int CurrentConfigVersion = 14; // MUST BE INCREMENTED FOR EVERY CONFIG CHANGE!
static ServerSettings()
{
if (ServerStatics.IsUnitTesting) return; // Unit testing, we don't want to read configurations here since the tests will provide their own
@ -74,6 +74,12 @@ namespace LBPUnion.ProjectLighthouse.Types.Settings
public string EulaText { get; set; } = "";
#if !DEBUG
public string AnnounceText { get; set; } = "You are now logged in as %user.";
#else
public string AnnounceText { get; set; } = "You are now logged in as %user (id: %id).";
#endif
public string DbConnectionString { get; set; } = "server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse";
public string ExternalUrl { get; set; } = "http://localhost:10060";

View file

@ -125,6 +125,10 @@ namespace LBPUnion.ProjectLighthouse.Types
}
#nullable disable
public bool Banned { get; set; }
public string BannedReason { get; set; }
public string Serialize(GameVersion gameVersion = GameVersion.LittleBigPlanet1)
{
string user = LbpSerializer.TaggedStringElement("npHandle", this.Username, "icon", this.IconHash) +

View file

@ -1,7 +1,6 @@
# Project Lighthouse
Project Lighthouse is an umbrella project for all work to investigate and develop private servers for LittleBigPlanet.
This project is the main server component that LittleBigPlanet games connect to.
Project Lighthouse is a clean-room, open-source custom server for LittleBigPlanet. This is a project conducted by the [LBP Union Ministry of Technology Research and Development team.](https://www.lbpunion.com/technology) For concerns and inquiries about the project, please [contact us here.](https://www.lbpunion.com/contact) For general questions and discussion about Project Lighthouse, please see the [megathread](https://www.lbpunion.com/forum/union-hall/project-lighthouse-littlebigplanet-private-servers-megathread) on our forum.
## WARNING!